@invisibleloop/pulse 0.1.21
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/.claude/commands/build-page.md +59 -0
- package/.claude/commands/new-doc-page.md +45 -0
- package/.claude/commands/verify.md +52 -0
- package/.claude/pulse-checklist.md +111 -0
- package/.claude/settings.local.json +102 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.pulse/load-reports/home/1773432711417.json +22 -0
- package/CLAUDE.md +383 -0
- package/README.md +95 -0
- package/docs/.claude/pulse-checklist.md +111 -0
- package/docs/public/.pulse-ui-version +1 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
- package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
- package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
- package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
- package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
- package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
- package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
- package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
- package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
- package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
- package/docs/public/dist/docs-8e3d4b5c.css +1 -0
- package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
- package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
- package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
- package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
- package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
- package/docs/public/dist/manifest.json +94 -0
- package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
- package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
- package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
- package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
- package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
- package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
- package/docs/public/dist/runtime-B73WLANC.js +1 -0
- package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
- package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
- package/docs/public/dist/runtime-QFURDKA2.js +5 -0
- package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
- package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
- package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
- package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
- package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
- package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
- package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
- package/docs/public/docs.css +2020 -0
- package/docs/public/menu.js +83 -0
- package/docs/public/pulse-ui.css +2739 -0
- package/docs/public/pulse-ui.js +236 -0
- package/docs/server.js +192 -0
- package/docs/src/lib/component-page.js +47 -0
- package/docs/src/lib/highlight.js +255 -0
- package/docs/src/lib/layout.js +131 -0
- package/docs/src/lib/metrics-store.js +6 -0
- package/docs/src/lib/nav.js +159 -0
- package/docs/src/lib/stats.js +81 -0
- package/docs/src/pages/accessibility.js +157 -0
- package/docs/src/pages/actions.js +191 -0
- package/docs/src/pages/auth.js +177 -0
- package/docs/src/pages/caching.js +95 -0
- package/docs/src/pages/components/accordion.js +48 -0
- package/docs/src/pages/components/alert.js +35 -0
- package/docs/src/pages/components/app-badge.js +41 -0
- package/docs/src/pages/components/avatar.js +35 -0
- package/docs/src/pages/components/badge.js +36 -0
- package/docs/src/pages/components/banner.js +45 -0
- package/docs/src/pages/components/breadcrumbs.js +94 -0
- package/docs/src/pages/components/button.js +84 -0
- package/docs/src/pages/components/card.js +225 -0
- package/docs/src/pages/components/carousel.js +72 -0
- package/docs/src/pages/components/charts.js +278 -0
- package/docs/src/pages/components/checkbox.js +129 -0
- package/docs/src/pages/components/cluster.js +47 -0
- package/docs/src/pages/components/code-window.js +57 -0
- package/docs/src/pages/components/container.js +40 -0
- package/docs/src/pages/components/cta.js +53 -0
- package/docs/src/pages/components/divider.js +37 -0
- package/docs/src/pages/components/empty.js +36 -0
- package/docs/src/pages/components/feature.js +60 -0
- package/docs/src/pages/components/fieldset.js +65 -0
- package/docs/src/pages/components/fileupload.js +127 -0
- package/docs/src/pages/components/footer.js +58 -0
- package/docs/src/pages/components/grid.js +165 -0
- package/docs/src/pages/components/heading.js +107 -0
- package/docs/src/pages/components/hero.js +65 -0
- package/docs/src/pages/components/icons.js +285 -0
- package/docs/src/pages/components/image.js +71 -0
- package/docs/src/pages/components/input.js +51 -0
- package/docs/src/pages/components/list.js +112 -0
- package/docs/src/pages/components/media.js +51 -0
- package/docs/src/pages/components/modal.js +111 -0
- package/docs/src/pages/components/nav.js +86 -0
- package/docs/src/pages/components/pricing.js +68 -0
- package/docs/src/pages/components/progress.js +102 -0
- package/docs/src/pages/components/prose.js +111 -0
- package/docs/src/pages/components/pullquote.js +71 -0
- package/docs/src/pages/components/radio.js +194 -0
- package/docs/src/pages/components/rating.js +106 -0
- package/docs/src/pages/components/search.js +61 -0
- package/docs/src/pages/components/section.js +59 -0
- package/docs/src/pages/components/segmented.js +121 -0
- package/docs/src/pages/components/select.js +45 -0
- package/docs/src/pages/components/slider.js +114 -0
- package/docs/src/pages/components/spinner.js +73 -0
- package/docs/src/pages/components/stack.js +48 -0
- package/docs/src/pages/components/stat.js +55 -0
- package/docs/src/pages/components/stepper.js +66 -0
- package/docs/src/pages/components/table.js +45 -0
- package/docs/src/pages/components/testimonial.js +49 -0
- package/docs/src/pages/components/textarea.js +31 -0
- package/docs/src/pages/components/timeline.js +227 -0
- package/docs/src/pages/components/toggle.js +84 -0
- package/docs/src/pages/components/tooltip.js +48 -0
- package/docs/src/pages/components.js +204 -0
- package/docs/src/pages/config.js +193 -0
- package/docs/src/pages/constraints.js +99 -0
- package/docs/src/pages/deploy.js +233 -0
- package/docs/src/pages/extending.js +198 -0
- package/docs/src/pages/faq.js +96 -0
- package/docs/src/pages/getting-started.js +106 -0
- package/docs/src/pages/guard.js +121 -0
- package/docs/src/pages/home.js +401 -0
- package/docs/src/pages/how-it-works.js +183 -0
- package/docs/src/pages/hydration.js +98 -0
- package/docs/src/pages/images.js +121 -0
- package/docs/src/pages/meta.js +120 -0
- package/docs/src/pages/mutations.js +106 -0
- package/docs/src/pages/navigation.js +85 -0
- package/docs/src/pages/performance.js +157 -0
- package/docs/src/pages/persist.js +88 -0
- package/docs/src/pages/project-structure.js +90 -0
- package/docs/src/pages/prompt-examples.js +186 -0
- package/docs/src/pages/raw-responses.js +124 -0
- package/docs/src/pages/routing.js +99 -0
- package/docs/src/pages/server-api.js +281 -0
- package/docs/src/pages/server-data.js +185 -0
- package/docs/src/pages/slash-commands.js +55 -0
- package/docs/src/pages/spec.js +207 -0
- package/docs/src/pages/state.js +101 -0
- package/docs/src/pages/store.js +181 -0
- package/docs/src/pages/streaming.js +108 -0
- package/docs/src/pages/stripe.js +193 -0
- package/docs/src/pages/supabase.js +323 -0
- package/docs/src/pages/testing.js +198 -0
- package/docs/src/pages/validation.js +138 -0
- package/examples/contact.js +166 -0
- package/examples/counter.js +94 -0
- package/examples/dev.server.js +91 -0
- package/examples/examples.test.js +394 -0
- package/examples/pricing.js +244 -0
- package/examples/products.js +191 -0
- package/examples/quiz.js +208 -0
- package/examples/shared.js +78 -0
- package/examples/todos.js +162 -0
- package/package.json +75 -0
- package/public/.pulse-ui-version +1 -0
- package/public/chippy-bird.css +246 -0
- package/public/examples/contact.css +119 -0
- package/public/examples/counter.css +79 -0
- package/public/examples/pricing.css +132 -0
- package/public/examples/products.css +100 -0
- package/public/examples/quiz.css +200 -0
- package/public/examples/todos.css +137 -0
- package/public/favicon.ico +0 -0
- package/public/log-dashboard.css +383 -0
- package/public/pulse-ui.css +2740 -0
- package/public/pulse-ui.js +236 -0
- package/public/pulse.css +149 -0
- package/scripts/build.js +411 -0
- package/src/agent/checklist.md +111 -0
- package/src/agent/coverage-check.js +66 -0
- package/src/agent/guide-components.md +274 -0
- package/src/agent/guide-examples.md +54 -0
- package/src/agent/guide-routing.md +36 -0
- package/src/agent/guide-server.md +258 -0
- package/src/agent/guide-spec.md +103 -0
- package/src/agent/guide-styles.md +191 -0
- package/src/agent/guide.md +979 -0
- package/src/agent/identity.md +106 -0
- package/src/agent/workflow.md +108 -0
- package/src/cli/cli.test.js +82 -0
- package/src/cli/dev.js +195 -0
- package/src/cli/discover.js +113 -0
- package/src/cli/index.js +361 -0
- package/src/cli/load-report.js +91 -0
- package/src/cli/load-runner.js +121 -0
- package/src/cli/report-server.js +723 -0
- package/src/cli/report.js +116 -0
- package/src/cli/scaffold.archive.js +1371 -0
- package/src/cli/scaffold.js +349 -0
- package/src/cli/start.js +74 -0
- package/src/html.js +19 -0
- package/src/mcp/server.js +884 -0
- package/src/mcp/validate-worker.js +110 -0
- package/src/runtime/image.js +74 -0
- package/src/runtime/image.test.js +111 -0
- package/src/runtime/index.js +621 -0
- package/src/runtime/navigate.js +146 -0
- package/src/runtime/runtime.test.js +773 -0
- package/src/runtime/ssr.js +464 -0
- package/src/runtime/ssr.test.js +421 -0
- package/src/runtime/store.js +92 -0
- package/src/runtime/toast.js +163 -0
- package/src/server/index.js +1386 -0
- package/src/server/server.test.js +1248 -0
- package/src/spec/schema.js +428 -0
- package/src/spec/schema.test.js +291 -0
- package/src/store/index.js +102 -0
- package/src/store/store.test.js +210 -0
- package/src/testing/html.js +283 -0
- package/src/testing/index.js +249 -0
- package/src/testing/testing.test.js +450 -0
- package/src/ui/accordion.js +28 -0
- package/src/ui/alert.js +43 -0
- package/src/ui/app-badge.js +48 -0
- package/src/ui/avatar.js +47 -0
- package/src/ui/badge.js +24 -0
- package/src/ui/banner.js +26 -0
- package/src/ui/breadcrumbs.js +38 -0
- package/src/ui/button.js +66 -0
- package/src/ui/card.js +34 -0
- package/src/ui/carousel.js +59 -0
- package/src/ui/charts.js +321 -0
- package/src/ui/checkbox.js +65 -0
- package/src/ui/cluster.js +44 -0
- package/src/ui/code-window.js +39 -0
- package/src/ui/container.js +24 -0
- package/src/ui/cta.js +37 -0
- package/src/ui/divider.js +29 -0
- package/src/ui/empty.js +33 -0
- package/src/ui/feature.js +33 -0
- package/src/ui/fieldset.js +37 -0
- package/src/ui/fileupload.js +89 -0
- package/src/ui/footer.js +38 -0
- package/src/ui/grid.js +36 -0
- package/src/ui/heading.js +45 -0
- package/src/ui/hero.js +37 -0
- package/src/ui/icons.js +161 -0
- package/src/ui/index.js +89 -0
- package/src/ui/input.js +74 -0
- package/src/ui/list.js +36 -0
- package/src/ui/media.js +44 -0
- package/src/ui/modal.js +80 -0
- package/src/ui/nav.js +61 -0
- package/src/ui/pricing.js +56 -0
- package/src/ui/progress.js +62 -0
- package/src/ui/prose.js +29 -0
- package/src/ui/pullquote.js +34 -0
- package/src/ui/radio.js +102 -0
- package/src/ui/rating.js +93 -0
- package/src/ui/search.js +77 -0
- package/src/ui/section.js +69 -0
- package/src/ui/segmented.js +50 -0
- package/src/ui/select.js +77 -0
- package/src/ui/slider.js +84 -0
- package/src/ui/spinner.js +34 -0
- package/src/ui/stack.js +36 -0
- package/src/ui/stat.js +52 -0
- package/src/ui/stepper.js +46 -0
- package/src/ui/switch.js +57 -0
- package/src/ui/table.js +45 -0
- package/src/ui/testimonial.js +48 -0
- package/src/ui/textarea.js +72 -0
- package/src/ui/timeline.js +72 -0
- package/src/ui/tooltip.js +28 -0
- package/src/ui/ui.test.js +1241 -0
- package/src/ui/uiimage.js +65 -0
- package/tsconfig.json +13 -0
- package/types/html.d.ts +17 -0
- package/types/image.d.ts +70 -0
- package/types/index.d.ts +7 -0
- package/types/navigate.d.ts +38 -0
- package/types/runtime.d.ts +63 -0
- package/types/schema.d.ts +243 -0
- package/types/server.d.ts +145 -0
- package/types/ssr.d.ts +110 -0
- package/types/testing.d.ts +154 -0
- package/types/ui.d.ts +704 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import{a as t}from"./runtime-QFURDKA2.js";import{a as n,b as i,c as l,d as p,e,g as o,h as r,i as s}from"./runtime-L2HNXIHW.js";import{a,b as u}from"./runtime-B73WLANC.js";var{prev:h,next:m}=n("/server-api"),d={route:"/server-api",meta:{title:"Server API \u2014 Pulse Docs",description:"Complete reference for createServer \u2014 all options, hooks, and response behaviour.",styles:["/docs.css"]},state:{},view:()=>i({currentHref:"/server-api",prev:h,next:m,content:`
|
|
2
|
+
${l("Server API")}
|
|
3
|
+
${p("<code>createServer(specs, options)</code> starts an HTTP server with all guarantees active. Specs are validated before the server accepts connections. SSR, streaming, brotli compression, immutable asset caching, security headers, CSP nonces, and HSTS are all handled automatically.")}
|
|
4
|
+
|
|
5
|
+
${e("signature","createServer(specs, options)")}
|
|
6
|
+
${o(t(`import { createServer } from '@invisibleloop/pulse'
|
|
7
|
+
|
|
8
|
+
createServer(specs, options)`,"js"))}
|
|
9
|
+
${r(["Parameter","Type","Description"],[["<code>specs</code>","<code>Spec[]</code>","Array of page spec objects. Validated at startup \u2014 a bad spec throws before the server accepts connections."],["<code>options</code>","<code>object</code>","Server configuration options (see below)."]])}
|
|
10
|
+
|
|
11
|
+
${e("options","Options")}
|
|
12
|
+
${r(["Option","Type","Default","Description"],[["<code>port</code>","<code>number</code>","<code>3000</code>","Port to listen on."],["<code>stream</code>","<code>boolean</code>","<code>true</code>","Enable streaming SSR globally. Individual specs also declare a <code>stream</code> field to opt in."],["<code>staticDir</code>","<code>string</code>","<code>undefined</code>","Path to a directory of static files to serve. Relative to the process working directory."],["<code>manifest</code>","<code>string | object</code>","<code>null</code>","Explicit manifest path or object. Overrides auto-detection from <code>staticDir/dist/manifest.json</code>."],["<code>trailingSlash</code>",'<code>"remove" | "add" | "allow"</code>','<code>"remove"</code>','<code>"remove"</code> \u2014 301 redirect <code>/about/</code> \u2192 <code>/about</code>. <code>"add"</code> \u2014 301 redirect <code>/about</code> \u2192 <code>/about/</code>. <code>"allow"</code> \u2014 serve both, no redirect.'],["<code>store</code>","<code>object</code>","<code>null</code>",'Global store definition (default export from <code>pulse.store.js</code>). See <a href="/store">Global Store</a>.'],["<code>maxBody</code>","<code>number</code>","<code>1048576</code>","Maximum request body size in bytes (default 1 MB). Requests exceeding this limit receive a 413 response."],["<code>defaultCache</code>","<code>boolean | number | object</code>","<code>null</code>","Default HTML cache TTL for all pages in production. <code>true</code> = 1 h + 24 h SWR. A number sets <code>max-age</code> in seconds. An object accepts <code>{ public, maxAge, staleWhileRevalidate }</code>. <code>spec.cache</code> overrides per-page."],["<code>fetcherTimeout</code>","<code>number</code>","<code>null</code>","Global timeout in milliseconds for all server fetchers. A fetcher that does not resolve within this limit rejects with a timeout error (\u2192 500). Override per page with <code>spec.serverTimeout</code>."],["<code>shutdownTimeout</code>","<code>number</code>","<code>30000</code>",'Milliseconds to wait for in-flight requests to finish during graceful shutdown before force-exiting. See <a href="#graceful-shutdown">Graceful shutdown</a>.'],["<code>healthCheck</code>","<code>string | false</code>",'<code>"/healthz"</code>','Path for the built-in health check endpoint. Returns <code>{ status: "ok", uptime }</code>. Set to <code>false</code> to disable. The endpoint bypasses <code>onRequest</code> so load balancers always get a response.'],["<code>resolveBrand</code>","<code>async (host) => any</code>","<code>undefined</code>","Multi-brand support. Called once per host (cached 60s). Result is attached to <code>ctx.brand</code> and available in <code>guard</code>, <code>server</code>, and <code>meta</code> functions."],["<code>onRequest</code>","<code>function</code>","<code>undefined</code>","Called on every request before routing. Return <code>false</code> to short-circuit Pulse handling."],["<code>onError</code>","<code>function</code>","<code>undefined</code>","Called on unhandled errors. Receives <code>(err, req, res)</code>."]])}
|
|
13
|
+
|
|
14
|
+
${e("example","Full example")}
|
|
15
|
+
${o(t(`import { createServer } from '@invisibleloop/pulse'
|
|
16
|
+
import home from './src/pages/home.js'
|
|
17
|
+
import contact from './src/pages/contact.js'
|
|
18
|
+
|
|
19
|
+
createServer([home, contact], {
|
|
20
|
+
port: 3000,
|
|
21
|
+
stream: true,
|
|
22
|
+
staticDir: 'public',
|
|
23
|
+
onRequest: (req, res) => {
|
|
24
|
+
// Add custom headers
|
|
25
|
+
res.setHeader('X-My-Header', 'my-value')
|
|
26
|
+
// Return false to block a request
|
|
27
|
+
if (req.url.startsWith('/admin') && !isAuthenticated(req)) {
|
|
28
|
+
res.writeHead(401)
|
|
29
|
+
res.end('Unauthorized')
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
// Return undefined (or nothing) to let Pulse handle it
|
|
33
|
+
},
|
|
34
|
+
onError: (err, req, res) => {
|
|
35
|
+
console.error(err)
|
|
36
|
+
if (!res.headersSent) {
|
|
37
|
+
res.writeHead(500, { 'Content-Type': 'text/html' })
|
|
38
|
+
res.end('<h1>Internal Server Error</h1>')
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
})`,"js"))}
|
|
42
|
+
|
|
43
|
+
${e("multi-brand","Multi-brand sites")}
|
|
44
|
+
<p>One Pulse server can serve multiple brands, using the request domain as the key. Pass <code>resolveBrand</code> to <code>createServer</code> \u2014 it receives the <code>host</code> header and returns a brand config object of any shape you choose. The result is cached per host for 60 seconds and attached to <code>ctx.brand</code>.</p>
|
|
45
|
+
${o(t(`// server.js
|
|
46
|
+
createServer(specs, {
|
|
47
|
+
resolveBrand: async (host) => {
|
|
48
|
+
const slug = host.split('.')[0] // 'acme' from 'acme.myco.com'
|
|
49
|
+
return db.brands.findBySlug(slug) // { slug, name, accent, logo, ... }
|
|
50
|
+
}
|
|
51
|
+
})`,"js"))}
|
|
52
|
+
<p><code>ctx.brand</code> is available in <code>guard</code>, <code>server</code> fetchers, and any <code>meta</code> field. Meta fields can be functions that receive <code>ctx</code> \u2014 Pulse calls them per request:</p>
|
|
53
|
+
${o(t(`export default {
|
|
54
|
+
route: '/',
|
|
55
|
+
|
|
56
|
+
meta: {
|
|
57
|
+
title: (ctx) => \`\${ctx.brand.name} \u2014 Home\`,
|
|
58
|
+
description: (ctx) => ctx.brand.tagline,
|
|
59
|
+
styles: (ctx) => ['/pulse-ui.css', \`/themes/\${ctx.brand.slug}.css\`],
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Expose brand config to the view via server state
|
|
63
|
+
server: {
|
|
64
|
+
brand: (ctx) => ctx.brand,
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
view: (state, { brand }) => \`
|
|
68
|
+
<header>
|
|
69
|
+
<img src="\${brand.logo}" alt="\${brand.name}">
|
|
70
|
+
<nav>...</nav>
|
|
71
|
+
</header>
|
|
72
|
+
<main>...</main>
|
|
73
|
+
\`,
|
|
74
|
+
|
|
75
|
+
guard: async (ctx) => {
|
|
76
|
+
if (!ctx.brand) return { redirect: '/not-found' }
|
|
77
|
+
},
|
|
78
|
+
}`,"js"))}
|
|
79
|
+
${s("tip","Keep brand theme differences in CSS custom properties. One <code>/pulse-ui.css</code> handles layout and components \u2014 each <code>/themes/brand.css</code> only overrides <code>:root</code> variables like <code>--color-accent</code> and <code>--font-heading</code>. Theme files are typically under 1 kB.")}
|
|
80
|
+
|
|
81
|
+
${e("startup-validation","Startup validation")}
|
|
82
|
+
<p>All specs are validated against the Pulse schema at startup. An invalid spec throws before the server accepts any connections \u2014 misconfigured specs are caught immediately, not when a user first hits the route. There is no silent failure path.</p>
|
|
83
|
+
|
|
84
|
+
${e("static-files","Static file serving")}
|
|
85
|
+
<p>When <code>staticDir</code> is set, Pulse serves all files in that directory at their relative path. For example, a file at <code>public/app.css</code> is served at <code>/app.css</code>.</p>
|
|
86
|
+
<p>If <code>staticDir/dist/manifest.json</code> exists, Pulse automatically loads it to resolve production hydration bundle paths. No additional configuration is needed.</p>
|
|
87
|
+
${o(t(`createServer(specs, {
|
|
88
|
+
staticDir: 'public', // serves public/* at /*
|
|
89
|
+
// manifest auto-detected from public/dist/manifest.json
|
|
90
|
+
})`,"js"))}
|
|
91
|
+
|
|
92
|
+
${e("response-behaviour","Response behaviour")}
|
|
93
|
+
${r(["Request type","Response"],[["Full page request (GET/HEAD)","SSR HTML with doctype, head, body, and optional hydration script"],["<code>X-Pulse-Navigate: true</code> header","JSON: <code>{ html, title, hydrate, serverState }</code> for client-side navigation"],["POST/PUT/DELETE to a raw response spec","Handled by <code>spec.render</code> \u2014 used for webhooks and API endpoints"],["POST/PUT/DELETE to a page spec","405 Method Not Allowed"],["Static file","File contents with appropriate Content-Type"],["No matching route","404 response"]])}
|
|
94
|
+
|
|
95
|
+
${e("body-parsing","Reading request bodies")}
|
|
96
|
+
<p>Body parsing is available in <code>guard</code>, <code>server.*</code> fetchers, and <code>render</code> (raw specs). All methods are lazy \u2014 the stream is consumed once and the result is memoised per request.</p>
|
|
97
|
+
${r(["Method","Returns","Description"],[["<code>await ctx.json()</code>","<code>object | null</code>","Parse a JSON request body. Returns <code>null</code> for an empty body."],["<code>await ctx.text()</code>","<code>string</code>","Read the body as a plain string."],["<code>await ctx.formData()</code>","<code>object | null</code>","Parse a URL-encoded body into a plain object. Returns <code>null</code> for an empty body."],["<code>await ctx.buffer()</code>","<code>Buffer</code>","Read the raw body as a Node.js Buffer."]])}
|
|
98
|
+
<p>Bodies larger than <code>maxBody</code> (default 1 MB) are rejected with a <strong>413</strong> before the handler runs. Set <code>maxBody</code> in <code>createServer</code> options to adjust.</p>
|
|
99
|
+
<p>Page specs only accept <strong>GET and HEAD</strong> by default \u2014 POST returns 405. To accept other methods, declare <code>spec.methods</code>:</p>
|
|
100
|
+
${o(t(`export default {
|
|
101
|
+
route: '/contact',
|
|
102
|
+
methods: ['GET', 'POST'],
|
|
103
|
+
|
|
104
|
+
guard: async (ctx) => {
|
|
105
|
+
if (ctx.method === 'POST') {
|
|
106
|
+
const data = await ctx.formData()
|
|
107
|
+
if (!data.email) return { status: 422, json: { error: 'Email required' } }
|
|
108
|
+
await db.leads.create(data)
|
|
109
|
+
return { redirect: '/contact?sent=1' }
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
state: {},
|
|
114
|
+
view: () => \`<form method="POST">...</form>\`,
|
|
115
|
+
}`,"js"))}
|
|
116
|
+
${s("note","Raw response specs (<code>contentType</code> set) accept any HTTP method without <code>spec.methods</code> \u2014 they are always method-agnostic.")}
|
|
117
|
+
|
|
118
|
+
${e("escaping","Escaping user data")}
|
|
119
|
+
<p>Import <code>escHtml</code> from <code>@invisibleloop/pulse/html</code> to safely embed untrusted values in HTML view strings:</p>
|
|
120
|
+
${o(t(`import { escHtml } from '@invisibleloop/pulse/html'
|
|
121
|
+
|
|
122
|
+
view: (state) => \`
|
|
123
|
+
<p>Hello, \${escHtml(state.username)}</p>
|
|
124
|
+
\``,"js"))}
|
|
125
|
+
${s("warning","Always use <code>escHtml</code> around values that originate from user input, URL params, or external APIs. Omitting it is an XSS vulnerability.")}
|
|
126
|
+
<p>To use a nonce on a view-authored inline script, pass <code>ctx.nonce</code> through a server fetcher:</p>
|
|
127
|
+
${o(t(`server: {
|
|
128
|
+
meta: async (ctx) => ({ nonce: ctx.nonce }),
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
view: (state, server) => \`
|
|
132
|
+
<script nonce="\${server.meta.nonce}">console.log('inline ok')<\/script>
|
|
133
|
+
\``,"js"))}
|
|
134
|
+
<p>Inline scripts without the matching nonce are blocked by the CSP.</p>
|
|
135
|
+
|
|
136
|
+
${e("security-headers","Security headers")}
|
|
137
|
+
<p>Pulse sends the following headers on <strong>every</strong> response \u2014 including 404 and 500 errors. There is no configuration required and no way to accidentally omit them:</p>
|
|
138
|
+
${o(t(`X-Content-Type-Options: nosniff
|
|
139
|
+
X-Frame-Options: DENY
|
|
140
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
141
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
142
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
143
|
+
Cross-Origin-Resource-Policy: same-origin`,"bash"))}
|
|
144
|
+
|
|
145
|
+
${e("csp","Content Security Policy")}
|
|
146
|
+
<p>HTML page responses include a <code>Content-Security-Policy</code> header. Scripts require a per-request cryptographic nonce; stylesheets are restricted to same-origin; everything else defaults to <code>'none'</code>:</p>
|
|
147
|
+
${o(t(`Content-Security-Policy:
|
|
148
|
+
default-src 'none';
|
|
149
|
+
script-src 'self' 'nonce-{random}';
|
|
150
|
+
style-src 'self';
|
|
151
|
+
style-src-attr 'unsafe-inline';
|
|
152
|
+
img-src 'self' data:;
|
|
153
|
+
font-src 'self';
|
|
154
|
+
connect-src 'self';
|
|
155
|
+
frame-ancestors 'none';
|
|
156
|
+
base-uri 'self';
|
|
157
|
+
form-action 'self'`,"bash"))}
|
|
158
|
+
<p>All inline scripts injected by the framework carry a matching <code>nonce</code> attribute. The nonce is also available as <code>ctx.nonce</code> so view functions can attach it to their own inline scripts.</p>
|
|
159
|
+
<p>To load resources from external origins \u2014 Google Fonts, a CDN, an external API \u2014 pass a <code>csp</code> object to <code>createServer</code>. Sources are merged into the framework defaults; existing directives are not replaced:</p>
|
|
160
|
+
${o(t(`createServer(specs, {
|
|
161
|
+
csp: {
|
|
162
|
+
'style-src': ['https://fonts.googleapis.com'],
|
|
163
|
+
'font-src': ['https://fonts.gstatic.com'],
|
|
164
|
+
'connect-src': ['https://api.example.com'],
|
|
165
|
+
'img-src': ['https://images.unsplash.com'],
|
|
166
|
+
},
|
|
167
|
+
})`,"js"))}
|
|
168
|
+
${s("note",`The <code>style-src-attr 'unsafe-inline'</code> directive is required for inline <code>style="..."</code> attributes used by the UI component library to set CSS custom properties (e.g. spinner size, progress fill). It is scoped to attributes only \u2014 <code><style></code> blocks are fully nonce-controlled.`)}
|
|
169
|
+
|
|
170
|
+
${e("hsts","HSTS")}
|
|
171
|
+
<p>When a request arrives with <code>x-forwarded-proto: https</code> (or over a TLS socket), Pulse adds:</p>
|
|
172
|
+
${o(t("Strict-Transport-Security: max-age=31536000; includeSubDomains; preload","bash"))}
|
|
173
|
+
<p>This is automatic \u2014 no configuration required. On plain HTTP the header is omitted. The <code>preload</code> directive means you can submit the domain to <a href="https://hstspreload.org" target="_blank" rel="noopener">hstspreload.org</a> so browsers enforce HTTPS before the first connection \u2014 this is a separate manual step, not automatic.</p>
|
|
174
|
+
|
|
175
|
+
${e("cookies","Cookie defaults")}
|
|
176
|
+
<p>Cookies set via <code>ctx.setCookie()</code> default to <code>SameSite=Lax</code>. CSRF protection is on by default \u2014 omitting a <code>sameSite</code> option does not weaken it.</p>
|
|
177
|
+
|
|
178
|
+
${e("compression","Compression")}
|
|
179
|
+
<p>Pulse compresses all compressible responses using brotli (preferred) or gzip (fallback), based on the <code>Accept-Encoding</code> header. Streaming responses use transform streams so compression and delivery happen concurrently.</p>
|
|
180
|
+
|
|
181
|
+
${e("nav-header","X-Pulse-Navigate header")}
|
|
182
|
+
<p>When a request includes <code>X-Pulse-Navigate: true</code>, Pulse returns a JSON response instead of full HTML. This is used by the client-side navigation system to swap page content without a full reload:</p>
|
|
183
|
+
${o(t(`{
|
|
184
|
+
"html": "<main>...rendered content...</main>",
|
|
185
|
+
"title": "Page Title \u2014 Site",
|
|
186
|
+
"hydrate": "/dist/page.boot-abc123.js",
|
|
187
|
+
"serverState": { "product": { "id": 1, "name": "..." } }
|
|
188
|
+
}`,"js"))}
|
|
189
|
+
|
|
190
|
+
${e("health-check","Health check endpoint")}
|
|
191
|
+
<p>Pulse exposes a built-in health check at <code>/healthz</code> (configurable). It responds before <code>onRequest</code>, static file serving, and route matching \u2014 so load balancers and orchestration systems always get a response even if a hook is faulty.</p>
|
|
192
|
+
${o(t(`GET /healthz \u2192 200 OK
|
|
193
|
+
{ "status": "ok", "uptime": 42.3 }`,"json"))}
|
|
194
|
+
<p>Configure the path or disable it entirely:</p>
|
|
195
|
+
${o(t(`createServer(specs, {
|
|
196
|
+
healthCheck: '/ping', // custom path
|
|
197
|
+
// healthCheck: false, // disable
|
|
198
|
+
})`,"js"))}
|
|
199
|
+
${s("note","<code>HEAD /healthz</code> is also supported \u2014 returns the same status headers with no body. The endpoint sets <code>Cache-Control: no-store</code> so proxies never serve a stale health status.")}
|
|
200
|
+
|
|
201
|
+
${e("graceful-shutdown","Graceful shutdown")}
|
|
202
|
+
<p>Pulse registers <code>SIGTERM</code> and <code>SIGINT</code> handlers automatically. When either signal arrives:</p>
|
|
203
|
+
<ol>
|
|
204
|
+
<li><code>server.close()</code> stops accepting new connections.</li>
|
|
205
|
+
<li>Idle keep-alive sockets are destroyed immediately.</li>
|
|
206
|
+
<li>In-flight requests are allowed to finish naturally.</li>
|
|
207
|
+
<li>After <code>shutdownTimeout</code> ms (default 30 000 ms), the process force-exits to prevent a stuck request from blocking a deploy indefinitely.</li>
|
|
208
|
+
</ol>
|
|
209
|
+
<p>The <code>shutdown()</code> function is also returned from <code>createServer</code> so you can trigger it programmatically:</p>
|
|
210
|
+
${o(t(`const { server, shutdown } = createServer(specs, {
|
|
211
|
+
port: 3000,
|
|
212
|
+
shutdownTimeout: 10000, // 10 s \u2014 override the 30 s default
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// SIGTERM is already wired automatically.
|
|
216
|
+
// Call manually when needed \u2014 idempotent, safe to call multiple times.
|
|
217
|
+
shutdown()`,"js"))}
|
|
218
|
+
${s("note","Idle keep-alive sockets are destroyed immediately on shutdown. In-flight streaming responses finish sending before the socket is closed \u2014 no partial responses are delivered to clients.")}
|
|
219
|
+
`})};var c=document.getElementById("pulse-root");c&&!c.dataset.pulseMounted&&(c.dataset.pulseMounted="1",a(d,c,window.__PULSE_SERVER__||{},{ssr:!0}),u(c,a));var T=d;export{T as default};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import{a as r}from"./runtime-QFURDKA2.js";import{a as c,b as d,c as i,d as l,e,g as t,h as p,i as a}from"./runtime-L2HNXIHW.js";import{a as o,b as u}from"./runtime-B73WLANC.js";var{prev:h,next:v}=c("/server-data"),n={route:"/server-data",meta:{title:"Server Data \u2014 Pulse Docs",description:"Fetch, transform, and combine data on the server before rendering \u2014 external APIs, multiple fetchers, parallel requests.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/server-data",prev:h,next:v,content:`
|
|
2
|
+
${i("Server Data")}
|
|
3
|
+
${l("The <code>server</code> field fetches data before the page renders. It runs exclusively on the server \u2014 credentials, database access, and API secrets stay there. The browser never receives the fetcher code, only its serialised output.")}
|
|
4
|
+
|
|
5
|
+
${e("basic","Basic usage")}
|
|
6
|
+
<p>Declare a <code>data</code> async function inside the <code>server</code> object. It receives a <code>ctx</code> object with request context and returns a plain object:</p>
|
|
7
|
+
${t(r(`export default {
|
|
8
|
+
route: '/products/:id',
|
|
9
|
+
state: { quantity: 1 },
|
|
10
|
+
server: {
|
|
11
|
+
data: async (ctx) => {
|
|
12
|
+
const product = await db.products.findById(ctx.params.id)
|
|
13
|
+
const related = await db.products.findRelated(product.category)
|
|
14
|
+
return { product, related }
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
view: (state, server) => \`
|
|
18
|
+
<main>
|
|
19
|
+
<h1>\${server.product.name}</h1>
|
|
20
|
+
<p>\${server.product.description}</p>
|
|
21
|
+
<p>Price: \xA3\${server.product.price}</p>
|
|
22
|
+
<div class="quantity">
|
|
23
|
+
<button data-event="decrement">-</button>
|
|
24
|
+
<span>\${state.quantity}</span>
|
|
25
|
+
<button data-event="increment">+</button>
|
|
26
|
+
</div>
|
|
27
|
+
</main>
|
|
28
|
+
\`,
|
|
29
|
+
}`,"js"))}
|
|
30
|
+
|
|
31
|
+
${e("ctx","The ctx object")}
|
|
32
|
+
<p>The <code>ctx</code> argument passed to <code>server.data()</code> contains the full request context:</p>
|
|
33
|
+
${p(["Property","Type","Description"],[["<code>ctx.params</code>","<code>object</code>","URL path parameters from dynamic route segments (e.g. <code>:id</code>)."],["<code>ctx.query</code>","<code>object</code>","Parsed query string parameters (e.g. <code>?page=2&sort=asc</code>)."],["<code>ctx.headers</code>","<code>object</code>","Incoming request headers (lowercase keys)."],["<code>ctx.cookies</code>","<code>object</code>","Parsed cookies from the <code>Cookie</code> header."]])}
|
|
34
|
+
${t(r(`server: {
|
|
35
|
+
data: async (ctx) => {
|
|
36
|
+
// Dynamic route: /blog/:year/:slug
|
|
37
|
+
const { year, slug } = ctx.params
|
|
38
|
+
|
|
39
|
+
// Query string: ?page=2
|
|
40
|
+
const page = parseInt(ctx.query.page ?? '1', 10)
|
|
41
|
+
|
|
42
|
+
// Authentication via cookie
|
|
43
|
+
const session = ctx.cookies.sessionId
|
|
44
|
+
? await sessions.find(ctx.cookies.sessionId)
|
|
45
|
+
: null
|
|
46
|
+
|
|
47
|
+
const post = await db.posts.findBySlug(year, slug)
|
|
48
|
+
return { post, page, session }
|
|
49
|
+
},
|
|
50
|
+
}`,"js"))}
|
|
51
|
+
|
|
52
|
+
${e("view-arg","Server state in the view")}
|
|
53
|
+
<p>The resolved values from all server fetchers are merged into a single object and passed to the <code>view</code> function as its second argument, conventionally named <code>server</code>. Each fetcher key becomes a property:</p>
|
|
54
|
+
${t(r(`// server: { post: async (ctx) => ... }
|
|
55
|
+
|
|
56
|
+
view: (state, server) => \`
|
|
57
|
+
<article>
|
|
58
|
+
<h1>\${server.post.title}</h1>
|
|
59
|
+
<time>\${server.post.date}</time>
|
|
60
|
+
\${server.post.body}
|
|
61
|
+
</article>
|
|
62
|
+
\``,"js"))}
|
|
63
|
+
${a("note","If no <code>server</code> fetchers are declared, the second argument to <code>view</code> is an empty object <code>{}</code>.")}
|
|
64
|
+
|
|
65
|
+
${e("ssr-only","SSR only \u2014 not available on the client")}
|
|
66
|
+
<p>Server data is resolved before the HTML is generated and is never re-fetched in the browser. After hydration, the serialised output is available to the view as <code>window.__PULSE_SERVER__</code> for client-side re-renders \u2014 it is the same value the server computed, not a new request.</p>
|
|
67
|
+
${a("warning","Server state is serialised into the page HTML as <code>window.__PULSE_SERVER__</code> and is visible to anyone who views source. Filter fetcher output to only what the view needs \u2014 never include credentials, internal IDs, or user data beyond what must be rendered.")}
|
|
68
|
+
|
|
69
|
+
${e("errors","Error handling")}
|
|
70
|
+
<p>If <code>server.data()</code> throws, the server returns a 500 error response. Handle errors gracefully by catching inside the function and returning a safe fallback:</p>
|
|
71
|
+
${t(r(`server: {
|
|
72
|
+
data: async (ctx) => {
|
|
73
|
+
try {
|
|
74
|
+
const product = await db.products.findById(ctx.params.id)
|
|
75
|
+
if (!product) return { product: null, notFound: true }
|
|
76
|
+
return { product, notFound: false }
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('Failed to load product', err)
|
|
79
|
+
return { product: null, notFound: true }
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
view: (state, server) => server.notFound
|
|
84
|
+
? \`<p>Product not found.</p>\`
|
|
85
|
+
: \`<h1>\${server.product.name}</h1>\``,"js"))}
|
|
86
|
+
|
|
87
|
+
${e("multiple-fetchers","Multiple named fetchers")}
|
|
88
|
+
<p>The <code>server</code> object supports any number of named async functions. Each one receives <code>ctx</code> and its return value is available on <code>server</code> in the view under the same key. Fetchers run in parallel \u2014 the page renders once all have resolved:</p>
|
|
89
|
+
${t(r(`export default {
|
|
90
|
+
route: '/products/:id',
|
|
91
|
+
state: { quantity: 1 },
|
|
92
|
+
server: {
|
|
93
|
+
product: async (ctx) => db.products.findById(ctx.params.id),
|
|
94
|
+
reviews: async (ctx) => db.reviews.forProduct(ctx.params.id),
|
|
95
|
+
related: async (ctx) => db.products.related(ctx.params.id),
|
|
96
|
+
},
|
|
97
|
+
view: (state, server) => \`
|
|
98
|
+
<h1>\${server.product.name}</h1>
|
|
99
|
+
<p>\${server.reviews.length} reviews</p>
|
|
100
|
+
\${server.related.map(p => \`<a href="/products/\${p.id}">\${p.name}</a>\`).join('')}
|
|
101
|
+
\`,
|
|
102
|
+
}`,"js"))}
|
|
103
|
+
|
|
104
|
+
${e("external-apis","External API fetching")}
|
|
105
|
+
<p>Server fetchers run in Node.js. API keys and credentials are read from environment variables and never leave the server \u2014 only the fetcher's return value is serialised into the page:</p>
|
|
106
|
+
${t(r(`server: {
|
|
107
|
+
weather: async (ctx) => {
|
|
108
|
+
const res = await fetch(
|
|
109
|
+
\`https://api.weather.example.com/current?city=\${ctx.query.city}\`,
|
|
110
|
+
{ headers: { Authorization: \`Bearer \${process.env.WEATHER_API_KEY}\` } }
|
|
111
|
+
)
|
|
112
|
+
if (!res.ok) return null
|
|
113
|
+
return res.json()
|
|
114
|
+
},
|
|
115
|
+
}`,"js"))}
|
|
116
|
+
|
|
117
|
+
${e("transforming","Transforming API responses")}
|
|
118
|
+
<p>Fetchers are the right place to reshape external responses before they reach the view. Filter to only what the view needs \u2014 this reduces payload size and prevents internal fields from being serialised into the page HTML:</p>
|
|
119
|
+
${t(r(`server: {
|
|
120
|
+
article: async (ctx) => {
|
|
121
|
+
const res = await fetch(\`https://cms.example.com/articles/\${ctx.params.slug}\`)
|
|
122
|
+
const data = await res.json()
|
|
123
|
+
|
|
124
|
+
// Shape and filter before serialisation
|
|
125
|
+
return {
|
|
126
|
+
title: data.fields.title,
|
|
127
|
+
body: data.fields.bodyHtml,
|
|
128
|
+
publishedAt: new Date(data.sys.createdAt).toLocaleDateString('en-GB'),
|
|
129
|
+
author: data.fields.author.name,
|
|
130
|
+
// data.sys.revision, internal IDs etc. are dropped here
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
}`,"js"))}
|
|
134
|
+
|
|
135
|
+
${e("parallel","Parallel fetches within a single fetcher")}
|
|
136
|
+
<p>When multiple independent requests are needed inside a single fetcher, <code>Promise.all</code> runs them concurrently so the total wait time is the slowest request, not the sum:</p>
|
|
137
|
+
${t(r(`server: {
|
|
138
|
+
page: async (ctx) => {
|
|
139
|
+
const [hero, featured, nav] = await Promise.all([
|
|
140
|
+
fetch('https://cms.example.com/hero').then(r => r.json()),
|
|
141
|
+
fetch('https://cms.example.com/featured').then(r => r.json()),
|
|
142
|
+
fetch('https://cms.example.com/nav').then(r => r.json()),
|
|
143
|
+
])
|
|
144
|
+
return { hero, featured, nav }
|
|
145
|
+
},
|
|
146
|
+
}`,"js"))}
|
|
147
|
+
|
|
148
|
+
${e("caching-link","Caching server data")}
|
|
149
|
+
<p>Use <a href="/caching"><code>serverTtl</code></a> to cache fetcher results in-process for a number of seconds. This avoids hitting external APIs or a database on every request for data that changes infrequently.</p>
|
|
150
|
+
${t(r(`export default {
|
|
151
|
+
route: '/homepage',
|
|
152
|
+
serverTtl: 60, // cache all server fetchers for 60 seconds
|
|
153
|
+
server: {
|
|
154
|
+
featured: async () => fetch('https://api.example.com/featured').then(r => r.json()),
|
|
155
|
+
},
|
|
156
|
+
}`,"js"))}
|
|
157
|
+
`})};var s=document.getElementById("pulse-root");s&&!s.dataset.pulseMounted&&(s.dataset.pulseMounted="1",o(n,s,window.__PULSE_SERVER__||{},{ssr:!0}),u(s,o));var j=n;export{j as default};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import{a as s}from"./runtime-QFURDKA2.js";import{a as n,b as i,c as d,d as l,e,g as o,h as c,i as u}from"./runtime-L2HNXIHW.js";import{a,b as h}from"./runtime-B73WLANC.js";var{prev:p,next:m}=n("/slash-commands"),r={route:"/slash-commands",meta:{title:"Slash Commands \u2014 Pulse Docs",description:"The built-in slash commands available in the Pulse AI agent session.",styles:["/docs.css"]},state:{},view:()=>i({currentHref:"/slash-commands",prev:p,next:m,content:`
|
|
2
|
+
${d("Slash Commands")}
|
|
3
|
+
${l("Slash commands close the development loop inside the agent session. Building, auditing, and verifying performance happen without leaving the conversation \u2014 and every audit is checked against the thresholds you have declared in config.")}
|
|
4
|
+
|
|
5
|
+
${e("commands","Available commands")}
|
|
6
|
+
${c(["Command","What it does"],[["<code>/pulse-dev</code>","Starts (or restarts) the development server. The server watches for file changes and reloads automatically."],["<code>/pulse-stop</code>","Stops the running development server."],["<code>/pulse-build</code>","Runs a production build. Bundles all specs via esbuild into <code>public/dist/</code> with content-hashed filenames."],["<code>/pulse-start</code>","Starts the production server against the built output. Used to verify production behaviour before deploying."],["<code>/pulse-report</code>","Runs a Lighthouse audit against a production build and opens the performance report dashboard. Captures Performance score, web vitals, bundle sizes, and request counts."]])}
|
|
7
|
+
|
|
8
|
+
${e("usage","Using commands")}
|
|
9
|
+
<p>Commands are typed directly into the agent chat:</p>
|
|
10
|
+
${o(s(`/pulse-dev
|
|
11
|
+
/pulse-report`,"bash"))}
|
|
12
|
+
<p>The agent executes the relevant CLI steps and reports back with results, including whether any Lighthouse score or Core Web Vitals metric failed a configured threshold.</p>
|
|
13
|
+
|
|
14
|
+
${u("note","<code>/pulse-report</code> performs a full production build before auditing. This guarantees accurate scores and correct brotli-compressed bundle sizes \u2014 development builds are unminified and serve no production metrics.")}
|
|
15
|
+
|
|
16
|
+
${e("plain-language","Plain language prompts")}
|
|
17
|
+
<p>Slash commands cover the most common operations. For everything else, describe the goal \u2014 the agent handles the implementation within Pulse's spec structure:</p>
|
|
18
|
+
${o(s(`"Create a blog index page that fetches posts from an API"
|
|
19
|
+
"Add email validation to the contact form"
|
|
20
|
+
"Build a checkout flow with a Stripe payment step"
|
|
21
|
+
"Add a guard to the dashboard so unauthenticated users are redirected to /login"`,"bash"))}
|
|
22
|
+
<p>The agent produces spec files that conform to Pulse's structure \u2014 the framework enforces correctness, so there is no manual wiring to verify.</p>
|
|
23
|
+
|
|
24
|
+
${e("report-dashboard","Performance report dashboard")}
|
|
25
|
+
<p>The report dashboard is available at <code>/_pulse/report</code> when the dev server is running. It shows a history of Lighthouse audits across all pages \u2014 Performance score, Core Web Vitals, bundle sizes, and request counts. Threshold failures are highlighted. Each audit is saved to <code>.pulse/reports/</code> as JSON.</p>
|
|
26
|
+
`})};var t=document.getElementById("pulse-root");t&&!t.dataset.pulseMounted&&(t.dataset.pulseMounted="1",a(r,t,window.__PULSE_SERVER__||{},{ssr:!0}),h(t,a));var P=r;export{P as default};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import{a as o}from"./runtime-QFURDKA2.js";import{a as n,b as d,c as i,d as l,e,g as t,h as p,i as a}from"./runtime-L2HNXIHW.js";import{a as s,b as h}from"./runtime-B73WLANC.js";var{prev:u,next:m}=n("/spec"),c={route:"/spec",meta:{title:"Spec Reference \u2014 Pulse Docs",description:"Complete reference for every field in a Pulse page spec.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/spec",prev:u,next:m,content:`
|
|
2
|
+
${i("Spec Reference")}
|
|
3
|
+
${l("The spec is a plain JavaScript object that defines a complete contract for a page. Pulse validates every spec at startup and rejects invalid ones before the server accepts connections. At runtime, it enforces state bounds, validation rules, and lifecycle order automatically.")}
|
|
4
|
+
|
|
5
|
+
${e("quick-ref","Quick reference")}
|
|
6
|
+
${p(["Field","Type","Required","Description"],[["<code>route</code>","<code>string</code>","Yes","URL pattern for this page. Supports <code>:param</code> segments."],["<code>state</code>","<code>object</code>","Yes","Initial client-side state. Deep-cloned on mount."],["<code>view</code>","<code>function</code>","Yes","Returns an HTML string. Receives <code>(state, serverState)</code>."],["<code>meta</code>","<code>object</code>","No","Page metadata: title, description, styles, OG tags, schema."],["<code>hydrate</code>","<code>string</code>","No","Browser-importable path to this spec file. Enables client hydration."],["<code>mutations</code>","<code>object</code>","No","Synchronous state updaters keyed by name."],["<code>actions</code>","<code>object</code>","No","Async operations with full lifecycle hooks."],["<code>validation</code>","<code>object</code>","No","Declarative validation rules keyed by dot-path state keys."],["<code>constraints</code>","<code>object</code>","No","Min/max bounds enforced after every mutation."],["<code>persist</code>","<code>string[]</code>","No","State keys to save in <code>localStorage</code>."],["<code>server</code>","<code>object</code>","No","Server-side data fetcher. Result passed to <code>view</code> as second arg."],["<code>store</code>","<code>string[]</code>","No",'Global store keys this page subscribes to. See <a href="/store">Global Store</a>.'],["<code>methods</code>","<code>string[]</code>","No","HTTP methods this page accepts. Default <code>['GET', 'HEAD']</code>. Add <code>'POST'</code> etc. to opt in."],["<code>stream</code>","<code>object</code>","No","Streaming SSR config: <code>shell</code> + <code>deferred</code> segment names."],["<code>cache</code>","<code>object</code>","No","HTTP cache control headers for the page response."],["<code>serverTtl</code>","<code>number</code>","No","Seconds to cache server data in-process."],["<code>serverTimeout</code>","<code>number</code>","No","Timeout in ms for all server fetchers on this page. Overrides the global <code>fetcherTimeout</code> option."],["<code>contentType</code>","<code>string</code>","No","Override response Content-Type. Enables raw (non-HTML) responses."],["<code>onViewError</code>","<code>function</code>","No","Fallback renderer called when <code>view()</code> throws. Return an HTML string."]])}
|
|
7
|
+
|
|
8
|
+
${e("route","route")}
|
|
9
|
+
<p>The URL pattern this spec handles. Supports static segments and dynamic <code>:param</code> segments.</p>
|
|
10
|
+
${t(o(`route: '/products/:id' // matches /products/42
|
|
11
|
+
route: '/blog/:year/:slug'`,"js"))}
|
|
12
|
+
<p>Dynamic segments are available in server data and actions via <code>ctx.params</code>. See <a href="/routing">Routing</a> for more.</p>
|
|
13
|
+
|
|
14
|
+
${e("state","state")}
|
|
15
|
+
<p>The initial client-side state for the page. Always a plain object. Pulse deep-clones it on every mount \u2014 mutations never affect the original spec, and state cannot leak between page loads.</p>
|
|
16
|
+
${t(o(`state: {
|
|
17
|
+
count: 0,
|
|
18
|
+
user: { name: '', email: '' },
|
|
19
|
+
items: [],
|
|
20
|
+
}`,"js"))}
|
|
21
|
+
<p>The state object is passed as the first argument to <code>view</code>, and as the first argument to every mutation and action hook. See <a href="/state">State</a>.</p>
|
|
22
|
+
|
|
23
|
+
${e("view","view")}
|
|
24
|
+
<p>A pure function that receives <code>(state, serverState)</code> and returns an HTML string. Side effects are not permitted \u2014 the same inputs must always produce the same output. Pulse uses this guarantee to diff and re-render efficiently after mutations.</p>
|
|
25
|
+
${t(o("view: (state, server) => `\n <main>\n <h1>Hello, ${state.name}</h1>\n ${server.items.map(item => `<p>${item.title}</p>`).join('')}\n </main>\n`","js"))}
|
|
26
|
+
<p>For streaming SSR, <code>view</code> can be an object of named segment functions. See <a href="/streaming">Streaming SSR</a>.</p>
|
|
27
|
+
|
|
28
|
+
${e("meta","meta")}
|
|
29
|
+
<p>Page-level metadata. All fields are optional.</p>
|
|
30
|
+
${t(o(`meta: {
|
|
31
|
+
title: 'Page Title \u2014 Site Name',
|
|
32
|
+
description: 'Meta description for search engines.',
|
|
33
|
+
styles: ['/app.css', '/page.css'],
|
|
34
|
+
ogTitle: 'Open Graph title',
|
|
35
|
+
ogImage: 'https://example.com/og.jpg',
|
|
36
|
+
schema: { '@type': 'WebPage', name: 'Page Title' }, // ld+json
|
|
37
|
+
}`,"js"))}
|
|
38
|
+
<p>See <a href="/meta">Metadata & SEO</a> for the full reference.</p>
|
|
39
|
+
|
|
40
|
+
${e("hydrate","hydrate")}
|
|
41
|
+
<p>A browser-importable path to this spec file. Setting this enables client-side hydration \u2014 Pulse emits a bootstrap script that imports the spec bundle and calls <code>mount()</code>. In production, the path is resolved automatically via <code>manifest.json</code>.</p>
|
|
42
|
+
${t(o(`hydrate: '/src/pages/counter.js' // dev: source file path
|
|
43
|
+
// Production: resolved automatically via manifest.json`,"js"))}
|
|
44
|
+
${a("note","Omit <code>hydrate</code> for purely server-rendered pages with no client interactivity. Pulse sends zero JavaScript to the browser \u2014 no runtime overhead, no hydration cost.")}
|
|
45
|
+
|
|
46
|
+
${e("mutations","mutations")}
|
|
47
|
+
<p>Synchronous state updaters. Each mutation is a function <code>(state, event) => partialState</code>. The returned partial object is merged into state. See <a href="/mutations">Mutations</a>.</p>
|
|
48
|
+
${t(o(`mutations: {
|
|
49
|
+
increment: (state) => ({ count: state.count + 1 }),
|
|
50
|
+
setName: (state, event) => ({ name: event.target.value }),
|
|
51
|
+
}`,"js"))}
|
|
52
|
+
<p>Mutations can return <code>_toast</code> to show a notification \u2014 it is stripped from state automatically. See <a href="/actions#toast">Toast notifications</a>.</p>
|
|
53
|
+
|
|
54
|
+
${e("actions","actions")}
|
|
55
|
+
<p>Async operations with a full lifecycle. Each action has hooks for <code>onStart</code>, optional <code>validate</code>, <code>run</code>, <code>onSuccess</code>, and <code>onError</code>. See <a href="/actions">Actions</a>.</p>
|
|
56
|
+
${t(o(`actions: {
|
|
57
|
+
submit: {
|
|
58
|
+
onStart: (state, formData) => ({ status: 'loading' }),
|
|
59
|
+
validate: true,
|
|
60
|
+
run: async (state, serverState, formData) => {
|
|
61
|
+
const res = await fetch('/api/submit', { method: 'POST', body: formData })
|
|
62
|
+
return res.json()
|
|
63
|
+
},
|
|
64
|
+
onSuccess: (state, payload) => ({ status: 'success', data: payload }),
|
|
65
|
+
onError: (state, err) => ({
|
|
66
|
+
status: 'error',
|
|
67
|
+
errors: err?.validation ?? [{ message: err.message }],
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
}`,"js"))}
|
|
71
|
+
|
|
72
|
+
${e("validation","validation")}
|
|
73
|
+
<p>Declarative rules checked when an action has <code>validate: true</code>. Keys are dot-path strings into state. See <a href="/validation">Validation</a>.</p>
|
|
74
|
+
${t(o(`validation: {
|
|
75
|
+
'fields.email': { required: true, format: 'email' },
|
|
76
|
+
'fields.name': { required: true, minLength: 2, maxLength: 100 },
|
|
77
|
+
'fields.age': { required: true, min: 18, max: 120 },
|
|
78
|
+
}`,"js"))}
|
|
79
|
+
|
|
80
|
+
${e("constraints","constraints")}
|
|
81
|
+
<p>Min/max bounds enforced automatically after every mutation. Constraints cannot be bypassed \u2014 the state is clamped before the view re-renders, regardless of what the mutation returns. See <a href="/constraints">Constraints</a>.</p>
|
|
82
|
+
${t(o(`constraints: {
|
|
83
|
+
count: { min: 0, max: 100 },
|
|
84
|
+
quantity: { min: 1, max: 99 },
|
|
85
|
+
}`,"js"))}
|
|
86
|
+
|
|
87
|
+
${e("persist","persist")}
|
|
88
|
+
<p>An array of dot-path state keys to persist in <code>localStorage</code>. Values are restored on the next visit. See <a href="/persist">Persist</a>.</p>
|
|
89
|
+
${t(o("persist: ['theme', 'user.preferences']","js"))}
|
|
90
|
+
|
|
91
|
+
${e("server","server")}
|
|
92
|
+
<p>Server-only data fetching. The result is passed to <code>view</code> as the second argument. Not available on the client. See <a href="/server-data">Server Data</a>.</p>
|
|
93
|
+
${t(o(`server: {
|
|
94
|
+
data: async (ctx) => {
|
|
95
|
+
const product = await db.products.find(ctx.params.id)
|
|
96
|
+
return { product }
|
|
97
|
+
}
|
|
98
|
+
}`,"js"))}
|
|
99
|
+
|
|
100
|
+
${e("stream","stream")}
|
|
101
|
+
<p>Enables streaming SSR. Declare which named view segments are in the <code>shell</code> (rendered immediately) and which are <code>deferred</code> (streamed when ready). See <a href="/streaming">Streaming SSR</a>.</p>
|
|
102
|
+
${t(o(`stream: {
|
|
103
|
+
shell: ['header', 'nav'],
|
|
104
|
+
deferred: ['feed', 'sidebar'],
|
|
105
|
+
}`,"js"))}
|
|
106
|
+
|
|
107
|
+
${e("cache","cache")}
|
|
108
|
+
<p>HTTP Cache-Control header configuration for the page response. See <a href="/caching">Caching</a>.</p>
|
|
109
|
+
${t(o(`cache: {
|
|
110
|
+
public: true,
|
|
111
|
+
maxAge: 300, // seconds
|
|
112
|
+
staleWhileRevalidate: 86400,
|
|
113
|
+
}`,"js"))}
|
|
114
|
+
|
|
115
|
+
${e("serverTtl","serverTtl")}
|
|
116
|
+
<p>Number of seconds to cache the result of <code>server.data()</code> in-process. Subsequent requests within the TTL skip the async data fetch and re-render the HTML with the cached data. See <a href="/caching">Caching</a>.</p>
|
|
117
|
+
${t(o("serverTtl: 60 // cache server data for 60 seconds","js"))}
|
|
118
|
+
${a("tip","<strong>serverTtl vs cache</strong> \u2014 <code>serverTtl</code> caches only the server data fetcher results. The HTML is re-rendered on every request (good for personalised pages where only the fetched data is stable). <code>cache</code> caches the complete rendered HTML and sets <code>Cache-Control</code> headers (good for fully public pages that are identical for all users).")}
|
|
119
|
+
|
|
120
|
+
${e("serverTimeout","serverTimeout")}
|
|
121
|
+
<p>Timeout in milliseconds for all <code>server.*</code> fetchers on this page. If any fetcher does not resolve within this limit, it rejects with a timeout error and the request returns a 500. Use this to prevent a slow DB query or external API from hanging the response indefinitely.</p>
|
|
122
|
+
${t(o("serverTimeout: 5000 // fail after 5 s \u2014 overrides createServer fetcherTimeout","js"))}
|
|
123
|
+
<p>A global default applies to all pages via the <code>fetcherTimeout</code> option in <code>createServer</code>. <code>spec.serverTimeout</code> overrides it per page.</p>
|
|
124
|
+
|
|
125
|
+
${e("on-view-error","onViewError")}
|
|
126
|
+
<p>An optional function called when <code>view()</code> throws at runtime. Return an HTML string to display in place of the crashed view. Without this, the Pulse runtime renders a default inline error message and logs the error to the console.</p>
|
|
127
|
+
${t(o(`onViewError: (err, state, serverState) => \`
|
|
128
|
+
<div class="u-p-4 u-text-center">
|
|
129
|
+
<p>Something went wrong. <a href="">Reload</a></p>
|
|
130
|
+
</div>
|
|
131
|
+
\``,"js"))}
|
|
132
|
+
${a("note","On the server, a throwing view propagates to the server's error handler (500 response) unless <code>onViewError</code> is defined \u2014 in which case the page renders with the fallback HTML and a 200 status. On the client, the runtime always catches view errors and shows a fallback, whether or not <code>onViewError</code> is defined.")}
|
|
133
|
+
|
|
134
|
+
${e("methods","methods")}
|
|
135
|
+
<p>HTTP methods this page accepts. Defaults to <code>['GET', 'HEAD']</code> \u2014 all other methods return 405. Add <code>'POST'</code> to handle form submissions or webhooks directly on a page route without a separate API endpoint.</p>
|
|
136
|
+
${t(o("methods: ['GET', 'POST']","js"))}
|
|
137
|
+
<p>Read the method in <code>guard</code> to branch on POST vs GET:</p>
|
|
138
|
+
${t(o(`export default {
|
|
139
|
+
route: '/contact',
|
|
140
|
+
methods: ['GET', 'POST'],
|
|
141
|
+
|
|
142
|
+
guard: async (ctx) => {
|
|
143
|
+
if (ctx.method === 'POST') {
|
|
144
|
+
const data = await ctx.formData()
|
|
145
|
+
if (!data?.email) return { status: 422, json: { error: 'Email required' } }
|
|
146
|
+
await db.leads.create(data)
|
|
147
|
+
return { redirect: '/contact?sent=1' }
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
state: {},
|
|
152
|
+
view: (state) => \`<form method="POST">...</form>\`,
|
|
153
|
+
}`,"js"))}
|
|
154
|
+
${a("note","For raw response specs (<code>contentType</code> set), all HTTP methods are accepted by default \u2014 <code>methods</code> has no effect. Use <code>ctx.method</code> inside <code>render</code> to branch.")}
|
|
155
|
+
|
|
156
|
+
${e("contentType","contentType")}
|
|
157
|
+
<p>Override the response <code>Content-Type</code>. When set, the view function receives <code>(ctx, serverState)</code> and returns the raw response body \u2014 the normal HTML wrapper is bypassed. See <a href="/raw-responses">Raw Responses</a>.</p>
|
|
158
|
+
${t(o("contentType: 'application/rss+xml'","js"))}
|
|
159
|
+
`})};var r=document.getElementById("pulse-root");r&&!r.dataset.pulseMounted&&(r.dataset.pulseMounted="1",s(c,r,window.__PULSE_SERVER__||{},{ssr:!0}),h(r,s));var b=c;export{b as default};
|