@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,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Project scaffolding
|
|
3
|
+
*
|
|
4
|
+
* Creates a minimal Pulse project in the target directory.
|
|
5
|
+
* Includes a working home page with a counter to prove the app runs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs'
|
|
9
|
+
import path from 'path'
|
|
10
|
+
import { execSync } from 'child_process'
|
|
11
|
+
|
|
12
|
+
const PULSE_PKG = '@invisibleloop/pulse'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scaffold a new Pulse project.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} targetDir - Absolute path to the project directory
|
|
18
|
+
* @param {Object} options
|
|
19
|
+
* @param {string} [options.name] - Project name (defaults to directory name)
|
|
20
|
+
* @param {number} [options.port] - Dev server port (defaults to 3000)
|
|
21
|
+
*/
|
|
22
|
+
export async function scaffold(targetDir, options = {}) {
|
|
23
|
+
const name = options.name || path.basename(targetDir)
|
|
24
|
+
const port = options.port || 3000
|
|
25
|
+
|
|
26
|
+
fs.mkdirSync(path.join(targetDir, 'src', 'pages'), { recursive: true })
|
|
27
|
+
fs.mkdirSync(path.join(targetDir, 'src', 'components'), { recursive: true })
|
|
28
|
+
fs.mkdirSync(path.join(targetDir, 'public'), { recursive: true })
|
|
29
|
+
|
|
30
|
+
// package.json
|
|
31
|
+
write(targetDir, 'package.json', JSON.stringify({
|
|
32
|
+
name,
|
|
33
|
+
version: '0.1.0',
|
|
34
|
+
type: 'module',
|
|
35
|
+
scripts: {
|
|
36
|
+
dev: 'pulse dev',
|
|
37
|
+
build: 'pulse build',
|
|
38
|
+
start: 'pulse start',
|
|
39
|
+
test: 'find src -name "*.test.js" | xargs node --test',
|
|
40
|
+
'test:coverage': 'find src -name "*.test.js" | xargs node --test --experimental-test-coverage --test-coverage-include=\'src/pages/!(*.test).js\'',
|
|
41
|
+
},
|
|
42
|
+
engines: {
|
|
43
|
+
node: '>=22',
|
|
44
|
+
},
|
|
45
|
+
dependencies: {
|
|
46
|
+
[PULSE_PKG]: 'latest',
|
|
47
|
+
}
|
|
48
|
+
}, null, 2))
|
|
49
|
+
|
|
50
|
+
// pulse.config.js
|
|
51
|
+
write(targetDir, 'pulse.config.js',
|
|
52
|
+
`export default {
|
|
53
|
+
${port !== 3000 ? ` port: ${port},\n` : ''}}
|
|
54
|
+
`)
|
|
55
|
+
|
|
56
|
+
// Home page — working counter proves the app runs
|
|
57
|
+
write(targetDir, 'src/pages/home.js', homePage(name))
|
|
58
|
+
|
|
59
|
+
// Minimal stylesheet
|
|
60
|
+
write(targetDir, 'public/app.css', baseCSS())
|
|
61
|
+
|
|
62
|
+
// Copy pulse-ui assets from the package into public/ so they're served immediately
|
|
63
|
+
const pkgPublic = new URL('../../public', import.meta.url).pathname
|
|
64
|
+
for (const asset of ['pulse-ui.css', 'pulse-ui.js']) {
|
|
65
|
+
const src = path.join(pkgPublic, asset)
|
|
66
|
+
const dst = path.join(targetDir, 'public', asset)
|
|
67
|
+
if (fs.existsSync(src)) fs.copyFileSync(src, dst)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Copy agent support files into .claude/
|
|
71
|
+
const agentFiles = [
|
|
72
|
+
['../agent/checklist.md', 'pulse-checklist.md'],
|
|
73
|
+
['../agent/coverage-check.js', 'coverage-check.js'],
|
|
74
|
+
]
|
|
75
|
+
for (const [src, dst] of agentFiles) {
|
|
76
|
+
const srcPath = new URL(src, import.meta.url).pathname
|
|
77
|
+
const dstPath = path.join(targetDir, '.claude', dst)
|
|
78
|
+
if (fs.existsSync(srcPath)) {
|
|
79
|
+
fs.mkdirSync(path.dirname(dstPath), { recursive: true })
|
|
80
|
+
fs.copyFileSync(srcPath, dstPath)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// CLAUDE.md — in .claude/ so it's alongside Claude Code's own config, not cluttering the project root
|
|
85
|
+
write(targetDir, '.claude/CLAUDE.md', claudeMd(name))
|
|
86
|
+
|
|
87
|
+
// settings.json — hooks that enforce correct agent behaviour
|
|
88
|
+
write(targetDir, '.claude/settings.json', JSON.stringify({
|
|
89
|
+
hooks: {
|
|
90
|
+
SessionStart: [
|
|
91
|
+
{
|
|
92
|
+
hooks: [
|
|
93
|
+
{
|
|
94
|
+
type: 'command',
|
|
95
|
+
command: `node -e "process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:'START OF SESSION: Before doing anything else — (1) run pulse_list_structure to see the current project structure, (2) read pulse://guide from MCP for the complete Pulse reference. Both are mandatory. Do not skip them.'}}))"`,
|
|
96
|
+
statusMessage: 'Loading Pulse session...',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'command',
|
|
100
|
+
command: `node -e "const{execSync}=require('child_process');let out='';let ok=true;try{out=execSync('npm test 2>&1',{encoding:'utf8',timeout:60000});}catch(e){ok=false;out=e.stdout||e.message;}const lines=out.trim().split('\\n');const summary=lines[lines.length-1]||'';const msg=ok?'TEST BASELINE — all tests passing: '+summary+'. Note this result. If tests fail after your changes, you introduced the regression.':'PRE-EXISTING TEST FAILURES detected before you wrote any code:\\n'+out.slice(-2000)+'\\nDo not treat these as regressions you caused. Fix them first if they are related to your task, otherwise note them and proceed.';process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'SessionStart',additionalContext:msg}}))"`,
|
|
101
|
+
statusMessage: 'Running test baseline...',
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
PreToolUse: [
|
|
107
|
+
{
|
|
108
|
+
matcher: 'mcp__chrome-devtools__lighthouse_audit',
|
|
109
|
+
hooks: [
|
|
110
|
+
{
|
|
111
|
+
type: 'command',
|
|
112
|
+
command: `node -e "process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PreToolUse',additionalContext:'LIGHTHOUSE PRE-FLIGHT: This tool audits the currently loaded browser page. Before proceeding you MUST: (1) have called pulse_build to start the production server on port ${port + 1}, (2) have called navigate_page with url http://localhost:${port + 1}/ so the current page IS the production server. If either step is not done, stop — do those steps first, then call lighthouse_audit.'}}))"`,
|
|
113
|
+
statusMessage: 'Checking Lighthouse prerequisites...',
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
matcher: 'Bash',
|
|
119
|
+
hooks: [
|
|
120
|
+
{
|
|
121
|
+
type: 'command',
|
|
122
|
+
command: `node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const cmd=(i.tool_input||{}).command||'';if(!cmd.match(/\\bnpm\\s+(install|i)\\b|\\byarn\\s+add\\b|\\bpnpm\\s+(install|add)\\b|\\bbun\\s+add\\b/)){return;}const skipWords=new Set(['npm','yarn','pnpm','bun','install','i','add','ci']);const pkgs=cmd.split(/\\s+/).filter(p=>p&&!p.startsWith('-')&&!skipWords.has(p));if(!pkgs.length){return;}const BLOCK=['react','react-dom','react-router','react-router-dom','preact','vue','svelte','solid-js','alpinejs','htmx.org','jquery'];const BLOCK_SCOPE=['@vue/','@angular/','@sveltejs/'];const getName=p=>p.startsWith('@')?p:p.split('@')[0];const blocked=pkgs.filter(p=>{const n=getName(p);return BLOCK.includes(n)||BLOCK_SCOPE.some(s=>p.startsWith(s));});if(blocked.length){process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'deny',permissionDecisionReason:'BLOCKED: '+blocked.join(', ')+' is a client-side rendering library. Pulse handles all client rendering — do not install client-side UI frameworks. Use Pulse specs and components instead.'}}));}else{process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PreToolUse',additionalContext:'PACKAGE INSTALL: Confirm '+pkgs.join(', ')+' will only be used server-side (in server.data fetchers or server utilities). Do not import it in view functions or client-accessible code.'}}));}});"`,
|
|
123
|
+
statusMessage: 'Checking package install...',
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
PostToolUse: [
|
|
129
|
+
{
|
|
130
|
+
matcher: 'Write|Edit',
|
|
131
|
+
hooks: [
|
|
132
|
+
{
|
|
133
|
+
type: 'command',
|
|
134
|
+
command: `node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const f=(i.tool_input||{}).file_path||'';if(!f.includes('/src/pages/')||!f.endsWith('.js')){return;}const {execSync}=require('child_process');try{execSync('node --check '+JSON.stringify(f),{stdio:'pipe'});process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PostToolUse',additionalContext:'Syntax OK: '+f}}));}catch(e){process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PostToolUse',additionalContext:'SYNTAX ERROR in '+f+': '+e.stderr.toString().trim()}}));}})"`,
|
|
135
|
+
statusMessage: 'Checking page syntax...',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: 'command',
|
|
139
|
+
command: `node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const f=(i.tool_input||{}).file_path||'';if(!f.endsWith('.css')){return;}const fs=require('fs');let c;try{c=fs.readFileSync(f,'utf8');}catch(e){return;}const m=c.match(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g);if(m&&m.length){const u=[...new Set(m)];process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PostToolUse',additionalContext:'HARDCODED HEX in '+f+': '+u.join(', ')+'. Use var(--ui-*) tokens instead — hardcoded colours break theming.'}}));}})"`,
|
|
140
|
+
statusMessage: 'Checking for hardcoded colours...',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'command',
|
|
144
|
+
command: `node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const f=(i.tool_input||{}).file_path||'';if((!f.includes('/src/pages/')&&!f.includes('/src/components/'))||!f.endsWith('.js')){return;}const fs=require('fs');let c;try{c=fs.readFileSync(f,'utf8');}catch(e){return;}const m=c.match(/[\\u{1F000}-\\u{1FFFF}\\u{2600}-\\u{27BF}\\u{1F300}-\\u{1F9FF}\\u{2300}-\\u{23FF}\\u{2B00}-\\u{2BFF}]/gu);if(m&&m.length){const u=[...new Set(m)];process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:'PostToolUse',additionalContext:'EMOJI in '+f+': '+u.join(' ')+'. Replace with icons from the icon library — e.g. iconCheck(), iconStar(), iconZap(). Never use emoji in UI output.'}}));}})"`,
|
|
145
|
+
statusMessage: 'Checking for emoji...',
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
Stop: [
|
|
151
|
+
{
|
|
152
|
+
hooks: [
|
|
153
|
+
{
|
|
154
|
+
type: 'command',
|
|
155
|
+
command: `node -e "const fs=require('fs');const{execSync}=require('child_process');let changed=new Set();let hasGit=false;try{execSync('git rev-parse --is-inside-work-tree',{stdio:'pipe'});hasGit=true;execSync('git status --porcelain',{encoding:'utf8'}).split('\\n').forEach(l=>{const f=l.slice(3).trim().split(' -> ').pop();if(f)changed.add(f);});}catch{}if(!hasGit&&fs.existsSync('src/pages')){for(const f of fs.readdirSync('src/pages')){if(f.endsWith('.js')&&!f.endsWith('.test.js'))changed.add('src/pages/'+f);}}const specs=[...changed].filter(f=>f.match(/^src\\/pages\\/.+\\.js$/)&&!f.endsWith('.test.js'));const missing=specs.filter(f=>{const t=f.replace(/\\.js$/,'.test.js');return!fs.existsSync(t)&&!changed.has(t);});if(missing.length){process.stdout.write(JSON.stringify({decision:'block',reason:'TESTS MISSING: '+missing.join(', ')+' — you must write tests before finishing. Use renderSync/render from @invisibleloop/pulse/testing to test view output, mutations, and any extracted logic functions. Run the tests with node to confirm they pass.'}));}"`,
|
|
156
|
+
statusMessage: 'Checking for missing tests...',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: 'command',
|
|
160
|
+
command: `node .claude/coverage-check.js`,
|
|
161
|
+
statusMessage: 'Checking test coverage...',
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
}, null, 2))
|
|
168
|
+
|
|
169
|
+
// .gitignore
|
|
170
|
+
write(targetDir, '.gitignore', [
|
|
171
|
+
'node_modules',
|
|
172
|
+
'public/dist',
|
|
173
|
+
'.pulse-build',
|
|
174
|
+
'.DS_Store',
|
|
175
|
+
].join('\n') + '\n')
|
|
176
|
+
|
|
177
|
+
console.log(' ✓ Project files created')
|
|
178
|
+
|
|
179
|
+
// Install dependencies
|
|
180
|
+
console.log(' ✓ Installing dependencies...\n')
|
|
181
|
+
try {
|
|
182
|
+
// Use the globally linked package if available (local dev), otherwise npm install
|
|
183
|
+
execSync(`npm link ${PULSE_PKG}`, { cwd: targetDir, stdio: 'inherit' })
|
|
184
|
+
} catch {
|
|
185
|
+
execSync('npm install', { cwd: targetDir, stdio: 'inherit' })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// File templates
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
function homePage(appName) {
|
|
194
|
+
return `\
|
|
195
|
+
import { button, heading, iconMinus, iconPlus } from '@invisibleloop/pulse/ui'
|
|
196
|
+
|
|
197
|
+
export default {
|
|
198
|
+
route: '/',
|
|
199
|
+
|
|
200
|
+
hydrate: '/src/pages/home.js',
|
|
201
|
+
|
|
202
|
+
meta: {
|
|
203
|
+
title: '${appName}',
|
|
204
|
+
description: '${appName} — built with Pulse.',
|
|
205
|
+
styles: ['/pulse-ui.css', '/app.css'],
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
state: {
|
|
209
|
+
count: 0,
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
constraints: {
|
|
213
|
+
count: { min: 0, max: 10 },
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
view: (state) => \`
|
|
217
|
+
<main id="main-content" class="page">
|
|
218
|
+
\${heading({ level: 1, text: '${appName}' })}
|
|
219
|
+
<p class="u-text-muted u-mt-2 u-mb-8">Your Pulse app is running.</p>
|
|
220
|
+
|
|
221
|
+
<div class="u-flex u-items-center u-gap-4">
|
|
222
|
+
\${button({
|
|
223
|
+
icon: iconMinus({ size: 16 }),
|
|
224
|
+
variant: 'secondary',
|
|
225
|
+
disabled: state.count <= 0,
|
|
226
|
+
attrs: { 'data-event': 'decrement', 'aria-label': 'Decrease count' },
|
|
227
|
+
})}
|
|
228
|
+
<span class="u-text-2xl u-font-bold u-tabular-nums" aria-live="polite">\${state.count}</span>
|
|
229
|
+
\${button({
|
|
230
|
+
icon: iconPlus({ size: 16 }),
|
|
231
|
+
variant: 'secondary',
|
|
232
|
+
disabled: state.count >= 10,
|
|
233
|
+
attrs: { 'data-event': 'increment', 'aria-label': 'Increase count' },
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
</main>
|
|
237
|
+
\`,
|
|
238
|
+
|
|
239
|
+
mutations: {
|
|
240
|
+
increment: (state) => ({ count: state.count + 1 }),
|
|
241
|
+
decrement: (state) => ({ count: state.count - 1 }),
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function baseCSS() {
|
|
248
|
+
return `\
|
|
249
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
250
|
+
|
|
251
|
+
body {
|
|
252
|
+
font-family: var(--ui-font, system-ui, sans-serif);
|
|
253
|
+
line-height: 1.6;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.page {
|
|
257
|
+
max-width: 640px;
|
|
258
|
+
margin: 0 auto;
|
|
259
|
+
padding: 3rem 1.5rem;
|
|
260
|
+
}
|
|
261
|
+
`
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function claudeMd(appName) {
|
|
265
|
+
return `\
|
|
266
|
+
# ${appName} — Pulse App
|
|
267
|
+
|
|
268
|
+
## Project
|
|
269
|
+
|
|
270
|
+
\`\`\`
|
|
271
|
+
src/pages/ ← one .js file per page, auto-discovered
|
|
272
|
+
src/components/ ← shared view fragments (JS functions returning HTML strings)
|
|
273
|
+
public/app.css ← global stylesheet
|
|
274
|
+
\`\`\`
|
|
275
|
+
|
|
276
|
+
## Start of every session
|
|
277
|
+
|
|
278
|
+
1. Run \`pulse_list_structure\` to see what already exists
|
|
279
|
+
2. Read \`pulse://guide\` from MCP — the complete reference for spec format, components, verification workflow, CSS rules, and patterns
|
|
280
|
+
|
|
281
|
+
The MCP guide is the single source of truth. Follow it for all technical decisions, component usage, and the mandatory verification workflow.
|
|
282
|
+
|
|
283
|
+
## After completing any feature
|
|
284
|
+
|
|
285
|
+
Run these steps in order — do not declare work done without them:
|
|
286
|
+
|
|
287
|
+
1. \`pulse_validate\` — fix all errors and warnings
|
|
288
|
+
2. \`pulse_review\` — switch into reviewer mode, read the source and rendered HTML critically, fix every issue before continuing
|
|
289
|
+
3. Write tests for every page you created or changed, run \`npm test\` (all pass), then \`npm run test:coverage\` — fix any untested branches
|
|
290
|
+
4. Navigate to the page in the browser and take a screenshot
|
|
291
|
+
5. Run Lighthouse (desktop then mobile) — all four scores must be 100
|
|
292
|
+
|
|
293
|
+
## Writing tests
|
|
294
|
+
|
|
295
|
+
Test files live next to the page they test: \`src/pages/foo.test.js\` for \`src/pages/foo.js\`.
|
|
296
|
+
|
|
297
|
+
\`\`\`
|
|
298
|
+
npm test # run all tests
|
|
299
|
+
npm run test:coverage # run tests + show branch/line coverage for src/pages/
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
Coverage target: **100% branch coverage on every view function**. The coverage report shows uncovered lines — add tests until every branch is exercised. Server fetcher functions (which hit real APIs) are exempt.
|
|
303
|
+
|
|
304
|
+
What to test for every page:
|
|
305
|
+
- View with real/populated server data (success path)
|
|
306
|
+
- View with \`null\` server data per fetcher (each fetcher can fail independently)
|
|
307
|
+
- View with empty arrays/collections (empty state vs populated state)
|
|
308
|
+
- \`onViewError\` fallback — call it directly: \`spec.onViewError(new Error('x'), {}, {})\`
|
|
309
|
+
- Every mutation — pure functions, test directly
|
|
310
|
+
- Every exported pure function (formatters, validators, etc.)
|
|
311
|
+
- XSS: pass \`'<script>alert(1)</script>'\` as user-controlled strings, assert \`!r.has('script')\`
|
|
312
|
+
|
|
313
|
+
\`\`\`js
|
|
314
|
+
import assert from 'node:assert/strict'
|
|
315
|
+
import { renderSync } from '@invisibleloop/pulse/testing'
|
|
316
|
+
import spec from './my-page.js'
|
|
317
|
+
|
|
318
|
+
// View — pass mock state and server data
|
|
319
|
+
const r = renderSync(spec, { state: { count: 5 }, server: { items: [] } })
|
|
320
|
+
assert(r.has('main#main-content')) // element exists
|
|
321
|
+
assert.equal(r.get('h1').text, 'Title') // text content (throws if not found)
|
|
322
|
+
assert(r.has('button[disabled]')) // attribute present
|
|
323
|
+
assert(!r.has('.ui-badge')) // element absent
|
|
324
|
+
assert.equal(r.count('li'), 3) // count elements
|
|
325
|
+
|
|
326
|
+
// Mutations — pure functions, test directly
|
|
327
|
+
assert.deepEqual(spec.mutations.increment({ count: 0 }), { count: 1 })
|
|
328
|
+
|
|
329
|
+
// onViewError
|
|
330
|
+
const fallback = spec.onViewError(new Error('boom'), {}, {})
|
|
331
|
+
assert(fallback.includes('main-content'))
|
|
332
|
+
\`\`\`
|
|
333
|
+
|
|
334
|
+
**Selector support:** \`tag\`, \`.class\`, \`#id\`, \`[attr]\`, \`[attr="value"]\`, and combinations (\`button.primary[disabled]\`).
|
|
335
|
+
Descendant selectors (\`tbody tr\`, \`ul li\`) are NOT supported — use \`r.count('tr')\` not \`r.count('tbody tr')\`.
|
|
336
|
+
|
|
337
|
+
@.claude/pulse-checklist.md
|
|
338
|
+
`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Helpers
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
function write(dir, relPath, content) {
|
|
346
|
+
const filePath = path.join(dir, relPath)
|
|
347
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
348
|
+
fs.writeFileSync(filePath, content, 'utf8')
|
|
349
|
+
}
|
package/src/cli/start.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Production server
|
|
3
|
+
*
|
|
4
|
+
* Loads pages from src/pages/, serves public/dist/ bundles via manifest.
|
|
5
|
+
* No source file serving. No AI session. For production use.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node src/cli/start.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
|
+
const args = process.argv.slice(2)
|
|
17
|
+
const rootArg = args.indexOf('--root')
|
|
18
|
+
const portArg = args.indexOf('--port')
|
|
19
|
+
|
|
20
|
+
const ROOT = rootArg !== -1 ? path.resolve(args[rootArg + 1]) : process.cwd()
|
|
21
|
+
const PUBLIC_DIR = path.join(ROOT, 'public')
|
|
22
|
+
const DIST_DIR = path.join(PUBLIC_DIR, 'dist')
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Pre-flight checks
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(DIST_DIR)) {
|
|
29
|
+
console.error('\n⚠ No build found. Run "pulse build" first.\n')
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const manifestPath = path.join(DIST_DIR, 'manifest.json')
|
|
34
|
+
if (!fs.existsSync(manifestPath)) {
|
|
35
|
+
console.error('\n⚠ No manifest found in public/dist/. Run "pulse build" first.\n')
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Read port — CLI flag > pulse.config.js > default 3000
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
let port = portArg !== -1 ? parseInt(args[portArg + 1], 10) : null
|
|
44
|
+
let defaultCache = null
|
|
45
|
+
|
|
46
|
+
const configPath = path.join(ROOT, 'pulse.config.js')
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const mod = await import(configPath)
|
|
50
|
+
port = port || mod.default?.port || null
|
|
51
|
+
defaultCache = mod.default?.defaultCache ?? null
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Render (and other PaaS platforms) inject PORT — always takes precedence
|
|
56
|
+
port = parseInt(process.env.PORT, 10) || port || 3000
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Start
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
const specs = await loadPages(ROOT)
|
|
63
|
+
|
|
64
|
+
if (specs.length === 0) {
|
|
65
|
+
console.error('No pages found in src/pages/.')
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
createServer(specs, {
|
|
70
|
+
port,
|
|
71
|
+
stream: true,
|
|
72
|
+
staticDir: PUBLIC_DIR,
|
|
73
|
+
defaultCache,
|
|
74
|
+
})
|
package/src/html.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — HTML escaping helper
|
|
3
|
+
* Exported for use in view functions to safely embed user-supplied data.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Escape HTML special characters. Use this in view functions whenever
|
|
8
|
+
* rendering untrusted user data into HTML attributes or text content.
|
|
9
|
+
*
|
|
10
|
+
* @param {unknown} str
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function escHtml(str) {
|
|
14
|
+
return String(str)
|
|
15
|
+
.replace(/&/g, '&')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>')
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
}
|