@invisibleloop/pulse 0.1.28 → 0.1.30
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/.github/workflows/publish.yml +11 -20
- package/README.md +1 -1
- 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 +1 -1
- package/public/pulse-ui.css +2 -2
- package/src/agent/guide-components.md +1 -1
- package/src/agent/guide-routing.md +20 -0
- package/src/agent/guide-spec.md +10 -1
- package/src/agent/guide-styles.md +16 -1
- package/src/agent/workflow.md +1 -1
- package/src/cli/scaffold.js +63 -2
- package/src/mcp/server.js +34 -18
- package/src/server/index.js +26 -7
- package/src/server/server.test.js +47 -0
- package/src/ui/stat.js +1 -1
- package/src/ui/ui.test.js +6 -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,71 +0,0 @@
|
|
|
1
|
-
import{a}from"./runtime-QFURDKA2.js";import{a as i,b as r,c,d as l,e as t,g as e,h as d,i as m}from"./runtime-L2HNXIHW.js";import{a as n,b as u}from"./runtime-B73WLANC.js";var{prev:p,next:h}=i("/constraints"),s={route:"/constraints",meta:{title:"Constraints \u2014 Pulse Docs",description:"Automatic min/max bounds on state values, enforced after every mutation.",styles:["/docs.css"]},state:{},view:()=>r({currentHref:"/constraints",prev:p,next:h,content:`
|
|
2
|
-
${c("Constraints")}
|
|
3
|
-
${l("Constraints are always-on bounds for numeric state values. After every mutation, Pulse clamps constrained values to their declared range before the view re-renders. The value can never go out of range \u2014 there is no code path where it can.")}
|
|
4
|
-
|
|
5
|
-
${t("declaring","Declaring constraints")}
|
|
6
|
-
<p>The <code>constraints</code> field maps top-level state keys to bounds objects with optional <code>min</code> and <code>max</code> properties:</p>
|
|
7
|
-
${e(a(`export default {
|
|
8
|
-
route: '/cart',
|
|
9
|
-
state: {
|
|
10
|
-
quantity: 1,
|
|
11
|
-
zoom: 1.0,
|
|
12
|
-
rating: 0,
|
|
13
|
-
},
|
|
14
|
-
constraints: {
|
|
15
|
-
quantity: { min: 1, max: 99 },
|
|
16
|
-
zoom: { min: 0.5, max: 3.0 },
|
|
17
|
-
rating: { min: 0, max: 5 },
|
|
18
|
-
},
|
|
19
|
-
mutations: {
|
|
20
|
-
increaseQty: (state) => ({ quantity: state.quantity + 1 }),
|
|
21
|
-
decreaseQty: (state) => ({ quantity: state.quantity - 1 }),
|
|
22
|
-
zoomIn: (state) => ({ zoom: state.zoom + 0.1 }),
|
|
23
|
-
zoomOut: (state) => ({ zoom: state.zoom - 0.1 }),
|
|
24
|
-
},
|
|
25
|
-
}`,"js"))}
|
|
26
|
-
<p>When <code>decreaseQty</code> runs and <code>quantity</code> is already 1, the constraint clamps it back to 1 before the view renders. The mutation does not need to check bounds \u2014 the spec declares them once and Pulse enforces them everywhere.</p>
|
|
27
|
-
|
|
28
|
-
${t("vs-validation","Constraints vs Validation")}
|
|
29
|
-
${d(["","Constraints","Validation"],[["When it runs","After <strong>every</strong> mutation, automatically","Only when an action has <code>validate: true</code>"],["What it does","Clamps numeric values silently","Rejects the action and surfaces errors"],["User feedback","None \u2014 state is silently corrected","Explicit error messages shown in the view"],["Best for","Numeric ranges that must never be exceeded","Form field correctness before submission"]])}
|
|
30
|
-
|
|
31
|
-
${m("note","Constraints and validation serve different purposes. Constraints silently enforce numeric bounds at every mutation \u2014 they cannot be bypassed. Validation rejects invalid form data before an action's async work begins \u2014 it only runs when explicitly declared.")}
|
|
32
|
-
|
|
33
|
-
${t("one-sided","One-sided bounds")}
|
|
34
|
-
<p>Either <code>min</code> or <code>max</code> can be declared alone \u2014 both are optional:</p>
|
|
35
|
-
${e(a(`constraints: {
|
|
36
|
-
count: { min: 0 }, // no upper limit
|
|
37
|
-
discount: { max: 100 }, // no lower limit
|
|
38
|
-
offset: { min: 0, max: 999 } // both bounds
|
|
39
|
-
}`,"js"))}
|
|
40
|
-
|
|
41
|
-
${t("how-it-works","How clamping works")}
|
|
42
|
-
<p>After Pulse applies a mutation's partial state update, it iterates over all declared constraints and applies <code>Math.max(min, Math.min(max, value))</code> to each constrained key. The view is then called with the clamped state.</p>
|
|
43
|
-
${e(a(`// state.count = 10, constraints.count = { min: 0, max: 10 }
|
|
44
|
-
mutations: {
|
|
45
|
-
increment: (state) => ({ count: state.count + 1 }),
|
|
46
|
-
// mutation returns { count: 11 }
|
|
47
|
-
// constraint clamps to 10
|
|
48
|
-
// view receives { count: 10 }
|
|
49
|
-
}`,"js"))}
|
|
50
|
-
|
|
51
|
-
${t("top-level","Top-level keys only")}
|
|
52
|
-
<p>Constraints apply to top-level state keys. To constrain nested values, consider flattening your state structure or applying bounds logic in the mutation itself:</p>
|
|
53
|
-
${e(a(`// Cannot do:
|
|
54
|
-
constraints: {
|
|
55
|
-
'player.health': { min: 0, max: 100 } // \u2717 nested paths not supported
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Do instead:
|
|
59
|
-
state: { playerHealth: 100 },
|
|
60
|
-
constraints: { playerHealth: { min: 0, max: 100 } },
|
|
61
|
-
|
|
62
|
-
// Or handle in the mutation:
|
|
63
|
-
mutations: {
|
|
64
|
-
takeDamage: (state, _, amount) => ({
|
|
65
|
-
player: {
|
|
66
|
-
...state.player,
|
|
67
|
-
health: Math.max(0, state.player.health - amount),
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
}`,"js"))}
|
|
71
|
-
`})};var o=document.getElementById("pulse-root");o&&!o.dataset.pulseMounted&&(o.dataset.pulseMounted="1",n(s,o,window.__PULSE_SERVER__||{},{ssr:!0}),u(o,n));var k=s;export{k as default};
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import{a as t}from"./runtime-QFURDKA2.js";import{a as d,b as c,c as p,d as l,e,g as o,h as r,i as s}from"./runtime-L2HNXIHW.js";import{a as n,b as u}from"./runtime-B73WLANC.js";var{prev:m,next:h}=d("/deploy"),i={route:"/deploy",meta:{title:"Deployment \u2014 Pulse Docs",description:"Deploy a Pulse app to a VPS, Docker, Fly.io, Railway, or Render.",styles:["/docs.css"]},state:{},view:()=>c({currentHref:"/deploy",prev:m,next:h,content:`
|
|
2
|
-
${p("Deployment")}
|
|
3
|
-
${l("Pulse deploys as a single Node.js process. No adapters, no serverless wrapping, no separate static file server required. Build once, run anywhere Node 22+ runs. All guarantees \u2014 security headers, brotli compression, immutable asset caching \u2014 are active in production automatically.")}
|
|
4
|
-
|
|
5
|
-
${e("build","Build")}
|
|
6
|
-
<p>Run the production build before deploying:</p>
|
|
7
|
-
${o(t("npm run build","bash"))}
|
|
8
|
-
<p>This generates content-hashed bundles in <code>public/dist/</code> and writes <code>public/dist/manifest.json</code>. The server reads the manifest at startup to resolve hydration script paths.</p>
|
|
9
|
-
${s("warning","Without a manifest, the server falls back to serving source files directly \u2014 no compression, no content-hashed filenames, and no <code>immutable</code> cache headers. Always run <code>npm run build</code> before deploying to production.")}
|
|
10
|
-
|
|
11
|
-
${e("files","What to deploy")}
|
|
12
|
-
${r(["Include","Reason"],[["<code>src/</code>","Page specs \u2014 imported by the server at runtime"],["<code>public/</code>","Static assets and built bundles (<code>public/dist/</code>)"],["<code>server.js</code>","Entry point"],["<code>pulse.config.js</code>","Server config"],["<code>package.json</code> + <code>node_modules/</code>","Runtime dependencies"]])}
|
|
13
|
-
${r(["Exclude","Reason"],[["<code>.claude/</code>","AI agent config \u2014 not needed at runtime"],["<code>.pulse/</code>","Local report data \u2014 not needed at runtime"]])}
|
|
14
|
-
|
|
15
|
-
${e("env-vars","Environment variables")}
|
|
16
|
-
${r(["Variable","Default","Description"],[["<code>NODE_ENV</code>","<code>development</code>","Set to <code>production</code> to enable HSTS headers and production cache behaviour."],["<code>PORT</code>","Value in <code>pulse.config.js</code> (default <code>3000</code>)","Override the listening port. Most PaaS platforms set this automatically."]])}
|
|
17
|
-
${o(t("NODE_ENV=production pulse start","bash"))}
|
|
18
|
-
|
|
19
|
-
${e("pm2","VPS with PM2")}
|
|
20
|
-
<p>PM2 keeps the process alive, restarts it on crash, and manages logs.</p>
|
|
21
|
-
${o(t(`# Install PM2 globally
|
|
22
|
-
npm install -g pm2
|
|
23
|
-
|
|
24
|
-
# Start the app
|
|
25
|
-
NODE_ENV=production pm2 start server.js --name myapp
|
|
26
|
-
|
|
27
|
-
# Persist across reboots
|
|
28
|
-
pm2 save
|
|
29
|
-
pm2 startup
|
|
30
|
-
|
|
31
|
-
# Zero-downtime reload after a deploy
|
|
32
|
-
pm2 reload myapp`,"bash"))}
|
|
33
|
-
<p>For repeatable deployments, check an <code>ecosystem.config.cjs</code> into version control:</p>
|
|
34
|
-
${o(t(`// ecosystem.config.cjs
|
|
35
|
-
module.exports = {
|
|
36
|
-
apps: [{
|
|
37
|
-
name: 'myapp',
|
|
38
|
-
script: 'server.js',
|
|
39
|
-
env_production: {
|
|
40
|
-
NODE_ENV: 'production',
|
|
41
|
-
PORT: 3000,
|
|
42
|
-
},
|
|
43
|
-
}],
|
|
44
|
-
}`,"js"))}
|
|
45
|
-
${o(t("pm2 start ecosystem.config.cjs --env production","bash"))}
|
|
46
|
-
|
|
47
|
-
${e("docker","Docker")}
|
|
48
|
-
<p>A two-stage build keeps the image small \u2014 build tools stay in the first stage.</p>
|
|
49
|
-
${o(t(`# ---- build stage ----
|
|
50
|
-
FROM node:22-alpine AS build
|
|
51
|
-
WORKDIR /app
|
|
52
|
-
COPY package*.json ./
|
|
53
|
-
RUN npm ci
|
|
54
|
-
COPY . .
|
|
55
|
-
RUN npx pulse build
|
|
56
|
-
|
|
57
|
-
# ---- runtime stage ----
|
|
58
|
-
FROM node:22-alpine
|
|
59
|
-
WORKDIR /app
|
|
60
|
-
ENV NODE_ENV=production
|
|
61
|
-
COPY package*.json ./
|
|
62
|
-
RUN npm ci --omit=dev
|
|
63
|
-
COPY --from=build /app/src ./src
|
|
64
|
-
COPY --from=build /app/public ./public
|
|
65
|
-
COPY --from=build /app/server.js ./server.js
|
|
66
|
-
COPY pulse.config.js ./
|
|
67
|
-
EXPOSE 3000
|
|
68
|
-
CMD ["node", "server.js"]`,"bash"))}
|
|
69
|
-
${o(t(`docker build -t myapp .
|
|
70
|
-
docker run -p 3000:3000 --env NODE_ENV=production myapp`,"bash"))}
|
|
71
|
-
|
|
72
|
-
${e("fly","Fly.io")}
|
|
73
|
-
${o(t(`# fly.toml
|
|
74
|
-
app = 'myapp'
|
|
75
|
-
primary_region = 'lhr'
|
|
76
|
-
|
|
77
|
-
[env]
|
|
78
|
-
NODE_ENV = 'production'
|
|
79
|
-
|
|
80
|
-
[build]
|
|
81
|
-
[build.args]
|
|
82
|
-
NODE_VERSION = '22'
|
|
83
|
-
|
|
84
|
-
[deploy]
|
|
85
|
-
release_command = 'npx pulse build'
|
|
86
|
-
|
|
87
|
-
[http_service]
|
|
88
|
-
internal_port = 3000
|
|
89
|
-
force_https = true
|
|
90
|
-
auto_stop_machines = 'stop'
|
|
91
|
-
auto_start_machines = true
|
|
92
|
-
|
|
93
|
-
[[vm]]
|
|
94
|
-
memory = '256mb'
|
|
95
|
-
cpu_kind = 'shared'
|
|
96
|
-
cpus = 1`,"bash"))}
|
|
97
|
-
${o(t(`# First deploy
|
|
98
|
-
fly launch
|
|
99
|
-
|
|
100
|
-
# Subsequent deploys
|
|
101
|
-
fly deploy`,"bash"))}
|
|
102
|
-
|
|
103
|
-
${e("railway","Railway")}
|
|
104
|
-
<p>Railway auto-detects Node apps. Add a <code>railway.json</code> to set the build and start commands:</p>
|
|
105
|
-
${o(t(`{
|
|
106
|
-
"$schema": "https://railway.app/railway.schema.json",
|
|
107
|
-
"build": {
|
|
108
|
-
"builder": "NIXPACKS",
|
|
109
|
-
"buildCommand": "npm run build"
|
|
110
|
-
},
|
|
111
|
-
"deploy": {
|
|
112
|
-
"startCommand": "NODE_ENV=production node server.js",
|
|
113
|
-
"healthcheckPath": "/",
|
|
114
|
-
"restartPolicyType": "ON_FAILURE"
|
|
115
|
-
}
|
|
116
|
-
}`,"js"))}
|
|
117
|
-
|
|
118
|
-
${e("render","Render")}
|
|
119
|
-
${r(["Setting","Value"],[["Environment","Node"],["Build command","<code>npm install && npm run build</code>"],["Start command","<code>NODE_ENV=production node server.js</code>"],["Node version","<code>22.x</code>"]])}
|
|
120
|
-
<p>Set <code>NODE_ENV=production</code> in the Render environment variables dashboard.</p>
|
|
121
|
-
|
|
122
|
-
${e("serverless","Vercel, Cloudflare, and edge platforms")}
|
|
123
|
-
<p>These platforms each have multiple products with very different runtimes \u2014 the compatibility story varies significantly between them.</p>
|
|
124
|
-
|
|
125
|
-
${e("vercel","Vercel")}
|
|
126
|
-
<p>Vercel has two distinct runtimes:</p>
|
|
127
|
-
${r(["Product","Runtime","Pulse compatible?"],[["<strong>Functions</strong> (Node.js)","Full Node.js \u2014 same built-ins as a VPS","Partially \u2014 see below"],["<strong>Edge Functions</strong>","V8 isolates (no Node built-ins)","No"]])}
|
|
128
|
-
<p><strong>Vercel Functions (Node.js)</strong> can run Pulse with some differences in behaviour:</p>
|
|
129
|
-
${r(["Feature","Behaviour on Vercel Functions"],[["<code>serverTtl</code> cache","Works within a warm instance, but cold starts reset it. Not reliable for expensive queries."],["Streaming SSR","Vercel Functions support streaming responses, but require explicit configuration via <code>supportsResponseStreaming</code>."],["Static files","Vercel serves <code>public/</code> automatically via its CDN \u2014 Pulse's static file serving is bypassed."],["Security headers","Work as normal \u2014 Pulse adds them to every response."]])}
|
|
130
|
-
${s("warning","Vercel Functions are not a tested or officially supported deployment target for Pulse. The adapter pattern (exporting a request handler rather than starting a server) is not yet documented. Railway, Render, or Fly.io are simpler choices with no adaptation required.")}
|
|
131
|
-
|
|
132
|
-
${e("cloudflare","Cloudflare")}
|
|
133
|
-
${r(["Product","Runtime","Pulse compatible?"],[["<strong>Workers</strong>","V8 isolates \u2014 no <code>node:http</code>, <code>node:fs</code>, <code>node:zlib</code>","No"],["<strong>Pages Functions</strong>","Same V8 isolate runtime as Workers","No"],["<strong>CDN / proxy</strong>","Sits in front of your origin server","Yes \u2014 works great with Fly.io or a VPS behind it"]])}
|
|
134
|
-
${s("tip","The recommended pattern for edge performance: deploy Pulse to <strong>Fly.io</strong> (which runs real VMs in many regions) and put <strong>Cloudflare as a CDN/proxy</strong> in front of it. Static assets and cached HTML are served from Cloudflare's edge; dynamic requests are proxied to the nearest Fly VM.")}
|
|
135
|
-
|
|
136
|
-
${e("https","HTTPS and reverse proxy")}
|
|
137
|
-
<p>Pulse detects TLS automatically. When a request arrives with an <code>x-forwarded-proto: https</code> header or over a direct TLS socket, <code>Strict-Transport-Security: max-age=31536000; includeSubDomains</code> is added to the response. All four platforms above forward this header \u2014 no Pulse config is needed.</p>
|
|
138
|
-
<p>If running behind nginx for TLS termination:</p>
|
|
139
|
-
${o(t(`# nginx \u2014 TLS termination, proxy to Pulse
|
|
140
|
-
server {
|
|
141
|
-
listen 443 ssl;
|
|
142
|
-
server_name myapp.com;
|
|
143
|
-
|
|
144
|
-
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
|
|
145
|
-
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
|
|
146
|
-
|
|
147
|
-
location / {
|
|
148
|
-
proxy_pass http://localhost:3000;
|
|
149
|
-
proxy_http_version 1.1;
|
|
150
|
-
proxy_set_header Host $host;
|
|
151
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
152
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
# Redirect HTTP to HTTPS
|
|
157
|
-
server {
|
|
158
|
-
listen 80;
|
|
159
|
-
server_name myapp.com;
|
|
160
|
-
return 301 https://$host$request_uri;
|
|
161
|
-
}`,"bash"))}
|
|
162
|
-
${s("tip",`Use <a href="https://certbot.eff.org">Certbot</a> to obtain and auto-renew a free Let's Encrypt certificate: <code>certbot --nginx -d myapp.com</code>.`)}
|
|
163
|
-
`})};var a=document.getElementById("pulse-root");a&&!a.dataset.pulseMounted&&(a.dataset.pulseMounted="1",n(i,a,window.__PULSE_SERVER__||{},{ssr:!0}),u(a,n));var P=i;export{P as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}:root{--bg:#0d0d10;--surface:#111116;--surface-2:#18181f;--border:#38383f;--border-subtle:#1a1a20;--text:#e2e2ea;--muted:#9090a0;--muted-2:#7e7e92;--accent:#c9b800;--accent-text:#0a0a0a;--accent-hover:#e0ce00;--accent-dim:rgba(201,184,0,0.12);--green:#3dd68c;--red:#ff6b6b;--tok-kw:#c792ea;--tok-str:#c3e88d;--tok-cmt:#7a8a9a;--tok-num:#f78c6c;--tok-fn:#82aaff;--tok-prop:#ffcb6b;--tok-op:#89ddff;--tok-punct:#7e8899;--sidebar-w:260px;--header-h:52px;--content-w:740px;--font:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;--mono:"Fira Code","Cascadia Code","JetBrains Mono","Menlo","Monaco",monospace;--radius:8px}html{font-size:16px}body{font-family:var(--font);background:var(--bg);color:var(--text);line-height:1.6;min-height:100vh;-webkit-font-smoothing:antialiased}a{color:var(--accent);text-decoration:none}a:hover{color:var(--accent-hover)}.home{min-height:100vh;background:#f0e642;background-image:radial-gradient( circle,rgba(0,0,0,0.12) 1px,transparent 1px );background-size:22px 22px;color:#0a0a0a;--accent:#0a0a0a;--accent-hover:#2a2a2a;--accent-dim:rgba(0,0,0,0.07);--text:#0a0a0a;--muted:#52524a;--muted-2:#888880;--surface:#ffffff;--surface-2:#f0ede0;--border:#c8c4aa;--border-subtle:rgba(0,0,0,0.1);--green:#1a7a4a;--red:#c0392b}.home-nav{display:flex;align-items:center;justify-content:space-between;padding:1.25rem 2rem;border-bottom:none;position:sticky;top:0;background:#0a0a0a;backdrop-filter:none;z-index:10;--accent:#f0e642;--text:#ffffff}.home-nav-links{display:flex;align-items:center;gap:1.5rem}.home-nav-links a{color:rgba(255,255,255,0.85);font-size:0.9rem;transition:color 0.15s}.home-nav-links a:hover{color:#ffffff}.home-nav-links .btn-primary{background:#f0e642;color:#0a0a0a;font-weight:600;padding:0.4rem 1rem;border-radius:6px;font-size:0.875rem}.home-nav-links .btn-primary:hover{background:#f7f040;color:#0a0a0a}.hero{max-width:860px;margin:0 auto;padding:5rem 2rem;text-align:center;background:none}.hero-icon{margin-bottom:1.25rem}.hero-badge{display:inline-flex;align-items:center;gap:0.4rem;font-size:0.78rem;font-weight:600;color:#0a0a0a;background:rgba(0,0,0,0.08);border:1px solid rgba(0,0,0,0.18);border-radius:100px;padding:0.3rem 0.85rem;margin-bottom:2rem;letter-spacing:0.06em;text-transform:uppercase}.hero h1{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:800;line-height:1.1;letter-spacing:-0.03em;text-wrap:balance;margin-bottom:1.5rem;color:#0a0a0a;background:none;-webkit-background-clip:unset;-webkit-text-fill-color:#0a0a0a;background-clip:unset}.hero-subtitle{font-size:1.1rem;color:var(--muted);max-width:580px;margin:0 auto 2.5rem;line-height:1.65}.hero-ctas{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}.btn-primary{display:inline-flex;align-items:center;padding:0.7rem 1.5rem;border-radius:var(--radius);font-size:0.95rem;font-weight:600;background:#0a0a0a;color:#f0e642;text-decoration:none;border:2px solid #0a0a0a;transition:background 0.15s,color 0.15s,transform 0.15s}.btn-primary:hover{background:#f0e642;color:#0a0a0a;border-color:#0a0a0a;transform:translateY(-1px)}.btn-secondary{display:inline-flex;align-items:center;padding:0.7rem 1.5rem;border-radius:var(--radius);font-size:0.95rem;font-weight:600;background:transparent;color:#0a0a0a;border:1px solid rgba(0,0,0,0.25);text-decoration:none;transition:background 0.15s,border-color 0.15s}.btn-secondary:hover{background:rgba(0,0,0,0.06);border-color:#0a0a0a;color:#0a0a0a}.home-cta .btn-primary{background:#f0e642;color:#0a0a0a;border-color:#f0e642}.home-cta .btn-primary:hover{background:#0a0a0a;color:#f0e642;border-color:#f0e642;transform:translateY(-1px)}.home-cta .btn-secondary{background:transparent;border-color:rgba(255,255,255,0.25);color:rgba(255,255,255,0.75)}.home-cta .btn-secondary:hover{background:rgba(255,255,255,0.08);border-color:rgba(255,255,255,0.5);color:#ffffff}.home-code{background:#111114;padding:5rem 2rem 6rem}.home-code-inner{max-width:1000px;margin:0 auto}.home-code-header{text-align:center;margin-bottom:2.5rem}.home-code-header h2{font-size:clamp(1.6rem,3vw,2.2rem);font-weight:700;letter-spacing:-0.02em;color:#ffffff;margin-bottom:0.6rem}.home-code-header p{color:rgba(255,255,255,0.5);font-size:1.1rem;max-width:560px;margin:0 auto;line-height:1.65}.faq-item{padding:2rem 0;border-bottom:1px solid var(--border-subtle)}.faq-item:last-child{border-bottom:none}.faq-q{font-size:1.1rem;font-weight:600;color:var(--text);margin-bottom:0.875rem;letter-spacing:-0.01em;line-height:1.4}.faq-a p{font-size:0.925rem;color:var(--muted);line-height:1.75;max-width:68ch;margin-bottom:0.75rem}.faq-a p:last-child{margin-bottom:0}.faq-a .code-block{margin:0.75rem 0}.home-footer{border-top:1px solid rgba(255,255,255,0.08);text-align:center;padding:2rem;background:#0a0a0a;color:rgba(255,255,255,0.6);font-size:0.82rem}.home-footer a{color:rgba(255,255,255,0.75)}.home-footer a:hover{color:#f0e642}.section-label{font-size:1rem;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:#0a0a0a;text-align:center;margin-bottom:1rem;opacity:0.5}.home-stats{display:flex;align-items:center;justify-content:center;gap:0;padding:2.5rem 2rem;border-top:none;border-bottom:1px solid rgba(255,255,255,0.08);background:#0a0a0a;flex-wrap:wrap}.home-stat{display:flex;flex-direction:column;align-items:center;gap:0.35rem;padding:0.75rem 3rem}.home-stat-value{font-size:2rem;font-weight:700;color:#f0e642;letter-spacing:-0.02em;line-height:1}.home-stat-label{font-size:1.1rem;color:rgba(255,255,255,0.7);text-align:center}.home-stat-divider{width:1px;height:2.5rem;background:#f0e642;flex-shrink:0}.how{background:#0a0a0a;background-image:radial-gradient( circle,rgba(255,255,255,0.08) 1px,transparent 1px );background-size:22px 22px;padding:5rem 2rem;margin:0;max-width:none;--accent:#f0e642;--text:#ffffff;--muted:47473e;--border:rgba(255,255,255,0.12)}.how-inner{max-width:960px;margin:0 auto;text-align:center}.how .section-label{color:#f0e642;opacity:1}.how-steps{display:flex;align-items:flex-start;justify-content:center;gap:0;margin-top:3rem}.how-step{flex:1;max-width:260px;display:flex;flex-direction:column;align-items:center;gap:0.75rem;padding:0 1rem}.how-step-num{width:2.5rem;height:2.5rem;border-radius:50%;background:#f0e642;border:1px solid #f0e642;display:flex;align-items:center;justify-content:center;font-size:0.9rem;font-weight:700;color:#0a0a0a;flex-shrink:0}.how-step h3{font-size:1.1rem;font-weight:600;color:var(--text)}.how-step p{font-size:1.1rem;color:rgba(255,255,255,0.5);line-height:1.6}.how-connector{width:3rem;height:1px;background:#f0e642;margin-top:1.25rem;flex-shrink:0}.ai-first{padding:5rem 2rem;max-width:900px;margin:0 auto}.ai-first-title{font-size:clamp(1.6rem,4vw,2.2rem);font-weight:700;letter-spacing:-0.02em;margin:0.75rem 0 1.25rem}.ai-first-lead{color:var(--muted);font-size:1.1rem;line-height:1.75;max-width:72ch;margin-bottom:3rem}.ai-cols{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}@media (max-width:640px){.ai-cols{grid-template-columns:1fr}.ai-col--pulse{order:-1}}.ai-col{padding:1.75rem;border-radius:10px;border:1px solid var(--border);background:var(--surface)}.ai-col--pulse{border-color:#0a0a0a;background:#0a0a0a}.ai-col-title{font-size:0.8rem;font-weight:600;letter-spacing:0.07em;text-transform:uppercase;margin-bottom:1.25rem}.ai-col-title--bad{color:#5f5f5a}.ai-col-title--good{color:#f0e642}.ai-col-list{list-style:none;display:flex;flex-direction:column;gap:0.9rem}.ai-col-list li{font-size:1.1rem;color:var(--muted);line-height:1.6;padding-left:1.1rem;position:relative}.ai-col-list li::before{content:"–";position:absolute;left:0;color:var(--muted-2)}.ai-col--pulse .ai-col-list li{color:rgba(255,255,255,0.75)}.ai-col--pulse .ai-col-list li::before{content:"✓";color:#f0e642}.versus{padding:5rem 2rem;background:#0a0a0a;border-top:none;border-bottom:none;text-align:center;--accent:#f0e642}.versus-title{font-size:1.75rem;font-weight:700;margin-bottom:0.75rem;color:#ffffff}.versus-sub{color:rgba(255,255,255,0.5);max-width:560px;margin:0 auto 2.5rem;font-size:1.1rem;line-height:1.7}.versus .section-label{color:#f0e642;opacity:1}.table-sticky-col{overflow-x:auto}.table-sticky-col table th:first-child,.table-sticky-col table td:first-child{position:sticky;left:0;z-index:1;border-right:1px solid rgba(255,255,255,0.15)}.versus-table-wrap{overflow-x:auto;max-width:900px;margin:0 auto}.versus-table{width:100%;min-width:640px;border-collapse:collapse;font-size:1rem;text-align:left}.versus-table thead{background:rgba(255,255,255,0.06)}.versus-table thead th{padding:0.75rem 1rem;color:rgba(255,255,255,0.45);font-size:0.78rem;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;border-bottom:1px solid rgba(255,255,255,0.1)}.versus-table thead th:first-child{width:22%;background:#111314}.versus-table th[scope="row"]{background:#0a0a0a}.versus-table tbody tr:nth-child(odd) th[scope="row"]{background:#111214}.versus-table tbody tr:nth-child(odd){background:rgba(255,255,255,0.04)}.versus-table tr:hover td{background:rgba(255,255,255,0.08)}.versus-table td{padding:0.85rem 1rem;border-bottom:1px solid rgba(255,255,255,0.06);color:rgba(255,255,255,0.45);vertical-align:top;line-height:1.5}.versus-table th[scope="row"]{padding:0.85rem 1rem;border-bottom:1px solid rgba(255,255,255,0.06);color:rgba(255,255,255,0.75);font-weight:500;font-size:0.87rem;text-align:left}.versus-table .v-yes{color:#f0e642}.versus-table .v-partial{color:rgba(255,255,255,0.6)}.versus-table .v-no{color:rgba(255,255,255,0.6)}.usp-blocks{padding:4rem 2rem;max-width:1000px;margin:0 auto;display:flex;flex-direction:column;gap:0}.usp-block{display:grid;grid-template-columns:280px 1fr;gap:3rem;padding:3.5rem 0;border-bottom:1px solid var(--border-subtle);align-items:start}.usp-block:last-child{border-bottom:none}.usp-block-alt{direction:rtl}.usp-block-alt>*{direction:ltr}.usp-block-aside{display:flex;flex-direction:column;gap:0.75rem}.usp-icon{width:3rem;height:3rem;background:#0a0a0a;border:none;border-radius:var(--radius);display:flex;align-items:center;justify-content:center;margin-bottom:0.25rem;--accent:#f0e642}.usp-block-aside h2{font-size:1.2rem;font-weight:700}.usp-block-aside p{font-size:1.1rem;color:var(--muted);line-height:1.7}.usp-points{list-style:none;display:flex;flex-direction:column;gap:1.25rem;padding-top:0.25rem}.usp-points li{padding-left:1.25rem;border-left:2px solid #0a0a0a;font-size:1.1rem;color:var(--muted);line-height:1.7}.usp-points li strong{display:block;color:var(--text);font-weight:600;margin-bottom:0.15rem}.metrics-report{padding:5rem 2rem;border-top:1px solid var(--border-subtle);max-width:1100px;margin:0 auto}.metrics-header{text-align:center;margin-bottom:3rem}.metrics-title{font-size:clamp(1.6rem,3.5vw,2.2rem);font-weight:700;letter-spacing:-0.02em;margin-bottom:0.6rem}.metrics-generated{font-size:0.8rem;color:var(--muted);letter-spacing:0.01em}.metrics-groups{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem}.metrics-group{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--radius);padding:1.25rem 1.5rem 1.5rem}.metrics-group-label{font-size:0.7rem;font-weight:600;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);margin-bottom:1rem;padding-bottom:0.6rem;border-bottom:1px solid var(--border-subtle)}.metrics-items{display:flex;flex-direction:column;gap:1rem}.metric-item{display:flex;flex-direction:column;gap:0.15rem}.metric-val{font-size:1.5rem;font-weight:700;letter-spacing:-0.02em;color:var(--text);line-height:1}.metric-val--green{color:var(--green)}.metric-val--accent{color:var(--accent)}.metric-label{font-size:0.78rem;color:var(--muted);line-height:1.3}.home-cta{text-align:center;padding:5rem 2rem;background:#0a0a0a;background-image:radial-gradient( circle,rgba(255,255,255,0.07) 1px,transparent 1px );background-size:22px 22px;border-top:none}.home-cta h2{font-size:2rem;font-weight:700;margin-bottom:0.75rem;color:#ffffff}.home-cta p{color:rgba(255,255,255,0.5);margin-bottom:2rem;font-size:1.1rem}.home-cta-checks{list-style:none;display:flex;flex-wrap:wrap;justify-content:center;gap:0.6rem 1.5rem;margin-bottom:2rem}.home-cta-checks li{font-size:1.1rem;color:rgba(255,255,255,0.8);display:flex;align-items:center;gap:0.4rem}.home-cta-checks li::before{content:"✓";color:#f0e642;font-weight:700;font-size:1rem}.home-cta-actions{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}.sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:99;opacity:0;transition:opacity 0.25s ease}@media (max-width:768px){.sidebar-overlay{display:block;pointer-events:none}.sidebar-overlay.visible{opacity:1;pointer-events:auto}}.component-demo{border:1px solid var(--border);border-radius:var(--radius);margin:1.5rem 0}.demo-preview{padding:3.5rem 1.5rem 1.5rem;background:#0d0d10;border-radius:var(--radius) var(--radius) 0 0;display:flex;flex-direction:column;gap:0.75rem;position:relative}.demo-preview-inner{display:flex;flex-wrap:wrap;gap:0.75rem;align-items:flex-start;min-width:0;width:100%}.demo-preview--col .demo-preview-inner{flex-direction:column;align-items:stretch}.demo-preview--scroll .demo-preview-inner{flex-wrap:nowrap;overflow-x:auto}.demo-mobile-nav .ui-nav-links{display:none}.demo-mobile-nav .ui-nav-burger{display:flex}.demo-phone{width:320px;border-radius:32px;overflow:hidden;background:var(--bg,#0d0d10);box-shadow:0 0 0 7px var(--surface-2,#18181f),0 0 0 8px var(--border,#222228),0 24px 48px rgba(0,0,0,0.5)}.demo-phone-statusbar{height:28px;background:var(--bg,#0d0d10);display:flex;align-items:center;justify-content:center}.demo-phone-pill{width:64px;height:10px;background:var(--surface-2,#18181f);border-radius:99px}.demo-phone-content{min-height:160px;display:flex;align-items:center;justify-content:center;padding:1.5rem}.demo-phone-content p{margin:0;color:var(--muted,#6b6b80);font-size:0.8rem;text-align:center}.demo-preview.is-light{background:#f0f0f8}.demo-theme-toggle{position:absolute;top:0.6rem;right:0.6rem;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid rgba(255,255,255,0.15);border-radius:6px;background:rgba(255,255,255,0.07);color:rgba(255,255,255,0.5);cursor:pointer;transition:background 0.15s,color 0.15s;z-index:1}.demo-theme-toggle:hover{background:rgba(255,255,255,0.15);color:rgba(255,255,255,0.9)}.demo-preview.is-light .demo-theme-toggle{border-color:rgba(0,0,0,0.15);background:rgba(0,0,0,0.05);color:rgba(0,0,0,0.4)}.demo-preview.is-light .demo-theme-toggle:hover{background:rgba(0,0,0,0.1);color:rgba(0,0,0,0.8)}.demo-theme-toggle__light{display:none}.demo-preview.is-light .demo-theme-toggle__dark{display:none}.demo-preview.is-light .demo-theme-toggle__light{display:block}.demo-code pre.code-block{margin:0;border-radius:0;border:none;border-top:1px solid var(--border)}.demo-code .code-filename{border-radius:0;border:none;border-top:1px solid var(--border)}.prompt-group{margin-bottom:3rem}.prompt-group-title{font-size:0.78rem;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;color:var(--muted);margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:1px solid var(--border-subtle)}.prompt-grid{display:flex;flex-direction:column;gap:0.75rem}.prompt-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem 1.5rem}.prompt-tag{font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:var(--accent);margin-bottom:0.65rem}.prompt-text{font-size:0.93rem;color:var(--text);line-height:1.65;border-left:3px solid var(--accent);padding-left:1rem;margin:0 0 0.85rem;font-style:italic}.prompt-produces{font-size:0.82rem;color:var(--muted);line-height:1.6}.prompt-produces code{font-family:var(--mono);font-size:0.8rem;background:var(--surface-2);border:1px solid var(--border);border-radius:3px;padding:0.1em 0.35em;color:var(--tok-str);font-style:normal}.docs-sidebar{position:fixed;top:0;left:0;width:var(--sidebar-w);height:100vh;overflow-y:auto;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:100;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.docs-sidebar::-webkit-scrollbar{width:4px}.docs-sidebar::-webkit-scrollbar-track{background:transparent}.docs-sidebar::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}.sidebar-logo{display:flex;align-items:center;justify-content:space-between;padding:1.25rem 1.25rem 1rem;border-bottom:1px solid var(--border);flex-shrink:0}.logo-link{display:flex;align-items:center;gap:0.5rem;color:var(--text);font-weight:700;font-size:1rem;text-decoration:none}.logo-link:hover{color:var(--accent)}.logo-name{color:var(--text)}.version-badge{font-size:0.7rem;font-weight:600;color:var(--accent);background:var(--accent-dim);border:1px solid rgba(155,141,255,0.2);border-radius:4px;padding:0.15rem 0.45rem;font-family:var(--mono)}.sidebar-nav{padding:0.75rem 0 2rem;flex:1}.nav-section{padding:0.5rem 0}.nav-section+.nav-section{border-top:1px solid var(--border-subtle);margin-top:0.25rem;padding-top:0.75rem}.nav-section-title{font-size:0.68rem;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:var(--muted-2);padding:0.25rem 1.25rem 0.5rem}.nav-link{display:block;padding:0.35rem 1.25rem;font-size:0.875rem;color:var(--muted);border-radius:0;transition:color 0.1s,background 0.1s;position:relative;border-left:2px solid transparent}.nav-link:hover{color:var(--text);background:var(--surface-2)}.nav-link.active{color:var(--accent);border-left-color:var(--accent);background:var(--accent-dim)}.docs-main{margin-left:var(--sidebar-w);flex:1;min-width:0;display:flex;flex-direction:column}.docs-header{height:var(--header-h);border-bottom:1px solid var(--border-subtle);display:flex;align-items:center;justify-content:space-between;padding:0 2rem;position:sticky;top:0;background:rgba(13,13,16,0.9);backdrop-filter:blur(8px);z-index:10}.mobile-menu-btn{display:none;background:none;border:none;color:var(--muted);cursor:pointer;padding:0.25rem}.mobile-menu-btn:hover{color:var(--text)}.header-logo-mobile{display:none;color:var(--text)}.header-github{display:flex;align-items:center;gap:0.4rem;font-size:0.82rem;color:var(--muted);transition:color 0.1s;margin-left:auto}.header-github:hover{color:var(--text)}.docs-content{flex:1;padding:3rem 3.5rem 5rem;max-width:calc(var(--content-w)+7rem)}.doc-h1{font-size:2.25rem;font-weight:800;letter-spacing:-0.03em;line-height:1.15;margin-bottom:1rem;color:var(--text)}.doc-lead{font-size:1.1rem;color:var(--muted);line-height:1.65;max-width:600px;margin-bottom:2.5rem}.doc-h2{font-size:1.35rem;font-weight:700;letter-spacing:-0.02em;margin:2.5rem 0 0.75rem;color:var(--text);padding-top:1rem;border-top:1px solid var(--border-subtle)}.doc-h2:first-of-type{border-top:none;margin-top:0}.doc-h3{font-size:1.05rem;font-weight:600;margin:1.75rem 0 0.6rem;color:var(--text)}.definition-list{display:flex;flex-direction:column;gap:0.75rem;margin:1rem 0}.definition-list dt{margin:0;font-weight:600}.definition-list dd{margin:0.2rem 0 0 0;color:var(--muted);font-size:0.9rem;line-height:1.55;padding-bottom:0.75rem;border-bottom:1px solid var(--border-subtle)}.definition-list dd:last-of-type{border-bottom:none;padding-bottom:0}.docs-content p:not([class*="ui-"]){margin-bottom:0.9rem;line-height:1.7;color:var(--text)}.docs-content p+p{margin-top:-0.1rem}.docs-content ul:not([class*="ui-"]),.docs-content ol{padding-left:1.5rem;margin-bottom:1rem}.docs-content li:not([class*="ui-"]){margin-bottom:0.35rem;line-height:1.65;color:var(--text)}.docs-content strong:not([class*="ui-"]){font-weight:600;color:#fff}.docs-content a:not(.ui-btn):not(.ui-nav-link):not(.ui-nav-logo):not(.ui-app-badge){color:var(--accent);text-decoration:underline;text-underline-offset:2px}.docs-content a:not(.ui-btn):not(.ui-nav-link):not(.ui-nav-logo):not(.ui-app-badge):hover{color:var(--accent-hover)}.docs-content .ui-theme-light p,.docs-content .ui-theme-light li,.docs-content .ui-theme-light strong{color:var(--ui-text)}.docs-content code:not(pre code){font-family:var(--mono);font-size:0.82em;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;padding:0.1em 0.35em;color:var(--accent-hover)}.heading-anchor{color:inherit;text-decoration:none}.heading-anchor:hover{color:var(--accent)}.code-filename{background:var(--surface-2);border:1px solid var(--border);border-bottom:none;border-radius:var(--radius) var(--radius) 0 0;padding:0.4rem 1rem;font-family:var(--mono);font-size:0.75rem;color:var(--muted)}.code-filename+.code-block{border-radius:0 0 var(--radius) var(--radius);margin-top:0}.code-block{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem 1.5rem;overflow-x:auto;font-family:var(--mono);font-size:0.9rem;line-height:1.75;margin:1.25rem 0;tab-size:2;white-space:pre;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.code-block::-webkit-scrollbar{height:4px}.code-block::-webkit-scrollbar-track{background:transparent}.code-block::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}.tok-kw{color:var(--tok-kw)}.tok-str{color:var(--tok-str)}.tok-cmt{color:var(--tok-cmt);font-style:italic}.tok-num{color:var(--tok-num)}.tok-fn{color:var(--tok-fn)}.tok-op{color:var(--tok-op)}.tok-punct{color:var(--tok-punct)}.table-wrap{overflow-x:auto;margin:1.25rem 0;border:1px solid var(--border);border-radius:var(--radius)}table{width:100%;border-collapse:collapse;font-size:0.875rem}thead{background:var(--surface-2)}th{font-weight:600;font-size:0.78rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);padding:0.65rem 1rem;text-align:left;border-bottom:1px solid var(--border)}td{padding:0.65rem 1rem;border-bottom:1px solid var(--border-subtle);vertical-align:top;line-height:1.5}tr:last-child td{border-bottom:none}tr:hover td{background:var(--surface-2)}.callout{display:flex;gap:0.75rem;padding:1rem 1.25rem;border-radius:var(--radius);margin:1.25rem 0;border-left:3px solid;font-size:0.9rem;line-height:1.6}.callout-note{background:rgba(130,170,255,0.07);border-color:rgba(130,170,255,0.5)}.callout-warning{background:rgba(255,107,107,0.07);border-color:rgba(255,107,107,0.5)}.callout-tip{background:rgba(61,214,140,0.07);border-color:rgba(61,214,140,0.5)}.callout-note .callout-icon{color:#82aaff}.callout-warning .callout-icon{color:var(--red)}.callout-tip .callout-icon{color:var(--green)}.callout-icon{font-size:1rem;flex-shrink:0;margin-top:0.1rem}.callout-body p:last-child{margin-bottom:0}.doc-prev-next{margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border)}.prev-next-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.prev-next-link{display:flex;flex-direction:column;gap:0.2rem;padding:1rem 1.25rem;border:1px solid var(--border);border-radius:var(--radius);color:inherit;text-decoration:none;transition:border-color 0.15s,background 0.15s}.prev-next-link:hover{border-color:var(--accent);background:var(--accent-dim);color:inherit}.next-link{text-align:right}.prev-next-label{font-size:0.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em}.prev-next-title{font-size:0.95rem;font-weight:600;color:var(--accent)}@media (max-width:768px){.docs-sidebar{transform:translateX(-100%);transition:transform 0.25s ease}.docs-sidebar.open{transform:translateX(0);box-shadow:4px 0 24px rgba(0,0,0,0.4)}.docs-main{margin-left:0}.mobile-menu-btn,.header-logo-mobile{display:flex}.docs-content{padding:2rem 1.25rem 4rem}.doc-h1{font-size:1.75rem}.prev-next-grid{grid-template-columns:1fr}.hero{padding:4rem 1.25rem 3rem}.home-nav{padding:1rem 1.25rem}.home-nav-links{gap:1rem}.home-stats{padding:2rem 1rem;gap:0}.home-stat{padding:0.75rem 1.5rem}.home-stat-value{font-size:1.5rem}.home-stat-divider{display:none}.how{padding:3.5rem 1.25rem}.how-steps{flex-direction:column;align-items:center;gap:1.5rem}.how-connector{width:1px;height:2rem}.how-step{max-width:100%}.versus{padding:3.5rem 1.25rem}.versus-title{font-size:1.4rem}.usp-blocks{padding:2.5rem 1.25rem}.usp-block{grid-template-columns:1fr;gap:1.5rem}.usp-block-alt{direction:ltr}.home-cta{padding:3.5rem 1.25rem}.home-cta h2{font-size:1.5rem}}.icon-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:0.5rem;margin-bottom:1rem}.icon-grid-item{display:flex;flex-direction:column;align-items:center;gap:0.5rem;padding:1rem 0.5rem;border:1px solid var(--border-subtle);border-radius:8px;background:var(--surface);cursor:default;transition:border-color 0.15s,background 0.15s}.icon-grid-item:hover{border-color:var(--border);background:var(--surface-2)}.icon-grid-preview{color:var(--text);line-height:1}.icon-grid-name{font-family:var(--mono,monospace);font-size:0.6rem;color:var(--muted);text-align:center;word-break:break-all;line-height:1.4}
|
|
@@ -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};
|