@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.
Files changed (344) hide show
  1. package/.claude/commands/build-page.md +59 -0
  2. package/.claude/commands/new-doc-page.md +45 -0
  3. package/.claude/commands/verify.md +52 -0
  4. package/.claude/pulse-checklist.md +111 -0
  5. package/.claude/settings.local.json +102 -0
  6. package/.github/workflows/ci.yml +22 -0
  7. package/.github/workflows/publish.yml +41 -0
  8. package/.pulse/load-reports/home/1773432711417.json +22 -0
  9. package/CLAUDE.md +383 -0
  10. package/README.md +95 -0
  11. package/docs/.claude/pulse-checklist.md +111 -0
  12. package/docs/public/.pulse-ui-version +1 -0
  13. package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
  14. package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
  15. package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
  16. package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
  17. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
  18. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
  19. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
  20. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
  21. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
  22. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
  23. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
  24. package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
  25. package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
  26. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
  27. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
  28. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
  29. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
  30. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
  31. package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
  32. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
  33. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
  34. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
  35. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
  36. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
  37. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
  38. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
  39. package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
  40. package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
  41. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
  42. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
  43. package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
  44. package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
  45. package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
  46. package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
  47. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
  48. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
  49. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
  50. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
  51. package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
  52. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
  53. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
  54. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
  55. package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
  56. package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
  57. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
  58. package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
  59. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
  60. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
  61. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
  62. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
  63. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
  64. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
  65. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
  66. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
  67. package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
  68. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
  69. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
  70. package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
  71. package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
  72. package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
  73. package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
  74. package/docs/public/dist/docs-8e3d4b5c.css +1 -0
  75. package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
  76. package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
  77. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
  78. package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
  79. package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
  80. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
  81. package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
  82. package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
  83. package/docs/public/dist/manifest.json +94 -0
  84. package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
  85. package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
  86. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
  87. package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
  88. package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
  89. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
  90. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
  91. package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
  92. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
  93. package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
  94. package/docs/public/dist/runtime-B73WLANC.js +1 -0
  95. package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
  96. package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
  97. package/docs/public/dist/runtime-QFURDKA2.js +5 -0
  98. package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
  99. package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
  100. package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
  101. package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
  102. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
  103. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
  104. package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
  105. package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
  106. package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
  107. package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
  108. package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
  109. package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
  110. package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
  111. package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
  112. package/docs/public/docs.css +2020 -0
  113. package/docs/public/menu.js +83 -0
  114. package/docs/public/pulse-ui.css +2739 -0
  115. package/docs/public/pulse-ui.js +236 -0
  116. package/docs/server.js +192 -0
  117. package/docs/src/lib/component-page.js +47 -0
  118. package/docs/src/lib/highlight.js +255 -0
  119. package/docs/src/lib/layout.js +131 -0
  120. package/docs/src/lib/metrics-store.js +6 -0
  121. package/docs/src/lib/nav.js +159 -0
  122. package/docs/src/lib/stats.js +81 -0
  123. package/docs/src/pages/accessibility.js +157 -0
  124. package/docs/src/pages/actions.js +191 -0
  125. package/docs/src/pages/auth.js +177 -0
  126. package/docs/src/pages/caching.js +95 -0
  127. package/docs/src/pages/components/accordion.js +48 -0
  128. package/docs/src/pages/components/alert.js +35 -0
  129. package/docs/src/pages/components/app-badge.js +41 -0
  130. package/docs/src/pages/components/avatar.js +35 -0
  131. package/docs/src/pages/components/badge.js +36 -0
  132. package/docs/src/pages/components/banner.js +45 -0
  133. package/docs/src/pages/components/breadcrumbs.js +94 -0
  134. package/docs/src/pages/components/button.js +84 -0
  135. package/docs/src/pages/components/card.js +225 -0
  136. package/docs/src/pages/components/carousel.js +72 -0
  137. package/docs/src/pages/components/charts.js +278 -0
  138. package/docs/src/pages/components/checkbox.js +129 -0
  139. package/docs/src/pages/components/cluster.js +47 -0
  140. package/docs/src/pages/components/code-window.js +57 -0
  141. package/docs/src/pages/components/container.js +40 -0
  142. package/docs/src/pages/components/cta.js +53 -0
  143. package/docs/src/pages/components/divider.js +37 -0
  144. package/docs/src/pages/components/empty.js +36 -0
  145. package/docs/src/pages/components/feature.js +60 -0
  146. package/docs/src/pages/components/fieldset.js +65 -0
  147. package/docs/src/pages/components/fileupload.js +127 -0
  148. package/docs/src/pages/components/footer.js +58 -0
  149. package/docs/src/pages/components/grid.js +165 -0
  150. package/docs/src/pages/components/heading.js +107 -0
  151. package/docs/src/pages/components/hero.js +65 -0
  152. package/docs/src/pages/components/icons.js +285 -0
  153. package/docs/src/pages/components/image.js +71 -0
  154. package/docs/src/pages/components/input.js +51 -0
  155. package/docs/src/pages/components/list.js +112 -0
  156. package/docs/src/pages/components/media.js +51 -0
  157. package/docs/src/pages/components/modal.js +111 -0
  158. package/docs/src/pages/components/nav.js +86 -0
  159. package/docs/src/pages/components/pricing.js +68 -0
  160. package/docs/src/pages/components/progress.js +102 -0
  161. package/docs/src/pages/components/prose.js +111 -0
  162. package/docs/src/pages/components/pullquote.js +71 -0
  163. package/docs/src/pages/components/radio.js +194 -0
  164. package/docs/src/pages/components/rating.js +106 -0
  165. package/docs/src/pages/components/search.js +61 -0
  166. package/docs/src/pages/components/section.js +59 -0
  167. package/docs/src/pages/components/segmented.js +121 -0
  168. package/docs/src/pages/components/select.js +45 -0
  169. package/docs/src/pages/components/slider.js +114 -0
  170. package/docs/src/pages/components/spinner.js +73 -0
  171. package/docs/src/pages/components/stack.js +48 -0
  172. package/docs/src/pages/components/stat.js +55 -0
  173. package/docs/src/pages/components/stepper.js +66 -0
  174. package/docs/src/pages/components/table.js +45 -0
  175. package/docs/src/pages/components/testimonial.js +49 -0
  176. package/docs/src/pages/components/textarea.js +31 -0
  177. package/docs/src/pages/components/timeline.js +227 -0
  178. package/docs/src/pages/components/toggle.js +84 -0
  179. package/docs/src/pages/components/tooltip.js +48 -0
  180. package/docs/src/pages/components.js +204 -0
  181. package/docs/src/pages/config.js +193 -0
  182. package/docs/src/pages/constraints.js +99 -0
  183. package/docs/src/pages/deploy.js +233 -0
  184. package/docs/src/pages/extending.js +198 -0
  185. package/docs/src/pages/faq.js +96 -0
  186. package/docs/src/pages/getting-started.js +106 -0
  187. package/docs/src/pages/guard.js +121 -0
  188. package/docs/src/pages/home.js +401 -0
  189. package/docs/src/pages/how-it-works.js +183 -0
  190. package/docs/src/pages/hydration.js +98 -0
  191. package/docs/src/pages/images.js +121 -0
  192. package/docs/src/pages/meta.js +120 -0
  193. package/docs/src/pages/mutations.js +106 -0
  194. package/docs/src/pages/navigation.js +85 -0
  195. package/docs/src/pages/performance.js +157 -0
  196. package/docs/src/pages/persist.js +88 -0
  197. package/docs/src/pages/project-structure.js +90 -0
  198. package/docs/src/pages/prompt-examples.js +186 -0
  199. package/docs/src/pages/raw-responses.js +124 -0
  200. package/docs/src/pages/routing.js +99 -0
  201. package/docs/src/pages/server-api.js +281 -0
  202. package/docs/src/pages/server-data.js +185 -0
  203. package/docs/src/pages/slash-commands.js +55 -0
  204. package/docs/src/pages/spec.js +207 -0
  205. package/docs/src/pages/state.js +101 -0
  206. package/docs/src/pages/store.js +181 -0
  207. package/docs/src/pages/streaming.js +108 -0
  208. package/docs/src/pages/stripe.js +193 -0
  209. package/docs/src/pages/supabase.js +323 -0
  210. package/docs/src/pages/testing.js +198 -0
  211. package/docs/src/pages/validation.js +138 -0
  212. package/examples/contact.js +166 -0
  213. package/examples/counter.js +94 -0
  214. package/examples/dev.server.js +91 -0
  215. package/examples/examples.test.js +394 -0
  216. package/examples/pricing.js +244 -0
  217. package/examples/products.js +191 -0
  218. package/examples/quiz.js +208 -0
  219. package/examples/shared.js +78 -0
  220. package/examples/todos.js +162 -0
  221. package/package.json +75 -0
  222. package/public/.pulse-ui-version +1 -0
  223. package/public/chippy-bird.css +246 -0
  224. package/public/examples/contact.css +119 -0
  225. package/public/examples/counter.css +79 -0
  226. package/public/examples/pricing.css +132 -0
  227. package/public/examples/products.css +100 -0
  228. package/public/examples/quiz.css +200 -0
  229. package/public/examples/todos.css +137 -0
  230. package/public/favicon.ico +0 -0
  231. package/public/log-dashboard.css +383 -0
  232. package/public/pulse-ui.css +2740 -0
  233. package/public/pulse-ui.js +236 -0
  234. package/public/pulse.css +149 -0
  235. package/scripts/build.js +411 -0
  236. package/src/agent/checklist.md +111 -0
  237. package/src/agent/coverage-check.js +66 -0
  238. package/src/agent/guide-components.md +274 -0
  239. package/src/agent/guide-examples.md +54 -0
  240. package/src/agent/guide-routing.md +36 -0
  241. package/src/agent/guide-server.md +258 -0
  242. package/src/agent/guide-spec.md +103 -0
  243. package/src/agent/guide-styles.md +191 -0
  244. package/src/agent/guide.md +979 -0
  245. package/src/agent/identity.md +106 -0
  246. package/src/agent/workflow.md +108 -0
  247. package/src/cli/cli.test.js +82 -0
  248. package/src/cli/dev.js +195 -0
  249. package/src/cli/discover.js +113 -0
  250. package/src/cli/index.js +361 -0
  251. package/src/cli/load-report.js +91 -0
  252. package/src/cli/load-runner.js +121 -0
  253. package/src/cli/report-server.js +723 -0
  254. package/src/cli/report.js +116 -0
  255. package/src/cli/scaffold.archive.js +1371 -0
  256. package/src/cli/scaffold.js +349 -0
  257. package/src/cli/start.js +74 -0
  258. package/src/html.js +19 -0
  259. package/src/mcp/server.js +884 -0
  260. package/src/mcp/validate-worker.js +110 -0
  261. package/src/runtime/image.js +74 -0
  262. package/src/runtime/image.test.js +111 -0
  263. package/src/runtime/index.js +621 -0
  264. package/src/runtime/navigate.js +146 -0
  265. package/src/runtime/runtime.test.js +773 -0
  266. package/src/runtime/ssr.js +464 -0
  267. package/src/runtime/ssr.test.js +421 -0
  268. package/src/runtime/store.js +92 -0
  269. package/src/runtime/toast.js +163 -0
  270. package/src/server/index.js +1386 -0
  271. package/src/server/server.test.js +1248 -0
  272. package/src/spec/schema.js +428 -0
  273. package/src/spec/schema.test.js +291 -0
  274. package/src/store/index.js +102 -0
  275. package/src/store/store.test.js +210 -0
  276. package/src/testing/html.js +283 -0
  277. package/src/testing/index.js +249 -0
  278. package/src/testing/testing.test.js +450 -0
  279. package/src/ui/accordion.js +28 -0
  280. package/src/ui/alert.js +43 -0
  281. package/src/ui/app-badge.js +48 -0
  282. package/src/ui/avatar.js +47 -0
  283. package/src/ui/badge.js +24 -0
  284. package/src/ui/banner.js +26 -0
  285. package/src/ui/breadcrumbs.js +38 -0
  286. package/src/ui/button.js +66 -0
  287. package/src/ui/card.js +34 -0
  288. package/src/ui/carousel.js +59 -0
  289. package/src/ui/charts.js +321 -0
  290. package/src/ui/checkbox.js +65 -0
  291. package/src/ui/cluster.js +44 -0
  292. package/src/ui/code-window.js +39 -0
  293. package/src/ui/container.js +24 -0
  294. package/src/ui/cta.js +37 -0
  295. package/src/ui/divider.js +29 -0
  296. package/src/ui/empty.js +33 -0
  297. package/src/ui/feature.js +33 -0
  298. package/src/ui/fieldset.js +37 -0
  299. package/src/ui/fileupload.js +89 -0
  300. package/src/ui/footer.js +38 -0
  301. package/src/ui/grid.js +36 -0
  302. package/src/ui/heading.js +45 -0
  303. package/src/ui/hero.js +37 -0
  304. package/src/ui/icons.js +161 -0
  305. package/src/ui/index.js +89 -0
  306. package/src/ui/input.js +74 -0
  307. package/src/ui/list.js +36 -0
  308. package/src/ui/media.js +44 -0
  309. package/src/ui/modal.js +80 -0
  310. package/src/ui/nav.js +61 -0
  311. package/src/ui/pricing.js +56 -0
  312. package/src/ui/progress.js +62 -0
  313. package/src/ui/prose.js +29 -0
  314. package/src/ui/pullquote.js +34 -0
  315. package/src/ui/radio.js +102 -0
  316. package/src/ui/rating.js +93 -0
  317. package/src/ui/search.js +77 -0
  318. package/src/ui/section.js +69 -0
  319. package/src/ui/segmented.js +50 -0
  320. package/src/ui/select.js +77 -0
  321. package/src/ui/slider.js +84 -0
  322. package/src/ui/spinner.js +34 -0
  323. package/src/ui/stack.js +36 -0
  324. package/src/ui/stat.js +52 -0
  325. package/src/ui/stepper.js +46 -0
  326. package/src/ui/switch.js +57 -0
  327. package/src/ui/table.js +45 -0
  328. package/src/ui/testimonial.js +48 -0
  329. package/src/ui/textarea.js +72 -0
  330. package/src/ui/timeline.js +72 -0
  331. package/src/ui/tooltip.js +28 -0
  332. package/src/ui/ui.test.js +1241 -0
  333. package/src/ui/uiimage.js +65 -0
  334. package/tsconfig.json +13 -0
  335. package/types/html.d.ts +17 -0
  336. package/types/image.d.ts +70 -0
  337. package/types/index.d.ts +7 -0
  338. package/types/navigate.d.ts +38 -0
  339. package/types/runtime.d.ts +63 -0
  340. package/types/schema.d.ts +243 -0
  341. package/types/server.d.ts +145 -0
  342. package/types/ssr.d.ts +110 -0
  343. package/types/testing.d.ts +154 -0
  344. package/types/ui.d.ts +704 -0
@@ -0,0 +1,193 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/config')
6
+
7
+ export default {
8
+ route: '/config',
9
+ meta: {
10
+ title: 'Configuration — Pulse Docs',
11
+ description: 'Full reference for pulse.config.js — port, Lighthouse thresholds, load test config, environments, and per-route overrides.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/config',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Configuration')}
21
+ ${lead('<code>pulse.config.js</code> sets the performance and load thresholds that Pulse enforces across your application. All fields are optional — the defaults match Google\'s Core Web Vitals "good" band. Configuration here is not about enabling features; it is about deciding where, if anywhere, you need to lower a guaranteed baseline.')}
22
+
23
+ ${section('schema', 'Full schema')}
24
+ ${codeBlock(highlight(`// pulse.config.js
25
+ export default {
26
+ port: 3000,
27
+
28
+ lighthouse: {
29
+ // Category scores — 0 to 100. Default: 100 for all four.
30
+ performance: 100,
31
+ accessibility: 100,
32
+ bestPractices: 100,
33
+ seo: 100,
34
+
35
+ // Core Web Vitals and timing metrics.
36
+ // Defaults are the Google "good" thresholds.
37
+ lcp: 2500, // Largest Contentful Paint (ms)
38
+ cls: 0.1, // Cumulative Layout Shift
39
+ tbt: 200, // Total Blocking Time (ms)
40
+ fcp: 1800, // First Contentful Paint (ms)
41
+ si: 3400, // Speed Index (ms)
42
+ inp: 200, // Interaction to Next Paint (ms)
43
+ },
44
+
45
+ load: {
46
+ duration: 10,
47
+ connections: 10,
48
+ thresholds: {
49
+ rps: undefined, // minimum req/s (optional)
50
+ p99: undefined, // maximum p99 latency ms (optional)
51
+ errors: 0,
52
+ },
53
+ },
54
+
55
+ environments: {
56
+ // Environment names are bespoke — choose whatever suits your project.
57
+ local: { url: 'http://localhost:3000', default: true },
58
+ staging: {
59
+ url: 'https://staging.myapp.com',
60
+ headers: { Authorization: \`Bearer \${process.env.STAGING_TOKEN}\` },
61
+ load: { duration: 30, connections: 50 },
62
+ lighthouse: { performance: 90 },
63
+ },
64
+ production: { url: 'https://myapp.com' },
65
+ },
66
+
67
+ routes: {
68
+ // Per-route overrides — merged on top of global lighthouse/load config.
69
+ // Only specify what differs from the global defaults.
70
+ '/dashboard': {
71
+ lighthouse: {
72
+ performance: 85,
73
+ lcp: 4000,
74
+ },
75
+ },
76
+ '/embed': {
77
+ lighthouse: {
78
+ bestPractices: 85,
79
+ },
80
+ },
81
+ },
82
+ }`, 'js'))}
83
+
84
+ ${section('port', 'port')}
85
+ ${table(
86
+ ['Field', 'Type', 'Default', 'Description'],
87
+ [
88
+ ['<code>port</code>', '<code>number</code>', '<code>3000</code>', 'Port the dev and production servers listen on.'],
89
+ ]
90
+ )}
91
+
92
+ ${section('lighthouse', 'lighthouse')}
93
+ <p>Global Lighthouse thresholds that every page must meet. <code>/pulse-report</code> enforces these after every audit — any page that falls below a threshold is reported as a failure, not a warning.</p>
94
+ ${table(
95
+ ['Field', 'Type', 'Default', 'Description'],
96
+ [
97
+ ['<code>performance</code>', '<code>number</code>', '<code>100</code>', 'Lighthouse Performance category score (0–100).'],
98
+ ['<code>accessibility</code>', '<code>number</code>', '<code>100</code>', 'Lighthouse Accessibility category score (0–100).'],
99
+ ['<code>bestPractices</code>', '<code>number</code>', '<code>100</code>', 'Lighthouse Best Practices category score (0–100).'],
100
+ ['<code>seo</code>', '<code>number</code>', '<code>100</code>', 'Lighthouse SEO category score (0–100).'],
101
+ ['<code>lcp</code>', '<code>number</code>', '<code>2500</code>', 'Largest Contentful Paint budget (ms).'],
102
+ ['<code>cls</code>', '<code>number</code>', '<code>0.1</code>', 'Cumulative Layout Shift budget.'],
103
+ ['<code>tbt</code>', '<code>number</code>', '<code>200</code>', 'Total Blocking Time budget (ms).'],
104
+ ['<code>fcp</code>', '<code>number</code>', '<code>1800</code>', 'First Contentful Paint budget (ms).'],
105
+ ['<code>si</code>', '<code>number</code>', '<code>3400</code>', 'Speed Index budget (ms).'],
106
+ ['<code>inp</code>', '<code>number</code>', '<code>200</code>', 'Interaction to Next Paint budget (ms).'],
107
+ ]
108
+ )}
109
+ ${callout('note', 'Unset fields default to the values above. Setting a field to <code>null</code> removes that check for the project — use sparingly. Raising a threshold is always preferable to disabling it.')}
110
+
111
+ ${section('routes', 'routes')}
112
+ <p>Route-specific overrides let you lower a threshold for a specific page without relaxing the global guarantee. Only specify the fields that differ — everything else inherits from the global config.</p>
113
+ ${codeBlock(highlight(`routes: {
114
+ // This route uses a third-party chart library — relax performance only.
115
+ '/dashboard': {
116
+ lighthouse: {
117
+ performance: 85,
118
+ lcp: 4000,
119
+ },
120
+ },
121
+ }`, 'js'))}
122
+ ${callout('note', 'Route keys must exactly match the spec <code>route</code> field, including any leading slash. Dynamic segments are not supported — create a specific override for each route pattern.')}
123
+
124
+ ${section('load', 'load')}
125
+ <p>Load test thresholds enforced by <code>/pulse-load</code>. All fields are optional — omitting a threshold means that check is not enforced.</p>
126
+ ${table(
127
+ ['Field', 'Type', 'Default', 'Description'],
128
+ [
129
+ ['<code>duration</code>', '<code>number</code>', '<code>10</code>', 'Test duration in seconds.'],
130
+ ['<code>connections</code>', '<code>number</code>', '<code>10</code>', 'Number of concurrent request chains.'],
131
+ ['<code>thresholds.rps</code>', '<code>number</code>', '<code>undefined</code>', 'Minimum acceptable requests per second. Unset = no check.'],
132
+ ['<code>thresholds.p99</code>', '<code>number</code>', '<code>undefined</code>', 'Maximum acceptable p99 latency (ms). Unset = no check.'],
133
+ ['<code>thresholds.errors</code>', '<code>number</code>', '<code>0</code>', 'Maximum acceptable error count.'],
134
+ ]
135
+ )}
136
+ ${codeBlock(highlight(`load: {
137
+ duration: 30,
138
+ connections: 20,
139
+ thresholds: {
140
+ rps: 100, // fail if below 100 req/s
141
+ p99: 500, // fail if p99 exceeds 500ms
142
+ errors: 0,
143
+ },
144
+ },`, 'js'))}
145
+ <p>Per-route overrides follow the same pattern as <code>lighthouse</code>:</p>
146
+ ${codeBlock(highlight(`routes: {
147
+ '/feed': {
148
+ load: { connections: 5, thresholds: { rps: 20 } },
149
+ },
150
+ }`, 'js'))}
151
+ <p>Results are saved to <code>.pulse/load-reports/</code> and displayed in the Load Tests tab of the report dashboard, alongside the Lighthouse Performance tab for the same route.</p>
152
+
153
+ ${section('environments', 'environments')}
154
+ <p>Named environments let you enforce thresholds against different targets — local, staging, production — from the same config. Thresholds are applied per-environment, so staging and production can have different performance floors.</p>
155
+ ${table(
156
+ ['Field', 'Type', 'Description'],
157
+ [
158
+ ['<code>url</code>', '<code>string</code>', 'Base URL to test against. If it contains <code>localhost</code> or <code>127.0.0.1</code>, a local production build is spun up automatically. Remote URLs are tested directly.'],
159
+ ['<code>default</code>', '<code>boolean</code>', 'The environment used when none is explicitly specified by <code>pulse report</code> and <code>pulse load-test</code>.'],
160
+ ['<code>headers</code>', '<code>object</code>', 'HTTP headers sent with every request to this environment. Useful for authorization tokens on protected staging deployments — read values from <code>process.env</code> rather than hardcoding them.'],
161
+ ['<code>load</code>', '<code>object</code>', 'Load test config overrides for this environment. Same fields as the global <code>load</code> block. Merged on top of global config.'],
162
+ ['<code>lighthouse</code>', '<code>object</code>', 'Lighthouse threshold overrides for this environment. Same fields as the global <code>lighthouse</code> block. Merged on top of global config.'],
163
+ ]
164
+ )}
165
+ ${callout('note', 'Environment names are fully bespoke — <code>local</code>, <code>staging</code>, <code>production</code>, <code>preview</code>, <code>eu</code> — whatever maps to your project\'s infrastructure. There are no reserved names.')}
166
+ <p>Threshold merge order: <strong>global config → environment override → per-route override</strong>.</p>
167
+ ${codeBlock(highlight(`environments: {
168
+ local: { url: 'http://localhost:3000', default: true },
169
+ staging: {
170
+ url: 'https://staging.myapp.com',
171
+ headers: { Authorization: \`Bearer \${process.env.STAGING_TOKEN}\` },
172
+ load: { duration: 30, connections: 50, thresholds: { rps: 500 } },
173
+ lighthouse: { performance: 90 },
174
+ },
175
+ production: { url: 'https://myapp.com' },
176
+ }`, 'js'))}
177
+
178
+ ${section('defaults', 'CWV default thresholds')}
179
+ <p>The default metric thresholds match the Google "good" band from the Core Web Vitals specification:</p>
180
+ ${table(
181
+ ['Metric', 'Default', 'Google "good" threshold'],
182
+ [
183
+ ['LCP', '<code>2500 ms</code>', '≤ 2500 ms'],
184
+ ['CLS', '<code>0.1</code>', '≤ 0.1'],
185
+ ['TBT', '<code>200 ms</code>', '≤ 200 ms'],
186
+ ['FCP', '<code>1800 ms</code>', '≤ 1800 ms'],
187
+ ['SI', '<code>3400 ms</code>', '≤ 3400 ms'],
188
+ ['INP', '<code>200 ms</code>', '≤ 200 ms'],
189
+ ]
190
+ )}
191
+ `,
192
+ }),
193
+ }
@@ -0,0 +1,99 @@
1
+ import { renderLayout, h1, lead, section, sub, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/constraints')
6
+
7
+ export default {
8
+ route: '/constraints',
9
+ meta: {
10
+ title: 'Constraints — Pulse Docs',
11
+ description: 'Automatic min/max bounds on state values, enforced after every mutation.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/constraints',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Constraints')}
21
+ ${lead('Constraints are always-on bounds for numeric state values. After every mutation, Pulse clamps constrained values to their declared range before the view re-renders. The value can never go out of range — there is no code path where it can.')}
22
+
23
+ ${section('declaring', 'Declaring constraints')}
24
+ <p>The <code>constraints</code> field maps top-level state keys to bounds objects with optional <code>min</code> and <code>max</code> properties:</p>
25
+ ${codeBlock(highlight(`export default {
26
+ route: '/cart',
27
+ state: {
28
+ quantity: 1,
29
+ zoom: 1.0,
30
+ rating: 0,
31
+ },
32
+ constraints: {
33
+ quantity: { min: 1, max: 99 },
34
+ zoom: { min: 0.5, max: 3.0 },
35
+ rating: { min: 0, max: 5 },
36
+ },
37
+ mutations: {
38
+ increaseQty: (state) => ({ quantity: state.quantity + 1 }),
39
+ decreaseQty: (state) => ({ quantity: state.quantity - 1 }),
40
+ zoomIn: (state) => ({ zoom: state.zoom + 0.1 }),
41
+ zoomOut: (state) => ({ zoom: state.zoom - 0.1 }),
42
+ },
43
+ }`, 'js'))}
44
+ <p>When <code>decreaseQty</code> runs and <code>quantity</code> is already 1, the constraint clamps it back to 1 before the view renders. The mutation does not need to check bounds — the spec declares them once and Pulse enforces them everywhere.</p>
45
+
46
+ ${section('vs-validation', 'Constraints vs Validation')}
47
+ ${table(
48
+ ['', 'Constraints', 'Validation'],
49
+ [
50
+ ['When it runs', 'After <strong>every</strong> mutation, automatically', 'Only when an action has <code>validate: true</code>'],
51
+ ['What it does', 'Clamps numeric values silently', 'Rejects the action and surfaces errors'],
52
+ ['User feedback', 'None — state is silently corrected', 'Explicit error messages shown in the view'],
53
+ ['Best for', 'Numeric ranges that must never be exceeded', 'Form field correctness before submission'],
54
+ ]
55
+ )}
56
+
57
+ ${callout('note', 'Constraints and validation serve different purposes. Constraints silently enforce numeric bounds at every mutation — they cannot be bypassed. Validation rejects invalid form data before an action\'s async work begins — it only runs when explicitly declared.')}
58
+
59
+ ${section('one-sided', 'One-sided bounds')}
60
+ <p>Either <code>min</code> or <code>max</code> can be declared alone — both are optional:</p>
61
+ ${codeBlock(highlight(`constraints: {
62
+ count: { min: 0 }, // no upper limit
63
+ discount: { max: 100 }, // no lower limit
64
+ offset: { min: 0, max: 999 } // both bounds
65
+ }`, 'js'))}
66
+
67
+ ${section('how-it-works', 'How clamping works')}
68
+ <p>After Pulse applies a mutation's partial state update, it iterates over all declared constraints and applies <code>Math.max(min, Math.min(max, value))</code> to each constrained key. The view is then called with the clamped state.</p>
69
+ ${codeBlock(highlight(`// state.count = 10, constraints.count = { min: 0, max: 10 }
70
+ mutations: {
71
+ increment: (state) => ({ count: state.count + 1 }),
72
+ // mutation returns { count: 11 }
73
+ // constraint clamps to 10
74
+ // view receives { count: 10 }
75
+ }`, 'js'))}
76
+
77
+ ${section('top-level', 'Top-level keys only')}
78
+ <p>Constraints apply to top-level state keys. To constrain nested values, consider flattening your state structure or applying bounds logic in the mutation itself:</p>
79
+ ${codeBlock(highlight(`// Cannot do:
80
+ constraints: {
81
+ 'player.health': { min: 0, max: 100 } // ✗ nested paths not supported
82
+ }
83
+
84
+ // Do instead:
85
+ state: { playerHealth: 100 },
86
+ constraints: { playerHealth: { min: 0, max: 100 } },
87
+
88
+ // Or handle in the mutation:
89
+ mutations: {
90
+ takeDamage: (state, _, amount) => ({
91
+ player: {
92
+ ...state.player,
93
+ health: Math.max(0, state.player.health - amount),
94
+ }
95
+ })
96
+ }`, 'js'))}
97
+ `,
98
+ }),
99
+ }
@@ -0,0 +1,233 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/deploy')
6
+
7
+ export default {
8
+ route: '/deploy',
9
+ meta: {
10
+ title: 'Deployment — Pulse Docs',
11
+ description: 'Deploy a Pulse app to a VPS, Docker, Fly.io, Railway, or Render.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ state: {},
15
+ view: () => renderLayout({
16
+ currentHref: '/deploy',
17
+ prev,
18
+ next,
19
+ content: `
20
+ ${h1('Deployment')}
21
+ ${lead('Pulse deploys as a single Node.js process. No adapters, no serverless wrapping, no separate static file server required. Build once, run anywhere Node 22+ runs. All guarantees — security headers, brotli compression, immutable asset caching — are active in production automatically.')}
22
+
23
+ ${section('build', 'Build')}
24
+ <p>Run the production build before deploying:</p>
25
+ ${codeBlock(highlight(`npm run build`, 'bash'))}
26
+ <p>This generates content-hashed bundles in <code>public/dist/</code> and writes <code>public/dist/manifest.json</code>. The server reads the manifest at startup to resolve hydration script paths.</p>
27
+ ${callout('warning', 'Without a manifest, the server falls back to serving source files directly — no compression, no content-hashed filenames, and no <code>immutable</code> cache headers. Always run <code>npm run build</code> before deploying to production.')}
28
+
29
+ ${section('files', 'What to deploy')}
30
+ ${table(
31
+ ['Include', 'Reason'],
32
+ [
33
+ ['<code>src/</code>', 'Page specs — imported by the server at runtime'],
34
+ ['<code>public/</code>', 'Static assets and built bundles (<code>public/dist/</code>)'],
35
+ ['<code>server.js</code>', 'Entry point'],
36
+ ['<code>pulse.config.js</code>', 'Server config'],
37
+ ['<code>package.json</code> + <code>node_modules/</code>', 'Runtime dependencies'],
38
+ ]
39
+ )}
40
+ ${table(
41
+ ['Exclude', 'Reason'],
42
+ [
43
+ ['<code>.claude/</code>', 'AI agent config — not needed at runtime'],
44
+ ['<code>.pulse/</code>', 'Local report data — not needed at runtime'],
45
+ ]
46
+ )}
47
+
48
+ ${section('env-vars', 'Environment variables')}
49
+ ${table(
50
+ ['Variable', 'Default', 'Description'],
51
+ [
52
+ ['<code>NODE_ENV</code>', '<code>development</code>', 'Set to <code>production</code> to enable HSTS headers and production cache behaviour.'],
53
+ ['<code>PORT</code>', 'Value in <code>pulse.config.js</code> (default <code>3000</code>)', 'Override the listening port. Most PaaS platforms set this automatically.'],
54
+ ]
55
+ )}
56
+ ${codeBlock(highlight(`NODE_ENV=production pulse start`, 'bash'))}
57
+
58
+ ${section('pm2', 'VPS with PM2')}
59
+ <p>PM2 keeps the process alive, restarts it on crash, and manages logs.</p>
60
+ ${codeBlock(highlight(`# Install PM2 globally
61
+ npm install -g pm2
62
+
63
+ # Start the app
64
+ NODE_ENV=production pm2 start server.js --name myapp
65
+
66
+ # Persist across reboots
67
+ pm2 save
68
+ pm2 startup
69
+
70
+ # Zero-downtime reload after a deploy
71
+ pm2 reload myapp`, 'bash'))}
72
+ <p>For repeatable deployments, check an <code>ecosystem.config.cjs</code> into version control:</p>
73
+ ${codeBlock(highlight(`// ecosystem.config.cjs
74
+ module.exports = {
75
+ apps: [{
76
+ name: 'myapp',
77
+ script: 'server.js',
78
+ env_production: {
79
+ NODE_ENV: 'production',
80
+ PORT: 3000,
81
+ },
82
+ }],
83
+ }`, 'js'))}
84
+ ${codeBlock(highlight(`pm2 start ecosystem.config.cjs --env production`, 'bash'))}
85
+
86
+ ${section('docker', 'Docker')}
87
+ <p>A two-stage build keeps the image small — build tools stay in the first stage.</p>
88
+ ${codeBlock(highlight(`# ---- build stage ----
89
+ FROM node:22-alpine AS build
90
+ WORKDIR /app
91
+ COPY package*.json ./
92
+ RUN npm ci
93
+ COPY . .
94
+ RUN npx pulse build
95
+
96
+ # ---- runtime stage ----
97
+ FROM node:22-alpine
98
+ WORKDIR /app
99
+ ENV NODE_ENV=production
100
+ COPY package*.json ./
101
+ RUN npm ci --omit=dev
102
+ COPY --from=build /app/src ./src
103
+ COPY --from=build /app/public ./public
104
+ COPY --from=build /app/server.js ./server.js
105
+ COPY pulse.config.js ./
106
+ EXPOSE 3000
107
+ CMD ["node", "server.js"]`, 'bash'))}
108
+ ${codeBlock(highlight(`docker build -t myapp .
109
+ docker run -p 3000:3000 --env NODE_ENV=production myapp`, 'bash'))}
110
+
111
+ ${section('fly', 'Fly.io')}
112
+ ${codeBlock(highlight(`# fly.toml
113
+ app = 'myapp'
114
+ primary_region = 'lhr'
115
+
116
+ [env]
117
+ NODE_ENV = 'production'
118
+
119
+ [build]
120
+ [build.args]
121
+ NODE_VERSION = '22'
122
+
123
+ [deploy]
124
+ release_command = 'npx pulse build'
125
+
126
+ [http_service]
127
+ internal_port = 3000
128
+ force_https = true
129
+ auto_stop_machines = 'stop'
130
+ auto_start_machines = true
131
+
132
+ [[vm]]
133
+ memory = '256mb'
134
+ cpu_kind = 'shared'
135
+ cpus = 1`, 'bash'))}
136
+ ${codeBlock(highlight(`# First deploy
137
+ fly launch
138
+
139
+ # Subsequent deploys
140
+ fly deploy`, 'bash'))}
141
+
142
+ ${section('railway', 'Railway')}
143
+ <p>Railway auto-detects Node apps. Add a <code>railway.json</code> to set the build and start commands:</p>
144
+ ${codeBlock(highlight(`{
145
+ "$schema": "https://railway.app/railway.schema.json",
146
+ "build": {
147
+ "builder": "NIXPACKS",
148
+ "buildCommand": "npm run build"
149
+ },
150
+ "deploy": {
151
+ "startCommand": "NODE_ENV=production node server.js",
152
+ "healthcheckPath": "/",
153
+ "restartPolicyType": "ON_FAILURE"
154
+ }
155
+ }`, 'js'))}
156
+
157
+ ${section('render', 'Render')}
158
+ ${table(
159
+ ['Setting', 'Value'],
160
+ [
161
+ ['Environment', 'Node'],
162
+ ['Build command', '<code>npm install &amp;&amp; npm run build</code>'],
163
+ ['Start command', '<code>NODE_ENV=production node server.js</code>'],
164
+ ['Node version', '<code>22.x</code>'],
165
+ ]
166
+ )}
167
+ <p>Set <code>NODE_ENV=production</code> in the Render environment variables dashboard.</p>
168
+
169
+ ${section('serverless', 'Vercel, Cloudflare, and edge platforms')}
170
+ <p>These platforms each have multiple products with very different runtimes — the compatibility story varies significantly between them.</p>
171
+
172
+ ${section('vercel', 'Vercel')}
173
+ <p>Vercel has two distinct runtimes:</p>
174
+ ${table(
175
+ ['Product', 'Runtime', 'Pulse compatible?'],
176
+ [
177
+ ['<strong>Functions</strong> (Node.js)', 'Full Node.js — same built-ins as a VPS', 'Partially — see below'],
178
+ ['<strong>Edge Functions</strong>', 'V8 isolates (no Node built-ins)', 'No'],
179
+ ]
180
+ )}
181
+ <p><strong>Vercel Functions (Node.js)</strong> can run Pulse with some differences in behaviour:</p>
182
+ ${table(
183
+ ['Feature', 'Behaviour on Vercel Functions'],
184
+ [
185
+ ['<code>serverTtl</code> cache', 'Works within a warm instance, but cold starts reset it. Not reliable for expensive queries.'],
186
+ ['Streaming SSR', 'Vercel Functions support streaming responses, but require explicit configuration via <code>supportsResponseStreaming</code>.'],
187
+ ['Static files', 'Vercel serves <code>public/</code> automatically via its CDN — Pulse\'s static file serving is bypassed.'],
188
+ ['Security headers', 'Work as normal — Pulse adds them to every response.'],
189
+ ]
190
+ )}
191
+ ${callout('warning', 'Vercel Functions are not a tested or officially supported deployment target for Pulse. The adapter pattern (exporting a request handler rather than starting a server) is not yet documented. Railway, Render, or Fly.io are simpler choices with no adaptation required.')}
192
+
193
+ ${section('cloudflare', 'Cloudflare')}
194
+ ${table(
195
+ ['Product', 'Runtime', 'Pulse compatible?'],
196
+ [
197
+ ['<strong>Workers</strong>', 'V8 isolates — no <code>node:http</code>, <code>node:fs</code>, <code>node:zlib</code>', 'No'],
198
+ ['<strong>Pages Functions</strong>', 'Same V8 isolate runtime as Workers', 'No'],
199
+ ['<strong>CDN / proxy</strong>', 'Sits in front of your origin server', 'Yes — works great with Fly.io or a VPS behind it'],
200
+ ]
201
+ )}
202
+ ${callout('tip', 'The recommended pattern for edge performance: deploy Pulse to <strong>Fly.io</strong> (which runs real VMs in many regions) and put <strong>Cloudflare as a CDN/proxy</strong> in front of it. Static assets and cached HTML are served from Cloudflare\'s edge; dynamic requests are proxied to the nearest Fly VM.')}
203
+
204
+ ${section('https', 'HTTPS and reverse proxy')}
205
+ <p>Pulse detects TLS automatically. When a request arrives with an <code>x-forwarded-proto: https</code> header or over a direct TLS socket, <code>Strict-Transport-Security: max-age=31536000; includeSubDomains</code> is added to the response. All four platforms above forward this header — no Pulse config is needed.</p>
206
+ <p>If running behind nginx for TLS termination:</p>
207
+ ${codeBlock(highlight(`# nginx — TLS termination, proxy to Pulse
208
+ server {
209
+ listen 443 ssl;
210
+ server_name myapp.com;
211
+
212
+ ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
213
+ ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
214
+
215
+ location / {
216
+ proxy_pass http://localhost:3000;
217
+ proxy_http_version 1.1;
218
+ proxy_set_header Host $host;
219
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
220
+ proxy_set_header X-Forwarded-Proto $scheme;
221
+ }
222
+ }
223
+
224
+ # Redirect HTTP to HTTPS
225
+ server {
226
+ listen 80;
227
+ server_name myapp.com;
228
+ return 301 https://$host$request_uri;
229
+ }`, 'bash'))}
230
+ ${callout('tip', 'Use <a href="https://certbot.eff.org">Certbot</a> to obtain and auto-renew a free Let\'s Encrypt certificate: <code>certbot --nginx -d myapp.com</code>.')}
231
+ `,
232
+ }),
233
+ }