@invisibleloop/pulse 0.1.23 → 0.1.29
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/settings.local.json +12 -1
- package/.github/workflows/publish.yml +11 -19
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/docs.css +19 -1
- package/docs/public/pulse-ui.css +1 -0
- package/docs/server.js +5 -2
- package/docs/src/lib/highlight.js +57 -13
- package/docs/src/lib/layout.js +5 -2
- package/docs/src/pages/faq.js +5 -2
- package/docs/src/pages/home.js +9 -5
- package/docs/src/pages/meta.js +21 -0
- package/docs/src/pages/routing.js +12 -1
- package/package.json +6 -2
- package/src/agent/guide-routing.md +20 -0
- package/src/agent/guide-spec.md +9 -1
- package/src/cli/scaffold.js +63 -2
- package/src/server/index.js +21 -6
- package/src/server/server.test.js +47 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +0 -115
- package/docs/public/dist/actions.boot-P66HKQEM.js +0 -164
- package/docs/public/dist/auth.boot-IMAJAUPH.js +0 -140
- package/docs/public/dist/caching.boot-DVR6KDE7.js +0 -53
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +0 -11
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +0 -6
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +0 -6
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +0 -5
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +0 -7
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +0 -7
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +0 -34
- package/docs/public/dist/components--button.boot-J54BQM2E.js +0 -23
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +0 -138
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +0 -12
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +0 -108
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +0 -54
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +0 -9
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +0 -20
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +0 -5
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +0 -11
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +0 -6
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +0 -7
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +0 -13
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +0 -19
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +0 -52
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +0 -14
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +0 -59
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +0 -44
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +0 -17
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +0 -68
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +0 -19
- package/docs/public/dist/components--input.boot-SGASZG5K.js +0 -7
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +0 -55
- package/docs/public/dist/components--media.boot-5VFIETZO.js +0 -13
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +0 -47
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +0 -33
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +0 -21
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +0 -30
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +0 -67
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +0 -22
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +0 -75
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +0 -38
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +0 -17
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +0 -12
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +0 -33
- package/docs/public/dist/components--select.boot-47X5RHOC.js +0 -10
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +0 -47
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +0 -22
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +0 -9
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +0 -9
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +0 -22
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +0 -11
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +0 -11
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +0 -4
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +0 -95
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +0 -29
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +0 -9
- package/docs/public/dist/components.boot-SE6PQ4P7.js +0 -103
- package/docs/public/dist/config.boot-DTRRWUE6.js +0 -126
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +0 -71
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +0 -163
- package/docs/public/dist/docs-8e3d4b5c.css +0 -1
- package/docs/public/dist/extending.boot-UA3CN243.js +0 -159
- package/docs/public/dist/faq.boot-6EQAWLQR.js +0 -43
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +0 -86
- package/docs/public/dist/guard.boot-AUHAWTG4.js +0 -80
- package/docs/public/dist/home.boot-BVQXRH32.js +0 -383
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +0 -104
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +0 -78
- package/docs/public/dist/images.boot-M6ZVKTZS.js +0 -80
- package/docs/public/dist/manifest.json +0 -94
- package/docs/public/dist/meta.boot-7NXGPHR4.js +0 -79
- package/docs/public/dist/mutations.boot-F6F43UDX.js +0 -79
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +0 -57
- package/docs/public/dist/performance.boot-C3UPCOBK.js +0 -98
- package/docs/public/dist/persist.boot-WT32PQOQ.js +0 -61
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +0 -63
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +0 -31
- package/docs/public/dist/pulse-ui-81a85c03.css +0 -1
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +0 -104
- package/docs/public/dist/routing.boot-FNX5FDGH.js +0 -70
- package/docs/public/dist/runtime-B73WLANC.js +0 -1
- package/docs/public/dist/runtime-KO4BHUQ3.js +0 -49
- package/docs/public/dist/runtime-L2HNXIHW.js +0 -59
- package/docs/public/dist/runtime-QFURDKA2.js +0 -5
- package/docs/public/dist/runtime-UVPXO4IR.js +0 -375
- package/docs/public/dist/runtime-VMJA3Z4N.js +0 -10
- package/docs/public/dist/runtime-ZJ4FXT5O.js +0 -11
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +0 -219
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +0 -157
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +0 -26
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +0 -159
- package/docs/public/dist/state.boot-B24GUE3R.js +0 -73
- package/docs/public/dist/store.boot-TLIB4XHH.js +0 -150
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +0 -80
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +0 -164
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +0 -303
- package/docs/public/dist/testing.boot-6U4WKMTE.js +0 -130
- package/docs/public/dist/validation.boot-PQHYGW5B.js +0 -100
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import{a as t}from"./runtime-QFURDKA2.js";import{a,b as d,c as l,d as h,e as r,g as e,h as n,i as s}from"./runtime-L2HNXIHW.js";import{a as c,b as p}from"./runtime-B73WLANC.js";var{prev:u,next:m}=a("/extending"),i={route:"/extending",meta:{title:"Extending Pulse \u2014 Pulse Docs",description:"Escape hatches for features outside the Pulse spec \u2014 onRequest middleware, raw server access, WebSockets, SSE, and custom error handling.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/extending",prev:u,next:m,content:`
|
|
2
|
-
${l("Extending Pulse")}
|
|
3
|
-
${h("Pulse handles the standard request-response lifecycle through specs. For requirements outside that model \u2014 middleware, WebSockets, SSE, custom error pages \u2014 Pulse exposes deliberate integration points directly on the underlying Node.js server. There is no abstraction layer to fight through.")}
|
|
4
|
-
|
|
5
|
-
${r("onrequest","Request interception with onRequest")}
|
|
6
|
-
<p><code>onRequest</code> is a hook on <code>createServer</code> that fires on every incoming request before Pulse handles it. Return <code>false</code> to short-circuit Pulse entirely and handle the response yourself.</p>
|
|
7
|
-
${e(t(`// server.js
|
|
8
|
-
import { createServer } from '@invisibleloop/pulse'
|
|
9
|
-
import home from './src/pages/home.js'
|
|
10
|
-
|
|
11
|
-
createServer([home], {
|
|
12
|
-
port: 3000,
|
|
13
|
-
onRequest(req, res) {
|
|
14
|
-
// Logging
|
|
15
|
-
console.log(req.method, req.url)
|
|
16
|
-
|
|
17
|
-
// Block a path entirely
|
|
18
|
-
if (req.url === '/internal') {
|
|
19
|
-
res.writeHead(403)
|
|
20
|
-
res.end('Forbidden')
|
|
21
|
-
return false // stops Pulse processing this request
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Custom header on every response
|
|
25
|
-
res.setHeader('X-App-Version', '1.0.0')
|
|
26
|
-
// returning nothing (or undefined) lets Pulse continue normally
|
|
27
|
-
},
|
|
28
|
-
})`,"js"))}
|
|
29
|
-
${s("note","<code>onRequest</code> runs before routing, guard, and all server fetchers. Returning <code>false</code> gives full control of the response \u2014 Pulse steps aside entirely for that request.")}
|
|
30
|
-
<p>Common uses:</p>
|
|
31
|
-
${n(["Use case","Approach"],[["Request logging","Log <code>req.method</code>, <code>req.url</code>, timing"],["IP allowlisting","Check <code>req.socket.remoteAddress</code>, return <code>false</code> with 403 if blocked"],["Rate limiting","Track request counts in a <code>Map</code>, return <code>false</code> with 429 when exceeded"],["Custom response headers","Call <code>res.setHeader()</code> before returning"],["Health check endpoint","Match <code>/healthz</code>, write <code>200 ok</code>, return <code>false</code>"]])}
|
|
32
|
-
|
|
33
|
-
${r("raw-server","Accessing the raw server")}
|
|
34
|
-
<p><code>createServer</code> returns a <code>{ server }</code> object where <code>server</code> is a plain Node.js <code>http.Server</code>. You can attach any listener to it directly.</p>
|
|
35
|
-
${e(t(`const { server } = createServer([home], { port: 3000 })
|
|
36
|
-
|
|
37
|
-
// The server instance is available immediately after createServer() returns.
|
|
38
|
-
// It starts listening automatically \u2014 no need to call server.listen().
|
|
39
|
-
server.on('listening', () => {
|
|
40
|
-
console.log('ready')
|
|
41
|
-
})`,"js"))}
|
|
42
|
-
|
|
43
|
-
${r("websockets","WebSockets")}
|
|
44
|
-
<p>Use the <code>upgrade</code> event on the server instance. The <code>ws</code> package handles the WebSocket handshake and framing.</p>
|
|
45
|
-
${e(t("npm install ws","bash"))}
|
|
46
|
-
${e(t(`// server.js
|
|
47
|
-
import { WebSocketServer } from 'ws'
|
|
48
|
-
import { createServer } from '@invisibleloop/pulse'
|
|
49
|
-
import home from './src/pages/home.js'
|
|
50
|
-
|
|
51
|
-
const { server } = createServer([home], { port: 3000 })
|
|
52
|
-
|
|
53
|
-
const wss = new WebSocketServer({ noServer: true })
|
|
54
|
-
|
|
55
|
-
server.on('upgrade', (req, socket, head) => {
|
|
56
|
-
if (req.url === '/ws') {
|
|
57
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
58
|
-
wss.emit('connection', ws, req)
|
|
59
|
-
})
|
|
60
|
-
} else {
|
|
61
|
-
socket.destroy()
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
wss.on('connection', (ws) => {
|
|
66
|
-
ws.send('connected')
|
|
67
|
-
ws.on('message', (msg) => ws.send(\`echo: \${msg}\`))
|
|
68
|
-
})`,"js"))}
|
|
69
|
-
<p>The Pulse-rendered page can connect to the WebSocket using an inline script. Pass <code>ctx.nonce</code> through a server fetcher so the script is allowed by the CSP:</p>
|
|
70
|
-
${e(t(`server: {
|
|
71
|
-
meta: async (ctx) => ({ nonce: ctx.nonce }),
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
view: (_state, server) => \`
|
|
75
|
-
<main id="main-content">
|
|
76
|
-
<p id="status">Connecting\u2026</p>
|
|
77
|
-
<script nonce="\${server.meta.nonce}">
|
|
78
|
-
const ws = new WebSocket('ws://' + location.host + '/ws')
|
|
79
|
-
ws.onmessage = (e) => {
|
|
80
|
-
document.getElementById('status').textContent = e.data
|
|
81
|
-
}
|
|
82
|
-
<\/script>
|
|
83
|
-
</main>
|
|
84
|
-
\`,`,"js"))}
|
|
85
|
-
|
|
86
|
-
${r("sse","Server-Sent Events")}
|
|
87
|
-
<p>SSE keeps an HTTP connection open and streams events to the browser. Use <code>onRequest</code> to intercept the SSE path and write to the response directly.</p>
|
|
88
|
-
${e(t(`onRequest(req, res) {
|
|
89
|
-
if (req.url !== '/events') return // let Pulse handle everything else
|
|
90
|
-
|
|
91
|
-
res.writeHead(200, {
|
|
92
|
-
'Content-Type': 'text/event-stream',
|
|
93
|
-
'Cache-Control': 'no-cache',
|
|
94
|
-
'Connection': 'keep-alive',
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
// Send a keepalive comment every 15 seconds
|
|
98
|
-
const keepalive = setInterval(() => res.write(': keepalive\\n\\n'), 15000)
|
|
99
|
-
|
|
100
|
-
// Send an event
|
|
101
|
-
function send(event, data) {
|
|
102
|
-
res.write(\`event: \${event}\\ndata: \${JSON.stringify(data)}\\n\\n\`)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
send('connected', { time: Date.now() })
|
|
106
|
-
|
|
107
|
-
req.on('close', () => clearInterval(keepalive))
|
|
108
|
-
|
|
109
|
-
return false
|
|
110
|
-
},`,"js"))}
|
|
111
|
-
|
|
112
|
-
${r("error","Custom error handling")}
|
|
113
|
-
<p><code>onError</code> in <code>createServer</code> receives unhandled errors from server fetchers and guard functions. Use it to log errors to an external service or render a custom error page.</p>
|
|
114
|
-
${e(t(`createServer([home], {
|
|
115
|
-
port: 3000,
|
|
116
|
-
|
|
117
|
-
onError(err, req, res) {
|
|
118
|
-
// Log to your error tracking service
|
|
119
|
-
console.error(err)
|
|
120
|
-
|
|
121
|
-
// Respond with a custom error page
|
|
122
|
-
res.writeHead(500, { 'Content-Type': 'text/html' })
|
|
123
|
-
res.end(\`
|
|
124
|
-
<!doctype html>
|
|
125
|
-
<html lang="en">
|
|
126
|
-
<head><title>Error</title></head>
|
|
127
|
-
<body>
|
|
128
|
-
<main id="main-content">
|
|
129
|
-
<h1>Something went wrong</h1>
|
|
130
|
-
<p>We've been notified and are looking into it.</p>
|
|
131
|
-
</main>
|
|
132
|
-
</body>
|
|
133
|
-
</html>
|
|
134
|
-
\`)
|
|
135
|
-
},
|
|
136
|
-
})`,"js"))}
|
|
137
|
-
|
|
138
|
-
${r("client-js","Custom client-side JavaScript")}
|
|
139
|
-
<p>Pulse has no client-side JS of its own beyond the hydration runtime. For behaviour that genuinely needs to run in the browser \u2014 third-party widgets, analytics, canvas \u2014 use an inline script in the view with the request nonce. The nonce is unique per request and is required for the script to pass the CSP.</p>
|
|
140
|
-
${e(t(`server: {
|
|
141
|
-
meta: async (ctx) => ({ nonce: ctx.nonce }),
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
view: (_state, server) => \`
|
|
145
|
-
<main id="main-content">
|
|
146
|
-
<canvas id="chart" width="600" height="300"></canvas>
|
|
147
|
-
<script nonce="\${server.meta.nonce}">
|
|
148
|
-
const ctx = document.getElementById('chart').getContext('2d')
|
|
149
|
-
// draw directly with Canvas API \u2014 no library needed for simple charts
|
|
150
|
-
ctx.fillStyle = '#9b8dff'
|
|
151
|
-
ctx.fillRect(10, 10, 100, 80)
|
|
152
|
-
<\/script>
|
|
153
|
-
</main>
|
|
154
|
-
\`,`,"js"))}
|
|
155
|
-
${s("note","External scripts loaded via <code>src</code> also need the nonce attribute, or their domain must be added to the <code>script-src</code> directive in the CSP. The nonce approach is simpler and does not require config changes.")}
|
|
156
|
-
|
|
157
|
-
${r("when","Choosing the right approach")}
|
|
158
|
-
${n(["What you need","Reach for"],[["Middleware \u2014 logging, rate limiting, custom headers","<code>onRequest</code>"],["Non-HTML responses \u2014 JSON APIs, webhooks, RSS, sitemaps","Raw response spec (<code>contentType</code> + <code>render</code>)"],["Real-time bidirectional communication","WebSockets via <code>server.on('upgrade')</code>"],["Server-pushed updates (read-only stream)","SSE via <code>onRequest</code>"],["Custom error pages","<code>onError</code>"],["Browser-only behaviour","Inline <code><script nonce></code> in the view"]])}
|
|
159
|
-
`})};var o=document.getElementById("pulse-root");o&&!o.dataset.pulseMounted&&(o.dataset.pulseMounted="1",c(i,o,window.__PULSE_SERVER__||{},{ssr:!0}),p(o,c));var q=i;export{q as default};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import{a as r,b as i,c as o,d as n}from"./runtime-L2HNXIHW.js";import{a,b as c}from"./runtime-B73WLANC.js";var{prev:l,next:u}=r("/faq");function e(d,p){return`
|
|
2
|
-
<div class="faq-item">
|
|
3
|
-
<h2 class="faq-q">${d}</h2>
|
|
4
|
-
<div class="faq-a">${p}</div>
|
|
5
|
-
</div>`}var s={route:"/faq",meta:{title:"FAQ \u2014 Pulse Docs",description:"Frequently asked questions about the Pulse framework.",styles:["/pulse-ui.css","/docs.css"]},state:{},view:()=>i({currentHref:"/faq",prev:l,next:u,content:`
|
|
6
|
-
${o("FAQ")}
|
|
7
|
-
${n("Common questions about Pulse \u2014 what it is, what it isn't, and whether it's right for your project.")}
|
|
8
|
-
|
|
9
|
-
${e("Is Pulse ready for production?",`<p>The architecture is production-quality \u2014 streaming SSR, security headers, immutable caching, and zero runtime dependencies are built in and have been running reliably in real deployments. The framework itself targets Lighthouse 100 on every scaffolded page.</p>
|
|
10
|
-
<p>That said, Pulse is v0.1 early access. The core spec format is stable, but some APIs may change before v1. It is best suited to new projects where you control the stack, and to teams who are comfortable building on something that is still evolving. If you need a framework with a five-year stability guarantee, wait for v1.</p>`)}
|
|
11
|
-
|
|
12
|
-
${e("Why plain JS objects instead of JSX or components?",`<p>JSX and component trees are designed for incremental DOM updates \u2014 the virtual DOM needs a tree to diff. Pulse does not have a virtual DOM. Views are pure functions that return an HTML string; the framework re-renders the whole page segment on state change.</p>
|
|
13
|
-
<p>Plain JS objects are also unambiguous for AI agents. There is no syntax to learn, no component abstraction to navigate \u2014 just a JS object with well-defined keys. An agent can read, write, and validate a spec without understanding any framework-specific idioms.</p>`)}
|
|
14
|
-
|
|
15
|
-
${e("Why no virtual DOM?",`<p>A virtual DOM solves the problem of efficient incremental updates to a large, complex component tree. Pulse pages are server-rendered HTML strings \u2014 the client runtime re-renders a bounded section of the page when state changes, which is fast enough for the kinds of interactions Pulse is designed for.</p>
|
|
16
|
-
<p>Eliminating the virtual DOM means eliminating the ~40\u2013100 kB runtime that comes with it. Pulse ships ~2 kB brotli to the browser on first visit. That is not a compression trick \u2014 there is genuinely no framework runtime on the client.</p>`)}
|
|
17
|
-
|
|
18
|
-
${e("Can I use npm packages in my specs?",`<p>Yes, on the server side. Server fetchers run in Node.js and can import any npm package \u2014 database clients, API SDKs, utility libraries. These never reach the browser.</p>
|
|
19
|
-
<p>Client-side code (mutations and actions) runs in the browser and is bundled by esbuild. You can import pure JS utilities here, but avoid packages that depend on Node.js built-ins or that are large \u2014 the goal is to keep the client bundle small. UI components come from the built-in component library, which covers most cases without external dependencies.</p>`)}
|
|
20
|
-
|
|
21
|
-
${e("Does Pulse work with TypeScript?",`<p>Type definitions are included for all public APIs \u2014 <code>createServer</code>, the spec shape, UI components, and the testing utilities. You can write specs in TypeScript and import the <code>Spec</code> type to get full autocompletion and type checking.</p>
|
|
22
|
-
<p>The examples and docs are in plain JS for readability, but TypeScript is a first-class option.</p>`)}
|
|
23
|
-
|
|
24
|
-
${e("Can I use a database directly in Pulse?",`<p>Yes. Server fetchers are plain async functions \u2014 you can query a database, call an ORM, or use any server-side library directly. There is no data layer abstraction to work around.</p>
|
|
25
|
-
<pre class="code-block"><code>server: {
|
|
26
|
-
posts: async (ctx) => {
|
|
27
|
-
return db.posts.findMany({ where: { published: true } })
|
|
28
|
-
},
|
|
29
|
-
}</code></pre>
|
|
30
|
-
<p>The result is passed to your view as <code>server.posts</code>. See the <a href="/server-data">Server Data</a> and <a href="/supabase">Supabase</a> docs for worked examples.</p>`)}
|
|
31
|
-
|
|
32
|
-
${e("How does Pulse compare to Next.js?",`<p>Next.js is a full-featured React framework designed for large teams building complex applications. It ships React to the browser on every page, supports many rendering strategies, and has a large ecosystem.</p>
|
|
33
|
-
<p>Pulse is opinionated in the other direction: one spec format, one rendering model, no client framework, no configuration. It is smaller, simpler, and faster to get a Lighthouse 100 result \u2014 but it does not have the ecosystem breadth or the component model that React provides. If your project needs React, use Next.js. If you want to ship fast, simple, server-rendered pages with minimal JS, Pulse is worth trying.</p>`)}
|
|
34
|
-
|
|
35
|
-
${e("Can I build a multi-page app with client-side navigation?",`<p>Yes. <code>initNavigation</code> intercepts same-origin link clicks, fetches the new page, swaps the DOM, and re-mounts the spec \u2014 all without a full page reload. From the user's perspective it feels like an SPA; from the server's perspective every navigation is a standard HTTP request.</p>
|
|
36
|
-
<p>See the <a href="/navigation">Navigation</a> docs for details.</p>`)}
|
|
37
|
-
|
|
38
|
-
${e("Does Pulse handle authentication?",`<p>Pulse has a <code>guard</code> function that runs before any server fetcher executes. It receives the request context (cookies, headers, params) and can redirect or return an error if the user is not authenticated. This makes it impossible to accidentally skip an auth check \u2014 the guard runs before data is fetched.</p>
|
|
39
|
-
<p>For a complete auth integration, see the <a href="/auth">Auth0 guide</a>. The pattern works the same with any auth provider.</p>`)}
|
|
40
|
-
|
|
41
|
-
${e("What is the AI-native claim actually about?",`<p>Two things. First, the spec format was designed to be easy for an AI agent to generate correctly \u2014 one JS object per page, a validated schema, no ambiguous patterns. An agent that writes a Pulse spec either produces a valid spec or gets a schema error it can fix. There is no grey area.</p>
|
|
42
|
-
<p>Second, the CLI starts an MCP server alongside the dev server, giving the agent tools to create pages, validate specs, screenshot routes, and run Lighthouse audits \u2014 without leaving the editor. The agent can build and verify a page end-to-end without human intervention on the tooling side.</p>`)}
|
|
43
|
-
`})};var t=document.getElementById("pulse-root");t&&!t.dataset.pulseMounted&&(t.dataset.pulseMounted="1",a(s,t,window.__PULSE_SERVER__||{},{ssr:!0}),c(t,a));var b=s;export{b as default};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import{a as s}from"./runtime-QFURDKA2.js";import{a as n,b as l,c as d,d as c,e,g as t,i as a}from"./runtime-L2HNXIHW.js";import{a as r,b as u}from"./runtime-B73WLANC.js";var{prev:p,next:h}=n("/getting-started"),i={route:"/getting-started",meta:{title:"Getting Started \u2014 Pulse Docs",description:"Install Pulse and build your first page with an AI agent in minutes.",styles:["/docs.css"]},state:{},view:()=>l({currentHref:"/getting-started",prev:p,next:h,content:`
|
|
2
|
-
${d("Getting Started")}
|
|
3
|
-
${c("Install Pulse, run one command, and have Claude building your first page \u2014 with streaming SSR, security headers, and a 100 Lighthouse score already in place.")}
|
|
4
|
-
|
|
5
|
-
${e("requirements","Requirements")}
|
|
6
|
-
<ul>
|
|
7
|
-
<li><strong>Node.js 22 or later</strong> \u2014 <a href="https://nodejs.org" target="_blank" rel="noopener">nodejs.org</a></li>
|
|
8
|
-
<li><strong>Claude Code</strong> \u2014 the CLI for Claude, installed and authenticated \u2014 <a href="https://docs.anthropic.com/en/docs/claude-code/getting-started" target="_blank" rel="noopener">installation guide</a></li>
|
|
9
|
-
</ul>
|
|
10
|
-
<p>Claude Code provides the <code>claude</code> command. Pulse launches it automatically with the Pulse MCP server wired in \u2014 so the agent has instant access to the framework reference, your project structure, and all Pulse tools without any manual configuration.</p>
|
|
11
|
-
${a("note","GitHub Copilot integration is coming soon. For now, Pulse works exclusively with Claude Code.")}
|
|
12
|
-
|
|
13
|
-
${e("install","Install Pulse")}
|
|
14
|
-
<p>Install the Pulse CLI globally:</p>
|
|
15
|
-
${t(s("npm install -g @invisibleloop/pulse","bash"))}
|
|
16
|
-
|
|
17
|
-
${e("create","Create your project")}
|
|
18
|
-
<p>Run <code>pulse</code> in any empty directory:</p>
|
|
19
|
-
${t(s(`mkdir my-app
|
|
20
|
-
cd my-app
|
|
21
|
-
pulse`,"bash"))}
|
|
22
|
-
<p>Pulse detects the directory is empty and scaffolds a project there. It creates the project files, installs dependencies, and exits with:</p>
|
|
23
|
-
${t(s("\u2713 Project ready. Run `pulse` again to start your AI session.","bash"))}
|
|
24
|
-
${a("tip","Running <code>pulse</code> in a non-empty directory prompts for a project name, then creates and scaffolds a subdirectory with that name \u2014 so you can run it from anywhere.")}
|
|
25
|
-
|
|
26
|
-
${e("session","Start a session")}
|
|
27
|
-
<p>Run <code>pulse</code> again from inside your project directory:</p>
|
|
28
|
-
${t(s("pulse","bash"))}
|
|
29
|
-
<p>This time, Pulse detects the existing project and launches Claude Code with the Pulse MCP server already connected. Claude opens with the complete framework guide loaded, your project structure visible, and all Pulse tools available \u2014 ready to build immediately.</p>
|
|
30
|
-
${a("note","Run <code>pulse</code> every time you open a working session. It handles starting Claude and wiring up the MCP server. Once Claude is open, use <code>/pulse-dev</code> to start the dev server.")}
|
|
31
|
-
|
|
32
|
-
${e("first-build","Build your first page")}
|
|
33
|
-
<p>Once Claude opens, start the dev server and ask for something:</p>
|
|
34
|
-
${t(s(`/pulse-dev
|
|
35
|
-
|
|
36
|
-
"Create a contact form with name, email, and message fields.
|
|
37
|
-
Validate the email format before submitting."`,"bash"))}
|
|
38
|
-
<p>The agent will:</p>
|
|
39
|
-
<ol>
|
|
40
|
-
<li>Fetch the Pulse guide from the MCP server \u2014 spec format, component library, quality rules</li>
|
|
41
|
-
<li>Check what pages already exist in your project</li>
|
|
42
|
-
<li>Write the spec \u2014 route, state, validation, action lifecycle, view</li>
|
|
43
|
-
<li>Validate it against the schema and fix every error and warning</li>
|
|
44
|
-
<li>Open the page in the browser and confirm it looks right</li>
|
|
45
|
-
</ol>
|
|
46
|
-
<p>You do not need to explain Pulse to the agent. The MCP server supplies the reference. Just describe what you want.</p>
|
|
47
|
-
|
|
48
|
-
${e("what-was-created","What got created")}
|
|
49
|
-
<p>When you ran <code>pulse</code> in step 2, these files were written to your directory:</p>
|
|
50
|
-
${t(s(`my-app/
|
|
51
|
-
\u251C\u2500\u2500 src/
|
|
52
|
-
\u2502 \u251C\u2500\u2500 pages/
|
|
53
|
-
\u2502 \u2502 \u2514\u2500\u2500 home.js \u2190 your first page spec (a working counter)
|
|
54
|
-
\u2502 \u2514\u2500\u2500 components/ \u2190 shared view components go here
|
|
55
|
-
\u251C\u2500\u2500 public/
|
|
56
|
-
\u2502 \u251C\u2500\u2500 app.css \u2190 global stylesheet
|
|
57
|
-
\u2502 \u251C\u2500\u2500 pulse-ui.css \u2190 Pulse component library styles
|
|
58
|
-
\u2502 \u2514\u2500\u2500 pulse-ui.js \u2190 Pulse component library behaviour
|
|
59
|
-
\u251C\u2500\u2500 .claude/
|
|
60
|
-
\u2502 \u251C\u2500\u2500 CLAUDE.md \u2190 session instructions Claude reads on startup
|
|
61
|
-
\u2502 \u251C\u2500\u2500 settings.json \u2190 hooks: syntax checks, colour guards, package blocklist
|
|
62
|
-
\u2502 \u2514\u2500\u2500 pulse-checklist.md \u2190 spec review checklist, kept in sync by Pulse
|
|
63
|
-
\u251C\u2500\u2500 package.json
|
|
64
|
-
\u2514\u2500\u2500 pulse.config.js \u2190 port and project settings`,"bash"))}
|
|
65
|
-
<p><code>src/pages/home.js</code> is a complete working spec \u2014 a counter with increment and decrement buttons. Open <a href="http://localhost:3000">localhost:3000</a> after running <code>/pulse-dev</code> to see it. Every new page you create goes into <code>src/pages/</code> and is discovered automatically.</p>
|
|
66
|
-
<p>The <code>.claude/</code> directory contains the agent's operating context. <code>CLAUDE.md</code> tells Claude how the project is structured, <code>settings.json</code> configures hooks that catch common mistakes before they reach you \u2014 hardcoded hex colours, emoji in UI output, and installing client-side rendering libraries are all flagged or blocked automatically.</p>
|
|
67
|
-
|
|
68
|
-
${e("commands","Agent commands")}
|
|
69
|
-
<p>These slash commands are available once Claude is open:</p>
|
|
70
|
-
${t(s(`/pulse-dev # start (or restart) the dev server
|
|
71
|
-
/pulse-stop # stop the dev server
|
|
72
|
-
/pulse-build # production build \u2192 public/dist/
|
|
73
|
-
/pulse-start # run the production server
|
|
74
|
-
/pulse-report # Lighthouse audit + performance report`,"bash"))}
|
|
75
|
-
<p>You can also skip the commands entirely \u2014 just describe what you want and the agent handles the rest, including starting the dev server when needed.</p>
|
|
76
|
-
|
|
77
|
-
${e("next-steps","Next steps")}
|
|
78
|
-
<ul>
|
|
79
|
-
<li><a href="/how-it-works">How It Works</a> \u2014 the MCP server, what the agent knows, and the full build cycle</li>
|
|
80
|
-
<li><a href="/project-structure">Project Structure</a> \u2014 where files live and how pages are discovered</li>
|
|
81
|
-
<li><a href="/spec">Spec Reference</a> \u2014 every field the spec supports</li>
|
|
82
|
-
<li><a href="/state">State</a> \u2014 client state and mutations</li>
|
|
83
|
-
<li><a href="/server-data">Server Data</a> \u2014 fetch data on the server before the page renders</li>
|
|
84
|
-
<li><a href="/prompt-examples">Prompt Examples</a> \u2014 real prompts with the output they produce</li>
|
|
85
|
-
</ul>
|
|
86
|
-
`})};var o=document.getElementById("pulse-root");o&&!o.dataset.pulseMounted&&(o.dataset.pulseMounted="1",r(i,o,window.__PULSE_SERVER__||{},{ssr:!0}),u(o,r));var C=i;export{C as default};
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import{a as r}from"./runtime-QFURDKA2.js";import{a as i,b as u,c as h,d as l,e,f as s,g as t,h as a,i as d}from"./runtime-L2HNXIHW.js";import{a as c,b as p}from"./runtime-B73WLANC.js";var{prev:f,next:g}=i("/guard"),n={route:"/guard",meta:{title:"Guard \u2014 Pulse Docs",description:"Per-route authorization in Pulse. Guard functions run before server data fetchers and redirect unauthorized requests.",styles:["/docs.css"]},state:{},view:()=>u({currentHref:"/guard",prev:f,next:g,content:`
|
|
2
|
-
${h("Guard")}
|
|
3
|
-
${l("A <code>guard</code> function runs on every request to a route, before any server data is fetched. It is the enforced access control point \u2014 unauthorized requests are redirected before any database queries or data fetchers execute.")}
|
|
4
|
-
|
|
5
|
-
${e("basics","Basic usage")}
|
|
6
|
-
<p>A <code>guard</code> function on any spec receives the same <code>ctx</code> object as server data fetchers \u2014 params, query, headers, and cookies.</p>
|
|
7
|
-
${t(r(`export default {
|
|
8
|
-
route: '/dashboard',
|
|
9
|
-
|
|
10
|
-
guard: async (ctx) => {
|
|
11
|
-
if (!ctx.cookies.session) return { redirect: '/login' }
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
server: {
|
|
15
|
-
user: async (ctx) => getCurrentUser(ctx.cookies.session),
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
state: {},
|
|
19
|
-
view: (state, server) => \`
|
|
20
|
-
<main id="main-content">
|
|
21
|
-
<h1>Welcome, \${server.user.name}</h1>
|
|
22
|
-
</main>
|
|
23
|
-
\`,
|
|
24
|
-
}`,"js"))}
|
|
25
|
-
|
|
26
|
-
<p>When the guard returns <code>{ redirect }</code>, the server responds with a <strong>302</strong> and all server data fetchers are skipped \u2014 no data is fetched for unauthorized requests. When the guard returns nothing, the request proceeds normally.</p>
|
|
27
|
-
|
|
28
|
-
${e("ctx","What ctx contains")}
|
|
29
|
-
${a(["Property / Method","Type","Description"],[["<code>ctx.cookies</code>","object","Parsed cookies from the <code>Cookie</code> header"],["<code>ctx.headers</code>","object","Raw request headers"],["<code>ctx.params</code>","object",'Route params e.g. <code>{ id: "42" }</code>'],["<code>ctx.query</code>","object","Parsed query string"],["<code>ctx.pathname</code>","string","URL path e.g. <code>/dashboard</code>"],["<code>ctx.method</code>","string","HTTP method e.g. <code>GET</code>, <code>POST</code>"],["<code>ctx.store</code>","object","Resolved global store state (if a store is registered)"],["<code>ctx.nonce</code>","string","CSP nonce for the current request"],["<code>await ctx.json()</code>","object | null","Parse a JSON request body"],["<code>await ctx.text()</code>","string","Read the body as a plain string"],["<code>await ctx.formData()</code>","object | null","Parse a URL-encoded body into a plain object"],["<code>await ctx.buffer()</code>","Buffer","Read the raw body as a Node.js Buffer"]])}
|
|
30
|
-
|
|
31
|
-
${e("examples","Common patterns")}
|
|
32
|
-
|
|
33
|
-
${s("Session check")}
|
|
34
|
-
<p>Redirect to login when no session cookie is present.</p>
|
|
35
|
-
${t(r(`guard: async (ctx) => {
|
|
36
|
-
if (!ctx.cookies.session) return { redirect: '/login' }
|
|
37
|
-
}`,"js"))}
|
|
38
|
-
|
|
39
|
-
${s("Role-based access")}
|
|
40
|
-
<p>Fetch the user from the session and check their role. Keep the lookup fast \u2014 guard runs on every request to the route.</p>
|
|
41
|
-
${t(r(`guard: async (ctx) => {
|
|
42
|
-
const user = await getUserFromSession(ctx.cookies.session)
|
|
43
|
-
if (!user) return { redirect: '/login' }
|
|
44
|
-
if (!user.isAdmin) return { redirect: '/403' }
|
|
45
|
-
}`,"js"))}
|
|
46
|
-
|
|
47
|
-
${s("Redirect authenticated users away from login")}
|
|
48
|
-
<p>Useful for login and signup pages \u2014 send already-authenticated users somewhere useful.</p>
|
|
49
|
-
${t(r(`export default {
|
|
50
|
-
route: '/login',
|
|
51
|
-
|
|
52
|
-
guard: async (ctx) => {
|
|
53
|
-
if (ctx.cookies.session) return { redirect: '/dashboard' }
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
state: {},
|
|
57
|
-
view: () => \`<main id="main-content">...</main>\`,
|
|
58
|
-
}`,"js"))}
|
|
59
|
-
|
|
60
|
-
${d("info","Guard runs server-side on every request, including client-side navigation requests \u2014 those go through the same server pipeline. There is no way to bypass guard from the browser.")}
|
|
61
|
-
|
|
62
|
-
${e("custom-responses","Custom status responses")}
|
|
63
|
-
<p>Guard can return a custom HTTP response instead of (or alongside) a redirect. Return <code>{ status, json?, body?, headers? }</code> to send any status code with an optional JSON or text body. This is useful for POST handlers that need to signal validation errors or API-style rejections:</p>
|
|
64
|
-
${t(r(`guard: async (ctx) => {
|
|
65
|
-
const token = ctx.headers.authorization
|
|
66
|
-
if (!token) return { status: 401, json: { error: 'Unauthorized' } }
|
|
67
|
-
|
|
68
|
-
if (ctx.method === 'POST') {
|
|
69
|
-
const data = await ctx.formData()
|
|
70
|
-
if (!data.email) return { status: 422, json: { error: 'Email required' } }
|
|
71
|
-
await db.leads.create(data)
|
|
72
|
-
return { redirect: '/contact?sent=1' }
|
|
73
|
-
}
|
|
74
|
-
// return nothing to let a GET request proceed to the view
|
|
75
|
-
}`,"js"))}
|
|
76
|
-
${d("note","To use <code>guard</code> as a POST handler, the spec must declare <code>methods: ['GET', 'POST']</code>. Without it, POST requests are rejected with 405 before guard runs.")}
|
|
77
|
-
|
|
78
|
-
${e("reference","Reference")}
|
|
79
|
-
${a(["Property","Type","Required"],[["<code>guard</code>","<code>async (ctx) => { redirect?: string } | { status, json?, body?, headers? } | void</code>","No"]])}
|
|
80
|
-
`})};var o=document.getElementById("pulse-root");o&&!o.dataset.pulseMounted&&(o.dataset.pulseMounted="1",c(n,o,window.__PULSE_SERVER__||{},{ssr:!0}),p(o,c));var P=n;export{P as default};
|