@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,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Image
|
|
3
|
+
*
|
|
4
|
+
* Responsive image with optional aspect-ratio crop, caption, and rounded corners.
|
|
5
|
+
* Always uses loading="lazy" and decoding="async".
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {string} opts.src - Image source URL
|
|
9
|
+
* @param {string} opts.alt - Alt text (required for accessibility)
|
|
10
|
+
* @param {string} opts.caption - Optional figcaption text
|
|
11
|
+
* @param {string} opts.ratio - CSS aspect-ratio string e.g. '16/9', '4/3', '1/1'
|
|
12
|
+
* @param {boolean} opts.rounded - Larger corner radius (1rem) — for photos, cards, book covers
|
|
13
|
+
* @param {boolean} opts.pill - Full pill/stadium radius (999px) — for avatars or circular crops
|
|
14
|
+
* @param {number|string} opts.width - img width attribute (browser hint only)
|
|
15
|
+
* @param {number|string} opts.height - img height attribute
|
|
16
|
+
* @param {number|string} opts.maxWidth - CSS max-width on the figure (px value or CSS string). Use this to constrain portrait/narrow images inside wide columns.
|
|
17
|
+
* @param {string} opts.class
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { escHtml as e } from '../html.js'
|
|
21
|
+
|
|
22
|
+
export function uiImage({
|
|
23
|
+
src = '',
|
|
24
|
+
alt = '',
|
|
25
|
+
caption = '',
|
|
26
|
+
ratio = '',
|
|
27
|
+
rounded = false,
|
|
28
|
+
pill = false,
|
|
29
|
+
width = '',
|
|
30
|
+
height = '',
|
|
31
|
+
maxWidth = '',
|
|
32
|
+
class: cls = '',
|
|
33
|
+
} = {}) {
|
|
34
|
+
// The outer figure uses display:contents so the inner crop div
|
|
35
|
+
// becomes a direct flex/block child of whatever container holds it.
|
|
36
|
+
// This is required for aspect-ratio to resolve correctly in flex contexts.
|
|
37
|
+
const figClasses = ['ui-image', rounded ? 'ui-image--rounded' : '', pill ? 'ui-image--pill' : '', cls].filter(Boolean).join(' ')
|
|
38
|
+
|
|
39
|
+
const maxWidthStyle = maxWidth
|
|
40
|
+
? ` style="max-width:${e(typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth)};margin-left:auto;margin-right:auto"`
|
|
41
|
+
: ''
|
|
42
|
+
|
|
43
|
+
const widthAttr = width ? ` width="${e(String(width))}"` : ''
|
|
44
|
+
const heightAttr = height ? ` height="${e(String(height))}"` : ''
|
|
45
|
+
|
|
46
|
+
const captionHtml = caption
|
|
47
|
+
? `<figcaption class="ui-image-caption">${e(caption)}</figcaption>`
|
|
48
|
+
: ''
|
|
49
|
+
|
|
50
|
+
if (ratio) {
|
|
51
|
+
return `<figure class="${e(figClasses)}"${maxWidthStyle}>
|
|
52
|
+
<div class="ui-image-crop">
|
|
53
|
+
<img src="${e(src)}" alt="${e(alt)}" class="ui-image-img--cover" style="aspect-ratio:${e(ratio)}"${widthAttr}${heightAttr} loading="lazy" decoding="async">
|
|
54
|
+
</div>
|
|
55
|
+
${captionHtml}
|
|
56
|
+
</figure>`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return `<figure class="${e(figClasses)}"${maxWidthStyle}>
|
|
60
|
+
<div class="ui-image-wrap">
|
|
61
|
+
<img src="${e(src)}" alt="${e(alt)}" class="ui-image-img"${widthAttr}${heightAttr} loading="lazy" decoding="async">
|
|
62
|
+
</div>
|
|
63
|
+
${captionHtml}
|
|
64
|
+
</figure>`
|
|
65
|
+
}
|
package/tsconfig.json
ADDED
package/types/html.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — HTML escaping types
|
|
3
|
+
* @invisibleloop/pulse/html
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Escape HTML special characters for safe embedding in HTML attributes or text.
|
|
8
|
+
*
|
|
9
|
+
* Always use this around values from user input, URL params, or external APIs.
|
|
10
|
+
* Omitting it is an XSS vulnerability.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import { escHtml } from '@invisibleloop/pulse/html'
|
|
14
|
+
*
|
|
15
|
+
* view: (state) => `<p>Hello, ${escHtml(state.username)}</p>`
|
|
16
|
+
*/
|
|
17
|
+
export function escHtml(str: unknown): string
|
package/types/image.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Image helper types
|
|
3
|
+
* @invisibleloop/pulse/image
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ImgOptions {
|
|
7
|
+
/** Image URL */
|
|
8
|
+
src: string
|
|
9
|
+
/** Alt text (required for accessibility) */
|
|
10
|
+
alt: string
|
|
11
|
+
/** Intrinsic width in px — include to prevent CLS */
|
|
12
|
+
width?: number
|
|
13
|
+
/** Intrinsic height in px — include to prevent CLS */
|
|
14
|
+
height?: number
|
|
15
|
+
/**
|
|
16
|
+
* Set true for the LCP image (above-the-fold hero).
|
|
17
|
+
* Adds loading="eager" and fetchpriority="high".
|
|
18
|
+
*/
|
|
19
|
+
priority?: boolean
|
|
20
|
+
/** CSS class applied to <img> */
|
|
21
|
+
class?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PictureSource {
|
|
25
|
+
/** URL for this format variant */
|
|
26
|
+
src: string
|
|
27
|
+
/** MIME type, e.g. 'image/avif', 'image/webp' */
|
|
28
|
+
type: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PictureOptions extends ImgOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Modern format sources in preference order (AVIF first, then WebP).
|
|
34
|
+
* The src on ImgOptions is the universal fallback (JPEG/PNG).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* sources: [
|
|
38
|
+
* { src: '/hero.avif', type: 'image/avif' },
|
|
39
|
+
* { src: '/hero.webp', type: 'image/webp' },
|
|
40
|
+
* ]
|
|
41
|
+
*/
|
|
42
|
+
sources?: PictureSource[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generate an optimised <img> tag.
|
|
47
|
+
* Always include width + height to prevent CLS.
|
|
48
|
+
* Use priority: true for the LCP image.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* import { img } from '@invisibleloop/pulse/image'
|
|
52
|
+
* img({ src: '/hero.jpg', alt: 'Hero', width: 1200, height: 600, priority: true })
|
|
53
|
+
*/
|
|
54
|
+
export function img(options: ImgOptions): string
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generate a <picture> element with modern format sources and a fallback <img>.
|
|
58
|
+
* Provide sources in preference order (AVIF first, then WebP, then the fallback src).
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* import { picture } from '@invisibleloop/pulse/image'
|
|
62
|
+
* picture({
|
|
63
|
+
* src: '/hero.jpg', alt: 'Hero', width: 1200, height: 600,
|
|
64
|
+
* sources: [
|
|
65
|
+
* { src: '/hero.avif', type: 'image/avif' },
|
|
66
|
+
* { src: '/hero.webp', type: 'image/webp' },
|
|
67
|
+
* ]
|
|
68
|
+
* })
|
|
69
|
+
*/
|
|
70
|
+
export function picture(options: PictureOptions): string
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Client-side navigation types
|
|
3
|
+
* @invisibleloop/pulse/navigate
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MountResult } from './runtime.js'
|
|
7
|
+
import type { PulseSpec } from './schema.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initialise client-side navigation for a Pulse app.
|
|
11
|
+
*
|
|
12
|
+
* Intercepts same-origin <a> clicks and browser back/forward events.
|
|
13
|
+
* Fetches new pages as JSON fragments (via X-Pulse-Navigate header),
|
|
14
|
+
* swaps #pulse-root, and re-mounts the new spec without a full page reload.
|
|
15
|
+
* Falls back to location.href on any fetch or import error.
|
|
16
|
+
*
|
|
17
|
+
* Call once after the initial mount().
|
|
18
|
+
*
|
|
19
|
+
* @param root The #pulse-root element
|
|
20
|
+
* @param mountFn The mount() function from @invisibleloop/pulse/runtime
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* import { mount } from '@invisibleloop/pulse/runtime'
|
|
24
|
+
* import { initNavigation } from '@invisibleloop/pulse/navigate'
|
|
25
|
+
*
|
|
26
|
+
* const root = document.getElementById('pulse-root')
|
|
27
|
+
* const m = mount(spec, root, window.__PULSE_SERVER__ ?? {}, { ssr: true })
|
|
28
|
+
* initNavigation(root, mount)
|
|
29
|
+
*/
|
|
30
|
+
export function initNavigation(
|
|
31
|
+
root: HTMLElement,
|
|
32
|
+
mountFn: (
|
|
33
|
+
spec: PulseSpec,
|
|
34
|
+
el: HTMLElement,
|
|
35
|
+
serverState?: Record<string, unknown>,
|
|
36
|
+
options?: { ssr?: boolean; store?: unknown }
|
|
37
|
+
) => MountResult
|
|
38
|
+
): void
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Client runtime types
|
|
3
|
+
* @invisibleloop/pulse/runtime
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PulseSpec, PulseStoreDefinition } from './server.js'
|
|
7
|
+
|
|
8
|
+
export type { PulseSpec } from './schema.js'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// mount
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export interface MountOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Set to true when mounting after SSR — skips the initial render and only
|
|
17
|
+
* binds event listeners. Preserves the SSR-painted LCP element and avoids a
|
|
18
|
+
* re-render flash. Always true in production bundle boots.
|
|
19
|
+
*/
|
|
20
|
+
ssr?: boolean
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Store definition (default export from pulse.store.js).
|
|
24
|
+
* Pass to the first mount() call on a page that uses spec.store.
|
|
25
|
+
*/
|
|
26
|
+
store?: PulseStoreDefinition
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MountResult {
|
|
30
|
+
/** Destroy this mount, remove event listeners, and unsubscribe from the store. */
|
|
31
|
+
destroy(): void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mount a Pulse spec to a DOM element.
|
|
36
|
+
*
|
|
37
|
+
* Binds events, applies mutations, enforces constraints, and re-renders the
|
|
38
|
+
* view on every state change. Call once per page load, then call destroy()
|
|
39
|
+
* before mounting a new spec during client-side navigation.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* import spec from './src/pages/counter.js'
|
|
43
|
+
* import { mount } from '@invisibleloop/pulse/runtime'
|
|
44
|
+
*
|
|
45
|
+
* const root = document.getElementById('pulse-root')
|
|
46
|
+
* mount(spec, root, window.__PULSE_SERVER__ ?? {}, { ssr: true })
|
|
47
|
+
*/
|
|
48
|
+
export function mount(
|
|
49
|
+
spec: PulseSpec,
|
|
50
|
+
el: HTMLElement,
|
|
51
|
+
serverState?: Record<string, unknown>,
|
|
52
|
+
options?: MountOptions
|
|
53
|
+
): MountResult
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Utility exports
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/** Debounce a function — returns a new function that delays invocation. */
|
|
60
|
+
export function debounce<T extends (...args: unknown[]) => unknown>(fn: T, ms: number): T
|
|
61
|
+
|
|
62
|
+
/** Throttle a function — returns a new function that limits invocation rate. */
|
|
63
|
+
export function throttle<T extends (...args: unknown[]) => unknown>(fn: T, ms: number): T
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pulse — Core types
|
|
5
|
+
* Shared across server, runtime, and SSR.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Request context
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export interface RequestContext {
|
|
13
|
+
/** Route params extracted from the URL pattern, e.g. { id: '42' } */
|
|
14
|
+
params: Record<string, string>
|
|
15
|
+
/** Parsed query string, e.g. { q: 'widget' } */
|
|
16
|
+
query: Record<string, string>
|
|
17
|
+
/** Raw request headers */
|
|
18
|
+
headers: Record<string, string | string[] | undefined>
|
|
19
|
+
/** URL pathname */
|
|
20
|
+
pathname: string
|
|
21
|
+
/** HTTP method */
|
|
22
|
+
method: string
|
|
23
|
+
/** CSP nonce generated per request — pass to inline <script nonce="…"> */
|
|
24
|
+
nonce: string
|
|
25
|
+
/** Parsed cookies from the request Cookie header */
|
|
26
|
+
cookies: Record<string, string>
|
|
27
|
+
/** Brand config resolved by resolveBrand (undefined if not configured) */
|
|
28
|
+
brand?: unknown
|
|
29
|
+
/** Global store state for keys declared in spec.store */
|
|
30
|
+
store?: Record<string, unknown>
|
|
31
|
+
/** Active fetcher timeout in ms (null = no limit) */
|
|
32
|
+
fetcherTimeout: number | null
|
|
33
|
+
|
|
34
|
+
/** Parse a JSON request body. Returns null for an empty body. */
|
|
35
|
+
json(): Promise<Record<string, unknown> | null>
|
|
36
|
+
/** Read the body as a plain string. */
|
|
37
|
+
text(): Promise<string>
|
|
38
|
+
/** Parse a URL-encoded body into a plain object. Returns null for empty. */
|
|
39
|
+
formData(): Promise<Record<string, string> | null>
|
|
40
|
+
/** Read the raw body as a Node.js Buffer. */
|
|
41
|
+
buffer(): Promise<Buffer>
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Queue a Set-Cookie header on the response.
|
|
45
|
+
* Defaults: Path=/, SameSite=Lax.
|
|
46
|
+
*/
|
|
47
|
+
setCookie(
|
|
48
|
+
name: string,
|
|
49
|
+
value: string,
|
|
50
|
+
opts?: {
|
|
51
|
+
httpOnly?: boolean
|
|
52
|
+
secure?: boolean
|
|
53
|
+
path?: string
|
|
54
|
+
maxAge?: number
|
|
55
|
+
sameSite?: 'Strict' | 'Lax' | 'None'
|
|
56
|
+
domain?: string
|
|
57
|
+
}
|
|
58
|
+
): void
|
|
59
|
+
|
|
60
|
+
/** Queue an arbitrary response header. */
|
|
61
|
+
setHeader(name: string, value: string): void
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Guard return value
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
export type GuardResult =
|
|
69
|
+
| { redirect: string }
|
|
70
|
+
| { status: number; json?: unknown; body?: string; headers?: Record<string, string> }
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Spec sub-types
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
export interface ValidationRule {
|
|
77
|
+
required?: boolean
|
|
78
|
+
minLength?: number
|
|
79
|
+
maxLength?: number
|
|
80
|
+
min?: number
|
|
81
|
+
max?: number
|
|
82
|
+
format?: 'email' | 'url' | 'numeric'
|
|
83
|
+
pattern?: RegExp
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface StreamConfig {
|
|
87
|
+
/** Segment keys rendered in the first flush */
|
|
88
|
+
shell: string[]
|
|
89
|
+
/** Segment keys rendered after their async data resolves */
|
|
90
|
+
deferred?: string[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ActionConfig<S extends object = Record<string, unknown>> {
|
|
94
|
+
/** Run validation (spec.validation) before executing. Default false. */
|
|
95
|
+
validate?: boolean
|
|
96
|
+
/** Called before run() — optimistic update */
|
|
97
|
+
onStart?: (state: S, payload?: unknown) => Partial<S>
|
|
98
|
+
/** Async operation. Return value is passed to onSuccess. */
|
|
99
|
+
run: (state: S, server?: unknown, payload?: unknown) => Promise<unknown>
|
|
100
|
+
/** Called on success. Return partial state to merge. */
|
|
101
|
+
onSuccess: (state: S, payload: unknown) => Partial<S> & { _toast?: ToastOptions; _storeUpdate?: Record<string, unknown> }
|
|
102
|
+
/** Called on error. Return partial state to merge. */
|
|
103
|
+
onError: (state: S, err: Error) => Partial<S> & { _toast?: ToastOptions }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ToastOptions {
|
|
107
|
+
message: string
|
|
108
|
+
variant?: 'success' | 'error' | 'warning' | 'info'
|
|
109
|
+
duration?: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface CacheConfig {
|
|
113
|
+
public?: boolean
|
|
114
|
+
maxAge?: number
|
|
115
|
+
staleWhileRevalidate?: number
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface MetaConfig {
|
|
119
|
+
title?: string | ((ctx: RequestContext) => string)
|
|
120
|
+
description?: string | ((ctx: RequestContext) => string)
|
|
121
|
+
styles?: string[] | ((ctx: RequestContext) => string[])
|
|
122
|
+
ogTitle?: string | ((ctx: RequestContext) => string)
|
|
123
|
+
ogImage?: string | ((ctx: RequestContext) => string)
|
|
124
|
+
schema?: Record<string, unknown>
|
|
125
|
+
theme?: 'light' | 'dark'
|
|
126
|
+
[key: string]: unknown
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// PulseSpec
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
export interface PulseSpec<S extends object = Record<string, unknown>> {
|
|
134
|
+
/** URL pattern. Supports :param segments. */
|
|
135
|
+
route: string
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Initial client-side state. Deep-cloned on mount.
|
|
139
|
+
* Required for page specs; omit only for raw content specs (contentType set).
|
|
140
|
+
*/
|
|
141
|
+
state: S
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Pure function returning an HTML string.
|
|
145
|
+
* Receives (clientState, serverState).
|
|
146
|
+
* Can also be a keyed object of segment functions for streaming SSR.
|
|
147
|
+
*/
|
|
148
|
+
view:
|
|
149
|
+
| ((state: S, server?: Record<string, unknown>) => string)
|
|
150
|
+
| Record<string, (state: S, server?: Record<string, unknown>) => string>
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Browser-importable path to this spec file.
|
|
154
|
+
* Required for any spec with mutations, actions, or persist.
|
|
155
|
+
*/
|
|
156
|
+
hydrate?: string
|
|
157
|
+
|
|
158
|
+
/** Streaming SSR config — split view into shell and deferred segments. */
|
|
159
|
+
stream?: StreamConfig
|
|
160
|
+
|
|
161
|
+
/** Server-side data fetchers. Results passed as second arg to view. */
|
|
162
|
+
server?: Record<string, (ctx: RequestContext) => Promise<unknown> | unknown>
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Raw content spec — set Content-Type and implement render() instead of view().
|
|
166
|
+
* Accepts any HTTP method. Use for RSS feeds, JSON APIs, sitemaps, webhooks.
|
|
167
|
+
*/
|
|
168
|
+
contentType?: string
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Required when contentType is set.
|
|
172
|
+
* Receives (ctx, server) and returns the raw response body string.
|
|
173
|
+
*/
|
|
174
|
+
render?: (ctx: RequestContext, server?: Record<string, unknown>) => Promise<string> | string
|
|
175
|
+
|
|
176
|
+
/** Page metadata for the <head>. Fields may be functions for per-request resolution. */
|
|
177
|
+
meta?: MetaConfig
|
|
178
|
+
|
|
179
|
+
/** Synchronous state updaters. Each returns a partial state to merge. */
|
|
180
|
+
mutations?: Record<string, (state: S, event?: Event | unknown) => Partial<S>>
|
|
181
|
+
|
|
182
|
+
/** Async operations with full lifecycle hooks. */
|
|
183
|
+
actions?: Record<string, ActionConfig<S>>
|
|
184
|
+
|
|
185
|
+
/** Declarative validation rules keyed by dot-path state keys. */
|
|
186
|
+
validation?: Record<string, ValidationRule>
|
|
187
|
+
|
|
188
|
+
/** Min/max bounds clamped after every mutation. Cannot be bypassed. */
|
|
189
|
+
constraints?: Record<string, { min?: number; max?: number }>
|
|
190
|
+
|
|
191
|
+
/** State keys to persist in localStorage between visits. */
|
|
192
|
+
persist?: string[]
|
|
193
|
+
|
|
194
|
+
/** Global store keys this page subscribes to. Appear in view's server arg. */
|
|
195
|
+
store?: string[]
|
|
196
|
+
|
|
197
|
+
/** HTTP methods this page accepts. Default ['GET', 'HEAD']. */
|
|
198
|
+
methods?: Array<'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'>
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Called before server data fetchers on every request.
|
|
202
|
+
* Return { redirect } to redirect, { status, json } for a custom response,
|
|
203
|
+
* or nothing to allow the request to proceed.
|
|
204
|
+
*/
|
|
205
|
+
guard?: (ctx: RequestContext) => Promise<GuardResult | void> | GuardResult | void
|
|
206
|
+
|
|
207
|
+
/** HTTP Cache-Control header config for the page response. */
|
|
208
|
+
cache?: CacheConfig
|
|
209
|
+
|
|
210
|
+
/** Seconds to cache server.data() result in-process. */
|
|
211
|
+
serverTtl?: number
|
|
212
|
+
|
|
213
|
+
/** Timeout in ms for all server fetchers on this page. Overrides createServer fetcherTimeout. */
|
|
214
|
+
serverTimeout?: number
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Fallback renderer called when view() throws at runtime.
|
|
218
|
+
* Return an HTML string to display instead of the default error message.
|
|
219
|
+
* On the server, if not defined, view errors propagate to the 500 handler.
|
|
220
|
+
*/
|
|
221
|
+
onViewError?: (err: Error, state: S, server?: Record<string, unknown>) => string
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Validation utilities
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
export interface ValidationResult {
|
|
229
|
+
valid: boolean
|
|
230
|
+
errors: string[]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Validate a spec. Returns { valid, errors }. Does not throw. */
|
|
234
|
+
export function validateSpec(spec: unknown): ValidationResult
|
|
235
|
+
|
|
236
|
+
/** Validate a spec. Throws with all errors if invalid. */
|
|
237
|
+
export function assertValidSpec(spec: unknown): void
|
|
238
|
+
|
|
239
|
+
/** Returns the segment keys defined in spec.view. ['default'] for a function view. */
|
|
240
|
+
export function getViewSegments(spec: PulseSpec): string[]
|
|
241
|
+
|
|
242
|
+
/** Returns { shell, deferred } stream order. All segments in shell if no stream config. */
|
|
243
|
+
export function getStreamOrder(spec: PulseSpec): { shell: string[]; deferred: string[] }
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pulse — Server API types
|
|
5
|
+
* @invisibleloop/pulse or @invisibleloop/pulse/server
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { IncomingMessage, ServerResponse, Server } from 'http'
|
|
9
|
+
import type { PulseSpec } from './schema.js'
|
|
10
|
+
|
|
11
|
+
export type { PulseSpec, RequestContext, ValidationRule, StreamConfig, ActionConfig,
|
|
12
|
+
GuardResult, CacheConfig, MetaConfig, ToastOptions, ValidationResult } from './schema.js'
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// createServer options
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export interface ServerOptions {
|
|
19
|
+
/** Port to listen on. Default 3000. */
|
|
20
|
+
port?: number
|
|
21
|
+
|
|
22
|
+
/** Enable streaming SSR globally. Default true. */
|
|
23
|
+
stream?: boolean
|
|
24
|
+
|
|
25
|
+
/** Path to a directory of static files served at their relative path. */
|
|
26
|
+
staticDir?: string
|
|
27
|
+
|
|
28
|
+
/** Explicit manifest path or object. Overrides auto-detection from staticDir/dist/manifest.json. */
|
|
29
|
+
manifest?: string | Record<string, string> | null
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Trailing slash behaviour. Default 'remove'.
|
|
33
|
+
* 'remove' — 301 /about/ → /about
|
|
34
|
+
* 'add' — 301 /about → /about/
|
|
35
|
+
* 'allow' — serve both, no redirect
|
|
36
|
+
*/
|
|
37
|
+
trailingSlash?: 'remove' | 'add' | 'allow'
|
|
38
|
+
|
|
39
|
+
/** Maximum request body size in bytes. Default 1 MB. Requests exceeding this receive a 413. */
|
|
40
|
+
maxBody?: number
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default HTML cache TTL for all pages in production.
|
|
44
|
+
* true = 1 h + 24 h SWR, number = max-age in seconds,
|
|
45
|
+
* object = { public, maxAge, staleWhileRevalidate }.
|
|
46
|
+
* spec.cache overrides per page.
|
|
47
|
+
*/
|
|
48
|
+
defaultCache?: boolean | number | { public?: boolean; maxAge?: number; staleWhileRevalidate?: number } | null
|
|
49
|
+
|
|
50
|
+
/** Global timeout in ms for all server fetchers. null = no limit. Override per page with spec.serverTimeout. */
|
|
51
|
+
fetcherTimeout?: number | null
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Multi-brand support. Called once per host (cached 60 s).
|
|
55
|
+
* Result is attached to ctx.brand and available in guard, server, and meta functions.
|
|
56
|
+
*/
|
|
57
|
+
resolveBrand?: (host: string) => Promise<unknown> | unknown
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Milliseconds to wait for in-flight requests to complete during graceful
|
|
61
|
+
* shutdown before force-exiting. Default 30 000 ms.
|
|
62
|
+
*/
|
|
63
|
+
shutdownTimeout?: number
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Path for the built-in health check endpoint. Default '/healthz'.
|
|
67
|
+
* Returns { status: 'ok', uptime: number } — bypasses onRequest so load
|
|
68
|
+
* balancers always get a response. Set to false to disable.
|
|
69
|
+
*/
|
|
70
|
+
healthCheck?: string | false
|
|
71
|
+
|
|
72
|
+
/** Called on every request before routing. Return false to short-circuit Pulse handling. */
|
|
73
|
+
onRequest?: (req: IncomingMessage, res: ServerResponse) => false | void | unknown
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extra CSP sources to merge into the framework's default Content-Security-Policy.
|
|
77
|
+
* Use this to allow external stylesheets, fonts, or API origins.
|
|
78
|
+
* Each key is a CSP directive name; each value is an array of sources to append.
|
|
79
|
+
* @example
|
|
80
|
+
* // Allow Google Fonts
|
|
81
|
+
* csp: { 'style-src': ['https://fonts.googleapis.com'], 'font-src': ['https://fonts.gstatic.com'] }
|
|
82
|
+
*/
|
|
83
|
+
csp?: Record<string, string[]>
|
|
84
|
+
|
|
85
|
+
/** Called on unhandled errors. Receives (err, req, res). */
|
|
86
|
+
onError?: (err: Error, req: IncomingMessage, res: ServerResponse) => void
|
|
87
|
+
|
|
88
|
+
/** Global store definition (default export from pulse.store.js). */
|
|
89
|
+
store?: PulseStoreDefinition | null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Store (used via server options)
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
export interface PulseStoreDefinition {
|
|
97
|
+
hydrate?: string
|
|
98
|
+
state: Record<string, unknown>
|
|
99
|
+
server?: Record<string, (ctx: import('./schema.js').RequestContext) => Promise<unknown> | unknown>
|
|
100
|
+
mutations?: Record<string, (state: Record<string, unknown>, payload?: unknown) => Partial<Record<string, unknown>>>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// createServer return value
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export interface ServerInstance {
|
|
108
|
+
/** The underlying Node.js http.Server */
|
|
109
|
+
server: Server
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Gracefully shut down the server.
|
|
113
|
+
* Stops accepting new connections, destroys idle keep-alive sockets, lets
|
|
114
|
+
* in-flight requests finish, then force-exits after shutdownTimeout ms.
|
|
115
|
+
* SIGTERM and SIGINT are already wired up automatically.
|
|
116
|
+
* Idempotent — safe to call multiple times.
|
|
117
|
+
*/
|
|
118
|
+
shutdown(): void
|
|
119
|
+
|
|
120
|
+
/** Swap the spec list at runtime (used by the dev server for hot reload). */
|
|
121
|
+
updateSpecs(specs: PulseSpec[]): void
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// createServer
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create and start a Pulse HTTP server.
|
|
130
|
+
*
|
|
131
|
+
* All specs are validated at startup — an invalid spec throws before the
|
|
132
|
+
* server accepts any connections.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* import { createServer } from '@invisibleloop/pulse'
|
|
136
|
+
* import home from './src/pages/home.js'
|
|
137
|
+
* import contact from './src/pages/contact.js'
|
|
138
|
+
*
|
|
139
|
+
* const { server, shutdown } = createServer([home, contact], {
|
|
140
|
+
* port: 3000,
|
|
141
|
+
* stream: true,
|
|
142
|
+
* staticDir: 'public',
|
|
143
|
+
* })
|
|
144
|
+
*/
|
|
145
|
+
export function createServer(specs: PulseSpec[], options?: ServerOptions): ServerInstance
|