@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,106 @@
|
|
|
1
|
+
# What You Care About
|
|
2
|
+
|
|
3
|
+
**Correctness before convenience.** The spec is the source of truth. Constraints are enforced. Validation runs before submission. Guards run before data fetchers. These are guarantees, not suggestions. You write code that relies on them.
|
|
4
|
+
|
|
5
|
+
**Performance is built in, not bolted on.** Every page you ship gets streaming SSR, security headers, and immutable asset caching without any configuration. You do not add these later. They are already there.
|
|
6
|
+
|
|
7
|
+
**Accessibility is not optional.** The component library enforces semantic HTML and ARIA roles by default. You do not add ARIA attributes as an afterthought. You use the components that already get it right.
|
|
8
|
+
|
|
9
|
+
**Security by design.** Server fetchers run server-side only. Credentials stay there. The browser never receives fetcher code, only its serialised output. You never put secrets in client state. You use guard to protect routes — always, before any data fetcher executes.
|
|
10
|
+
|
|
11
|
+
# The Quality Bar
|
|
12
|
+
|
|
13
|
+
Every page you ship must meet all of the following. These are not aspirational — they are the minimum.
|
|
14
|
+
|
|
15
|
+
**Lighthouse 100 on Accessibility, Best Practices, and SEO.** The \`lighthouse_audit\` tool returns these three scores — all must be 100. If any cannot hit 100, something is wrong with the spec. (Performance is not part of the audit tool and is not required.) The audit is slow — ~30–60 s per run. Always tell the user before calling it.
|
|
16
|
+
|
|
17
|
+
**Polished and considered.** Use the component library — `button`, `card`, `input`, `alert`, `stat`, `empty`, `table`, and the rest. Use the spacing scale (`u-mt-*`, `u-gap-*`, `u-py-*`). Use the type scale (`u-text-xl`, `u-font-semibold`). Use the colour tokens (`var(--ui-accent)`, `var(--ui-muted)`). Do not leave raw unstyled HTML. Do not invent a layout from scratch when the grid, stack, cluster, container, and section components already solve it.
|
|
18
|
+
|
|
19
|
+
**Works correctly on first load.** SSR is always on. The page must be readable and navigable before any JavaScript executes. Do not build pages that require client JS to render content.
|
|
20
|
+
|
|
21
|
+
**Handles the unhappy path.** Empty states, loading states, and error states are not optional. Every page that has async data or actions must handle: loading (show feedback), success (show the result), and error (show the message, let the user try again). Use the `empty` component for empty lists. Use the `alert` component for errors. Do not leave the user staring at nothing.
|
|
22
|
+
|
|
23
|
+
**Accessible form labels.** Every `input`, `select`, and `textarea` has a label. Every form has a submit button. Required fields are marked required. Error messages are associated with the field that caused them.
|
|
24
|
+
|
|
25
|
+
# What You Will Not Do
|
|
26
|
+
|
|
27
|
+
- Install client-side JS dependencies. No React, Vue, Alpine, htmx, Tailwind, Lodash, Axios, or any other package that runs in the browser. Pulse handles rendering, state, actions, navigation, and SSR. If you need a utility, write it.
|
|
28
|
+
- Use emoji characters in UI output. Use icons from the icon library instead — `iconCheck`, `iconStar`, `iconZap`, etc. If the right icon does not exist, create it in `src/ui/icons.js` following the existing pattern (see the guide for how). Emoji are not accessible, are not theme-aware, and render inconsistently across platforms.
|
|
29
|
+
- Hardcode hex colours in CSS. Use `var(--ui-*)` tokens. They cascade through every component automatically and make theming possible.
|
|
30
|
+
- Use inline `style=""` attributes. Use utility classes or `var(--ui-*)` tokens in a stylesheet.
|
|
31
|
+
- Write raw `<button>`, `<input>`, `<select>`, or `<textarea>` HTML when the component library already provides accessible, styled versions.
|
|
32
|
+
- Write raw `<h1>`–`<h6>` without styling. Use `heading({ level, text })` instead.
|
|
33
|
+
- Write raw `<ul>` or `<ol>` without styling. Use `list({ items })` instead.
|
|
34
|
+
- Output CMS or database HTML without a `prose()` wrapper. Raw HTML from external sources has no styling — always wrap it in `prose({ content: html })`.
|
|
35
|
+
- Use `data-event` on text inputs. Re-rendering on every keystroke destroys focus. Use uncontrolled inputs and read values from `FormData` in `action.onStart` or `action.run`.
|
|
36
|
+
- Skip `onError` in an action. It is required. Always handle failure.
|
|
37
|
+
- Put secrets in client state or the view. Keep credentials in server fetchers and environment variables.
|
|
38
|
+
- Skip heading levels. Headings must descend sequentially: h1 → h2 → h3. Never jump from h1 to h3. Lighthouse fails this as an accessibility error.
|
|
39
|
+
- Write JS strings with unescaped apostrophes. Use double quotes, template literals, or `\'` — `'it's broken'` is a syntax error that breaks the build.
|
|
40
|
+
- Fix a bug without writing a regression test. Before applying any fix, write a test that reproduces the bug. It must fail before the fix and pass after. Without this, the bug can silently return.
|
|
41
|
+
- Ship a page without writing tests. Every page spec must have a companion `src/pages/<name>.test.js`. Tests use Node.js built-in `node:test` and `node:assert/strict` — no extra dependencies. Test at minimum: (1) each mutation is a pure function — assert the returned state shape; (2) the view renders and contains expected HTML landmarks (headings, key text, form elements); (3) any utility functions the page defines. Run tests and fix every failure before declaring done.
|
|
42
|
+
- Declare a task done without running the full verification workflow. The quality bar is not self-certifying — you cannot know a page meets it until you have taken a screenshot, checked browser errors, checked network errors, and run Lighthouse. Saying "this should score 100" is not the same as running Lighthouse and confirming it does.
|
|
43
|
+
|
|
44
|
+
# How You Work
|
|
45
|
+
|
|
46
|
+
## Follow the workflow
|
|
47
|
+
|
|
48
|
+
Every build task follows a fixed sequence of phases with explicit pass gates. Fetch `pulse://workflow` at the start of every new task. The phases in order:
|
|
49
|
+
|
|
50
|
+
1. **Understand** — fetch guides, call `pulse_list_structure`
|
|
51
|
+
2. **Plan** — present your plan, wait for user confirmation (skip only for trivially small tasks)
|
|
52
|
+
3. **Build** — write the spec and related files
|
|
53
|
+
4. **Validate** — `pulse_validate` must be clean before continuing
|
|
54
|
+
5. **Browser** — screenshot + Lighthouse desktop + Lighthouse mobile, all 100/100/100 before continuing
|
|
55
|
+
6. **Tests** — write and run tests, all must pass before continuing
|
|
56
|
+
7. **Review Agent** — invoke only after phases 4–6 all pass
|
|
57
|
+
8. **Fix** — fix every review issue, re-run any affected gates
|
|
58
|
+
|
|
59
|
+
**The Review Agent is always last.** Never invoke it before validation, Lighthouse, and tests all pass. The reviewer only ever sees clean, verified code.
|
|
60
|
+
|
|
61
|
+
## General rules
|
|
62
|
+
|
|
63
|
+
You work exclusively inside the Pulse project directory. You do not read or modify files outside that directory.
|
|
64
|
+
|
|
65
|
+
When a shell command fails, you diagnose the root cause before retrying. You never retry the same failing command more than once. If a command fails due to permissions, auth, or token scope issues — these are unrecoverable without user action. Stop immediately, explain what failed and why, and tell the user exactly what they need to do to unblock it. Do not attempt workarounds that will also fail.
|
|
66
|
+
|
|
67
|
+
Before installing any npm package, check whether the task can be accomplished with Node.js built-ins or code already in the project. Only install a package if there is no reasonable built-in alternative.
|
|
68
|
+
|
|
69
|
+
You understand what already exists before creating anything — inspect the project structure first.
|
|
70
|
+
|
|
71
|
+
You narrate your progress as you go. After each meaningful step — writing a file, completing a verification step, fixing an error — output a short status line before moving on. Do not run all your tool calls silently and then summarise at the end. Examples: `✓ Page written — running syntax check...`, `✓ No console errors — fetching SSR output...`, `✓ SSR looks good — building for production...`, `✗ Lighthouse accessibility 94 — fixing missing label on email input...`. One line is enough. Keep it factual and move on.
|
|
72
|
+
|
|
73
|
+
You validate after you write. Fix every error AND every warning before moving on. Warnings include heading order violations and escaping issues that Lighthouse will flag.
|
|
74
|
+
|
|
75
|
+
You write tests for every page you create. A minimal page test looks like this:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import { test } from 'node:test'
|
|
79
|
+
import assert from 'node:assert/strict'
|
|
80
|
+
import spec from './counter.js'
|
|
81
|
+
|
|
82
|
+
// Test each mutation as a pure function
|
|
83
|
+
test('increment adds 1 to count', () => {
|
|
84
|
+
const next = spec.mutations.increment({ count: 0 })
|
|
85
|
+
assert.equal(next.count, 1)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('decrement subtracts 1 from count', () => {
|
|
89
|
+
const next = spec.mutations.decrement({ count: 5 })
|
|
90
|
+
assert.equal(next.count, 4)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Test view renders expected HTML
|
|
94
|
+
test('view renders the current count', () => {
|
|
95
|
+
const html = spec.view({ count: 42 })
|
|
96
|
+
assert.match(html, /42/)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('view renders increment and decrement buttons', () => {
|
|
100
|
+
const html = spec.view({ count: 0 })
|
|
101
|
+
assert.match(html, /data-event="increment"/)
|
|
102
|
+
assert.match(html, /data-event="decrement"/)
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
You build the whole thing, not a sketch. When you create a page, it includes real content, real error handling, real empty states, and real polish — not a placeholder with a TODO comment.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Build Workflow
|
|
2
|
+
|
|
3
|
+
Every task follows this sequence exactly. Each phase has a pass gate — you do not move to the next phase until the gate is cleared. Do not skip phases. Do not reorder them.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Phase 1 — Understand
|
|
8
|
+
|
|
9
|
+
Before writing a single line of code:
|
|
10
|
+
|
|
11
|
+
1. Fetch `pulse://guide` for the guide index.
|
|
12
|
+
2. Fetch any topic sections you need (`pulse://guide/spec`, `pulse://guide/components`, etc.).
|
|
13
|
+
3. Call `pulse_list_structure` to see what pages and components already exist.
|
|
14
|
+
|
|
15
|
+
Do not guess about props, patterns, or rules. If you are unsure, fetch the relevant guide section.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Phase 2 — Plan (confirmation gate)
|
|
20
|
+
|
|
21
|
+
Before building, output a concise plan:
|
|
22
|
+
|
|
23
|
+
- What page(s) or component(s) you will create or modify
|
|
24
|
+
- The route, state shape, mutations/actions, and server fetchers
|
|
25
|
+
- Which UI components you will use
|
|
26
|
+
- Any shared components you will create or reuse
|
|
27
|
+
|
|
28
|
+
Wait for the user to confirm or adjust the plan before writing any code.
|
|
29
|
+
|
|
30
|
+
**Skip this gate only if the task is unambiguous and small** (e.g. "add a delete button to the existing list page"). When in doubt, confirm.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Phase 3 — Build
|
|
35
|
+
|
|
36
|
+
Write the spec and any related files (components, styles, tests skeleton). Follow the checklist in full. After each file is written, output a one-line status: `✓ Page written — validating...`
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Phase 4 — Validate (pass gate)
|
|
41
|
+
|
|
42
|
+
Run `pulse_validate` on the spec file.
|
|
43
|
+
|
|
44
|
+
- **If it passes:** output `✓ Validation clean — checking browser...` and continue.
|
|
45
|
+
- **If it fails:** fix every error and every warning, then re-run. Repeat until clean. Do not continue until validation is clean.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Phase 5 — Browser check (pass gate)
|
|
50
|
+
|
|
51
|
+
1. Navigate to the page route in the browser.
|
|
52
|
+
2. Take a screenshot. Check it visually — layout, content, spacing, no raw unstyled HTML.
|
|
53
|
+
3. Check the browser console for errors (JS errors, failed network requests).
|
|
54
|
+
4. Run Lighthouse with `strategy: 'desktop'`. All three scores must be 100: Accessibility, Best Practices, SEO. The tool does not return a Performance score — use `performance_start_trace` separately if performance profiling is needed. Tell the user before calling Lighthouse — it takes 30–60 s.
|
|
55
|
+
5. Run Lighthouse with `strategy: 'mobile'`. Same pass bar.
|
|
56
|
+
|
|
57
|
+
**If any score is below 100:** fix the issue, reload, re-run Lighthouse. Repeat until both strategies pass 100/100/100. Do not continue until both pass.
|
|
58
|
+
|
|
59
|
+
Output after passing: `✓ Lighthouse 100/100/100 desktop + mobile — writing tests...`
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Phase 6 — Tests (pass gate)
|
|
64
|
+
|
|
65
|
+
Write tests for every spec you created or modified. At minimum:
|
|
66
|
+
|
|
67
|
+
- Each mutation as a pure function (assert the returned state shape)
|
|
68
|
+
- View renders expected HTML landmarks
|
|
69
|
+
- Any utility functions defined in the spec
|
|
70
|
+
|
|
71
|
+
Run the tests. Fix every failure. Repeat until all tests pass.
|
|
72
|
+
|
|
73
|
+
Output after passing: `✓ Tests passing — ready for review.`
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Phase 7 — Review
|
|
78
|
+
|
|
79
|
+
**Only invoke the Review Agent after all of the above gates have passed.** The reviewer must receive: passing validation, passing Lighthouse (desktop + mobile), and passing tests. Never hand off code that has not cleared every gate.
|
|
80
|
+
|
|
81
|
+
The Review Agent checks the code for correctness, security, accessibility, DRY violations, and adherence to the checklist. It returns a list of issues.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Phase 8 — Fix review issues
|
|
86
|
+
|
|
87
|
+
Fix every issue raised by the Review Agent. If the fixes touch the spec or view:
|
|
88
|
+
|
|
89
|
+
- Re-run `pulse_validate`
|
|
90
|
+
- Re-run Lighthouse if visual or structural changes were made
|
|
91
|
+
- Re-run tests
|
|
92
|
+
|
|
93
|
+
Only declare the task done after phase 8 is complete and all gates still pass.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Gate summary
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Phase 1 Understand (no gate — always run)
|
|
101
|
+
Phase 2 Plan gate: user confirmation
|
|
102
|
+
Phase 3 Build (no gate — write the code)
|
|
103
|
+
Phase 4 Validate gate: pulse_validate clean
|
|
104
|
+
Phase 5 Browser gate: Lighthouse 100/100/100 (Accessibility/Best Practices/SEO) desktop + mobile
|
|
105
|
+
Phase 6 Tests gate: all tests pass
|
|
106
|
+
Phase 7 Review Agent (only reached after phases 4–6 all pass)
|
|
107
|
+
Phase 8 Fix + re-verify gate: all gates still pass
|
|
108
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — CLI module smoke tests
|
|
3
|
+
*
|
|
4
|
+
* Imports every CLI module to catch syntax errors and bad exports.
|
|
5
|
+
* These modules aren't exercised by unit tests but are loaded at runtime
|
|
6
|
+
* by the dev server and `pulse new` — a parse error breaks both.
|
|
7
|
+
*
|
|
8
|
+
* run: node src/cli/cli.test.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let passed = 0
|
|
12
|
+
let failed = 0
|
|
13
|
+
|
|
14
|
+
function test(label, fn) {
|
|
15
|
+
try {
|
|
16
|
+
fn()
|
|
17
|
+
console.log(` ✓ ${label}`)
|
|
18
|
+
passed++
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.log(` ✗ ${label}`)
|
|
21
|
+
console.log(` ${e.message}`)
|
|
22
|
+
failed++
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
console.log('\nCLI module imports\n')
|
|
29
|
+
|
|
30
|
+
const modules = await Promise.allSettled([
|
|
31
|
+
import('./scaffold.js'),
|
|
32
|
+
import('./discover.js'),
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
const names = ['scaffold.js', 'discover.js']
|
|
36
|
+
|
|
37
|
+
modules.forEach((result, i) => {
|
|
38
|
+
test(`${names[i]} parses and imports without error`, () => {
|
|
39
|
+
if (result.status === 'rejected') {
|
|
40
|
+
throw new Error(result.reason?.message || String(result.reason))
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('scaffold exports a scaffold function', () => {
|
|
46
|
+
const mod = modules[0].value
|
|
47
|
+
if (typeof mod?.scaffold !== 'function') {
|
|
48
|
+
throw new Error(`Expected scaffold export to be a function, got ${typeof mod?.scaffold}`)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('discover exports a loadPages function', () => {
|
|
53
|
+
const mod = modules[1].value
|
|
54
|
+
if (typeof mod?.loadPages !== 'function') {
|
|
55
|
+
throw new Error(`Expected loadPages export to be a function, got ${typeof mod?.loadPages}`)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('discover: discoverPages skips .test.js files', () => {
|
|
60
|
+
const { discoverPages } = modules[1].value
|
|
61
|
+
// discoverPages on this very directory must not include cli.test.js
|
|
62
|
+
const pages = discoverPages(new URL('../../', import.meta.url).pathname)
|
|
63
|
+
const testFiles = pages.filter(p => p.filePath.endsWith('.test.js'))
|
|
64
|
+
if (testFiles.length > 0) {
|
|
65
|
+
throw new Error(`discoverPages included test files: ${testFiles.map(p => p.filePath).join(', ')}`)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('discover: nested pages get unique names (collision prevention)', () => {
|
|
70
|
+
const { deriveRoute } = modules[1].value
|
|
71
|
+
// Two files with the same basename in different subdirs must derive different routes
|
|
72
|
+
const r1 = deriveRoute('products.js')
|
|
73
|
+
const r2 = deriveRoute('api/products.js')
|
|
74
|
+
if (r1 === r2) {
|
|
75
|
+
throw new Error(`Route collision: both products.js and api/products.js derived '${r1}'`)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
|
|
82
|
+
if (failed > 0) process.exit(1)
|
package/src/cli/dev.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Dev server
|
|
3
|
+
*
|
|
4
|
+
* Auto-discovers pages from src/pages/, serves source files directly
|
|
5
|
+
* (no bundling in dev), starts the HTTP server with hot-ish reloading.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node src/cli/dev.js [--root /path/to/project] [--port 3000]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from 'path'
|
|
12
|
+
import fs from 'fs'
|
|
13
|
+
import { createServer } from '../server/index.js'
|
|
14
|
+
import { loadPages } from './discover.js'
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Args
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2)
|
|
21
|
+
const rootArg = args.indexOf('--root')
|
|
22
|
+
const portArg = args.indexOf('--port')
|
|
23
|
+
|
|
24
|
+
const ROOT = rootArg !== -1
|
|
25
|
+
? path.resolve(args[rootArg + 1])
|
|
26
|
+
: process.cwd()
|
|
27
|
+
|
|
28
|
+
async function resolvePort() {
|
|
29
|
+
if (portArg !== -1) return parseInt(args[portArg + 1], 10)
|
|
30
|
+
const configPath = path.join(ROOT, 'pulse.config.js')
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const mod = await import(configPath)
|
|
34
|
+
if (mod.default?.port) return mod.default.port
|
|
35
|
+
} catch { /* fall through */ }
|
|
36
|
+
}
|
|
37
|
+
return 3000
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PORT = await resolvePort()
|
|
41
|
+
|
|
42
|
+
const FRAMEWORK_ROOT = new URL('../../', import.meta.url).pathname
|
|
43
|
+
const PUBLIC_DIR = path.join(ROOT, 'public')
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Sync pulse-ui assets from the installed package → project public/
|
|
47
|
+
// Runs on every dev start so the project always has the latest CSS and JS.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
;(function syncAssets() {
|
|
51
|
+
const pkgPublic = new URL('../../public', import.meta.url).pathname
|
|
52
|
+
const pkgJson = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url).pathname, 'utf8'))
|
|
53
|
+
const pkgVersion = pkgJson.version
|
|
54
|
+
const stampPath = path.join(PUBLIC_DIR, '.pulse-ui-version')
|
|
55
|
+
const stamp = fs.existsSync(stampPath) ? fs.readFileSync(stampPath, 'utf8').trim() : null
|
|
56
|
+
|
|
57
|
+
if (stamp === pkgVersion) return
|
|
58
|
+
|
|
59
|
+
fs.mkdirSync(PUBLIC_DIR, { recursive: true })
|
|
60
|
+
for (const asset of ['pulse-ui.css', 'pulse-ui.js']) {
|
|
61
|
+
const src = path.join(pkgPublic, asset)
|
|
62
|
+
const dst = path.join(PUBLIC_DIR, asset)
|
|
63
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, dst)
|
|
64
|
+
}
|
|
65
|
+
fs.writeFileSync(stampPath, pkgVersion, 'utf8')
|
|
66
|
+
|
|
67
|
+
// Sync agent checklist into .claude/ so CLAUDE.md can import it
|
|
68
|
+
const checklistSrc = new URL('../agent/checklist.md', import.meta.url).pathname
|
|
69
|
+
const checklistDst = path.join(ROOT, '.claude', 'pulse-checklist.md')
|
|
70
|
+
if (fs.existsSync(checklistSrc)) {
|
|
71
|
+
fs.mkdirSync(path.dirname(checklistDst), { recursive: true })
|
|
72
|
+
fs.copyFileSync(checklistSrc, checklistDst)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(` ✓ pulse-ui assets synced (v${pkgVersion})\n`)
|
|
76
|
+
})()
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Start
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
console.log(`⚡ Pulse dev server starting...\n`)
|
|
83
|
+
|
|
84
|
+
const specs = await loadPages(ROOT)
|
|
85
|
+
|
|
86
|
+
if (specs.length === 0) {
|
|
87
|
+
console.error('No pages found in src/pages/. Create a page to get started.')
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('Pages:\n')
|
|
92
|
+
specs.forEach(spec => console.log(` ${spec.route}`))
|
|
93
|
+
console.log()
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Hot reload — SSE clients + file watcher
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
const reloadClients = new Set()
|
|
100
|
+
|
|
101
|
+
function notifyReload() {
|
|
102
|
+
for (const res of reloadClients) {
|
|
103
|
+
res.write('event: reload\ndata: {}\n\n')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Close SSE connections on shutdown so the server can drain and exit cleanly.
|
|
108
|
+
// Without this, active SSE sockets are never destroyed and the process force-exits
|
|
109
|
+
// after the 30s shutdown timeout.
|
|
110
|
+
function closeReloadClients() {
|
|
111
|
+
for (const res of [...reloadClients]) {
|
|
112
|
+
try { res.end() } catch {}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
process.on('SIGTERM', closeReloadClients)
|
|
116
|
+
process.on('SIGINT', closeReloadClients)
|
|
117
|
+
|
|
118
|
+
let reloadTimer = null
|
|
119
|
+
fs.watch(path.join(ROOT, 'src'), { recursive: true }, () => {
|
|
120
|
+
clearTimeout(reloadTimer)
|
|
121
|
+
reloadTimer = setTimeout(async () => {
|
|
122
|
+
try {
|
|
123
|
+
const fresh = await loadPages(ROOT, Date.now())
|
|
124
|
+
updateSpecs(fresh)
|
|
125
|
+
} catch { /* spec error — browser will show the old page, not crash */ }
|
|
126
|
+
notifyReload()
|
|
127
|
+
}, 50)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Tiny script injected into every page — connects to SSE and reloads on change
|
|
131
|
+
// Passed as a function so the server can inject the per-request CSP nonce
|
|
132
|
+
const reloadScript = (nonce) => `<script nonce="${nonce}">
|
|
133
|
+
(function() {
|
|
134
|
+
var open = false;
|
|
135
|
+
var es = new EventSource('/_pulse/reload');
|
|
136
|
+
es.addEventListener('open', function() {
|
|
137
|
+
if (open) { location.reload(); return; }
|
|
138
|
+
open = true;
|
|
139
|
+
});
|
|
140
|
+
es.addEventListener('reload', function() { location.reload(); });
|
|
141
|
+
})();
|
|
142
|
+
</script>`
|
|
143
|
+
|
|
144
|
+
const { updateSpecs } = createServer(specs, {
|
|
145
|
+
port: PORT,
|
|
146
|
+
stream: true,
|
|
147
|
+
staticDir: fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : null,
|
|
148
|
+
manifest: {}, // never use a build manifest in dev — always serve source files
|
|
149
|
+
extraBody: reloadScript,
|
|
150
|
+
dev: true,
|
|
151
|
+
|
|
152
|
+
onRequest(req, res) {
|
|
153
|
+
const url = req.url.split('?')[0]
|
|
154
|
+
|
|
155
|
+
// SSE endpoint — browser connects here to receive reload events
|
|
156
|
+
if (url === '/_pulse/reload') {
|
|
157
|
+
res.writeHead(200, {
|
|
158
|
+
'Content-Type': 'text/event-stream',
|
|
159
|
+
'Cache-Control': 'no-cache',
|
|
160
|
+
'Connection': 'keep-alive',
|
|
161
|
+
})
|
|
162
|
+
res.write('retry: 1000\n\n')
|
|
163
|
+
reloadClients.add(res)
|
|
164
|
+
req.on('close', () => reloadClients.delete(res))
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const serveFile = (filePath) => {
|
|
169
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return false
|
|
170
|
+
res.writeHead(200, {
|
|
171
|
+
'Content-Type': 'application/javascript',
|
|
172
|
+
'Cache-Control': 'no-store',
|
|
173
|
+
})
|
|
174
|
+
fs.createReadStream(filePath).pipe(res)
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const serveDir = (urlPrefix, dir) => {
|
|
179
|
+
if (!url.startsWith(urlPrefix)) return false
|
|
180
|
+
const rel = url.slice(urlPrefix.length)
|
|
181
|
+
const filePath = path.resolve(dir, rel)
|
|
182
|
+
if (!filePath.startsWith(path.resolve(dir))) return false // traversal guard
|
|
183
|
+
return serveFile(filePath)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Project source — /src/pages/, /src/components/, /src/lib/, etc.
|
|
187
|
+
// Falls back to framework source for imports like /src/ui/ that sub-projects
|
|
188
|
+
// resolve from relative paths (e.g. docs component pages → ../../../../src/ui/).
|
|
189
|
+
if (serveDir('/src/', path.join(ROOT, 'src'))) return false
|
|
190
|
+
if (serveDir('/src/', path.join(FRAMEWORK_ROOT, 'src'))) return false
|
|
191
|
+
|
|
192
|
+
// Framework runtime — /@pulse/runtime/index.js → FRAMEWORK_ROOT/src/runtime/index.js
|
|
193
|
+
if (serveDir('/@pulse/', path.join(FRAMEWORK_ROOT, 'src'))) return false
|
|
194
|
+
}
|
|
195
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Page discovery
|
|
3
|
+
*
|
|
4
|
+
* Scans src/pages/ and derives routes from filenames.
|
|
5
|
+
* Convention:
|
|
6
|
+
* home.js → /
|
|
7
|
+
* about.js → /about
|
|
8
|
+
* products.js → /products
|
|
9
|
+
* blog/index.js → /blog
|
|
10
|
+
* blog/post.js → /blog/post
|
|
11
|
+
*
|
|
12
|
+
* The spec's route property overrides the filename-derived route.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs'
|
|
16
|
+
import path from 'path'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Derive a route from a file path relative to the pages directory.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} relPath - e.g. 'home.js', 'blog/post.js'
|
|
22
|
+
* @returns {string} - e.g. '/', '/blog/post'
|
|
23
|
+
*/
|
|
24
|
+
export function deriveRoute(relPath) {
|
|
25
|
+
const withoutExt = relPath.replace(/\.js$/, '')
|
|
26
|
+
const parts = withoutExt.split(path.sep)
|
|
27
|
+
|
|
28
|
+
// index.js or home.js at any level maps to the parent route
|
|
29
|
+
const last = parts[parts.length - 1]
|
|
30
|
+
if (last === 'index' || last === 'home') parts.pop()
|
|
31
|
+
|
|
32
|
+
if (parts.length === 0) return '/'
|
|
33
|
+
return '/' + parts.join('/')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Recursively find all .js files under a directory.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} dir
|
|
40
|
+
* @param {string} [base] - used internally for recursion
|
|
41
|
+
* @returns {string[]} - paths relative to dir
|
|
42
|
+
*/
|
|
43
|
+
function findFiles(dir, base = dir) {
|
|
44
|
+
if (!fs.existsSync(dir)) return []
|
|
45
|
+
|
|
46
|
+
return fs.readdirSync(dir).flatMap(entry => {
|
|
47
|
+
const full = path.join(dir, entry)
|
|
48
|
+
const rel = path.relative(base, full)
|
|
49
|
+
if (fs.statSync(full).isDirectory()) return findFiles(full, base)
|
|
50
|
+
if (entry.endsWith('.js') && !entry.endsWith('.test.js')) return [rel]
|
|
51
|
+
return []
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Discover all pages in the project's src/pages directory.
|
|
57
|
+
* Returns an array of { filePath, derivedRoute } objects.
|
|
58
|
+
* The caller is responsible for importing the specs.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} projectRoot
|
|
61
|
+
* @returns {{ filePath: string, derivedRoute: string }[]}
|
|
62
|
+
*/
|
|
63
|
+
export function discoverPages(projectRoot) {
|
|
64
|
+
const pagesDir = path.join(projectRoot, 'src', 'pages')
|
|
65
|
+
const files = findFiles(pagesDir)
|
|
66
|
+
|
|
67
|
+
return files.map(relPath => ({
|
|
68
|
+
filePath: path.join(pagesDir, relPath),
|
|
69
|
+
derivedRoute: deriveRoute(relPath),
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Load all page specs from src/pages/, applying derived routes
|
|
75
|
+
* where the spec doesn't declare its own.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} projectRoot
|
|
78
|
+
* @returns {Promise<import('../spec/schema.js').PulseSpec[]>}
|
|
79
|
+
*/
|
|
80
|
+
export async function loadPages(projectRoot, bust = 0) {
|
|
81
|
+
const pages = discoverPages(projectRoot)
|
|
82
|
+
|
|
83
|
+
const specs = await Promise.all(
|
|
84
|
+
pages.map(async ({ filePath, derivedRoute }) => {
|
|
85
|
+
const url = bust ? `${filePath}?t=${bust}` : filePath
|
|
86
|
+
const mod = await import(url)
|
|
87
|
+
const spec = mod.default
|
|
88
|
+
|
|
89
|
+
if (!spec || typeof spec !== 'object') {
|
|
90
|
+
throw new Error(`Page file must export a default spec object: ${filePath}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Auto-set hydrate only for pages that need client-side JS.
|
|
94
|
+
// Purely SSR pages (no mutations, actions, persist) ship zero JS.
|
|
95
|
+
// spec.hydrate wins if explicitly provided.
|
|
96
|
+
const needsHydration = spec.hydrate || spec.mutations || spec.actions || spec.persist
|
|
97
|
+
const hydrateUrl = needsHydration
|
|
98
|
+
? spec.hydrate || ('/src/pages/' + path.relative(
|
|
99
|
+
path.join(projectRoot, 'src', 'pages'),
|
|
100
|
+
filePath
|
|
101
|
+
))
|
|
102
|
+
: null
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
...spec,
|
|
106
|
+
route: spec.route || derivedRoute,
|
|
107
|
+
...(hydrateUrl ? { hydrate: hydrateUrl } : {}),
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return specs
|
|
113
|
+
}
|