@invisibleloop/pulse 0.20.11 → 0.20.20
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/package.json +6 -5
- package/public/.pulse-ui-version +1 -1
- package/scripts/build.js +26 -0
- package/scripts/release-version.js +17 -8
- package/scripts/strip-server.test.js +60 -0
- package/src/agent/checklist.md +8 -12
- package/src/agent/commands/pulse-start.md +2 -0
- package/src/agent/commands/verify.md +30 -8
- package/src/agent/guide-components.md +7 -25
- package/src/agent/guide-routing.md +0 -12
- package/src/agent/guide-server.md +102 -1
- package/src/agent/guide-spec.md +8 -1
- package/src/agent/guide-styles.md +23 -0
- package/src/agent/identity.md +9 -0
- package/src/agent/workflow.md +4 -23
- package/src/cli/check-bundles.js +113 -0
- package/src/cli/check-bundles.test.js +137 -0
- package/src/cli/cli.test.js +105 -0
- package/src/cli/dev.js +48 -3
- package/src/cli/diagnose.js +105 -0
- package/src/cli/diagnose.test.js +124 -0
- package/src/cli/discover.js +9 -2
- package/src/cli/index.js +171 -9
- package/src/cli/process-utils.js +57 -0
- package/src/cli/process-utils.test.js +114 -0
- package/src/cli/review.js +391 -0
- package/src/cli/review.test.js +143 -0
- package/src/cli/scaffold.js +9 -4
- package/src/cli/stamp.js +102 -0
- package/src/cli/stamp.test.js +149 -0
- package/src/cli/start.js +15 -2
- package/src/cli/validate.js +184 -0
- package/src/cli/validate.test.js +153 -0
- package/src/mcp/mcp.test.js +261 -30
- package/src/mcp/server.js +207 -515
- package/src/mcp/validate-worker.js +18 -1
- package/src/runtime/index.js +144 -11
- package/src/runtime/runtime.test.js +199 -0
- package/src/runtime/ssr.js +13 -6
- package/src/runtime/ssr.test.js +31 -0
- package/src/server/index.js +162 -9
- package/src/server/server.test.js +225 -2
- package/src/spec/schema.js +1 -1
- package/src/ui/index.js +0 -5
- package/src/ui/ui.test.js +0 -75
- package/types/ui.d.ts +0 -21
- package/.claude/commands/build-page.md +0 -63
- package/.claude/commands/new-doc-page.md +0 -45
- package/.claude/commands/pulse-start.md +0 -11
- package/.claude/commands/pulse-stop.md +0 -5
- package/.claude/commands/verify.md +0 -118
- package/.claude/pulse-checklist.md +0 -189
- package/.github/workflows/ci.yml +0 -25
- package/.github/workflows/publish.yml +0 -45
- package/.pulse/load-reports/home/1773432711417.json +0 -22
- package/.pulse-verified +0 -1
- package/AGENTS.md +0 -697
- package/CLAUDE.md +0 -697
- package/README.md +0 -95
- package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
- package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
- package/benchmark/public/dist/manifest.json +0 -5
- package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
- package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
- package/benchmark/results/2026-03-27-09-20.json +0 -188
- package/benchmark/results/2026-03-27-09-21.json +0 -188
- package/benchmark/results/2026-03-27-09-29.json +0 -188
- package/benchmark/results/2026-03-27-10-14.json +0 -188
- package/benchmark/results/2026-03-27-10-18.json +0 -232
- package/benchmark/src/pages/counter.js +0 -37
- package/benchmark/src/pages/home.js +0 -21
- package/docs/.claude/commands/pulse-build.md +0 -7
- package/docs/.claude/commands/pulse-dev.md +0 -7
- package/docs/.claude/commands/pulse-start.md +0 -9
- package/docs/.claude/commands/pulse-stop.md +0 -5
- package/docs/.claude/commands/verify.md +0 -84
- package/docs/.claude/coverage-check.js +0 -196
- package/docs/.claude/pulse-checklist.md +0 -189
- package/docs/public/.pulse-ui-version +0 -1
- package/docs/public/andypixel.css +0 -237
- package/docs/public/bookworms.css +0 -422
- package/docs/public/coutumes.css +0 -561
- package/docs/public/docs.css +0 -2388
- package/docs/public/favicon.svg +0 -4
- package/docs/public/home-brut.css +0 -982
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/img/heic1501a.jpg +0 -0
- package/docs/public/img/placeholder-landscape.svg +0 -18
- package/docs/public/img/placeholder-square.svg +0 -14
- package/docs/public/img/placeholder-tall.svg +0 -22
- package/docs/public/journey.css +0 -528
- package/docs/public/menu.js +0 -327
- package/docs/public/pulse-ui.css +0 -3028
- package/docs/public/pulse-ui.js +0 -257
- package/docs/public/retrowheels.css +0 -425
- package/docs/public/shop.css +0 -269
- package/docs/public/sparkys.css +0 -382
- package/docs/public/suzukibento.css +0 -517
- package/docs/public/swish.css +0 -394
- package/docs/public/theme.css +0 -52
- package/docs/public/themes/lumio.css +0 -50
- package/docs/public/themes/observatory.css +0 -104
- package/docs/server.js +0 -161
- package/docs/src/lib/component-page.js +0 -49
- package/docs/src/lib/highlight.js +0 -2
- package/docs/src/lib/layout.js +0 -190
- package/docs/src/lib/metrics-store.js +0 -6
- package/docs/src/lib/nav.js +0 -171
- package/docs/src/lib/stats.js +0 -81
- package/docs/src/pages/accessibility.js +0 -157
- package/docs/src/pages/actions.js +0 -194
- package/docs/src/pages/auth.js +0 -176
- package/docs/src/pages/caching.js +0 -95
- package/docs/src/pages/components/accordion.js +0 -48
- package/docs/src/pages/components/alert.js +0 -35
- package/docs/src/pages/components/app-badge.js +0 -41
- package/docs/src/pages/components/avatar.js +0 -35
- package/docs/src/pages/components/badge.js +0 -36
- package/docs/src/pages/components/breadcrumbs.js +0 -94
- package/docs/src/pages/components/button.js +0 -84
- package/docs/src/pages/components/card.js +0 -225
- package/docs/src/pages/components/charts.js +0 -283
- package/docs/src/pages/components/checkbox.js +0 -129
- package/docs/src/pages/components/cluster.js +0 -47
- package/docs/src/pages/components/container.js +0 -40
- package/docs/src/pages/components/cta.js +0 -54
- package/docs/src/pages/components/divider.js +0 -37
- package/docs/src/pages/components/empty.js +0 -36
- package/docs/src/pages/components/feature.js +0 -60
- package/docs/src/pages/components/fieldset.js +0 -65
- package/docs/src/pages/components/fileupload.js +0 -128
- package/docs/src/pages/components/footer.js +0 -58
- package/docs/src/pages/components/grid.js +0 -169
- package/docs/src/pages/components/heading.js +0 -107
- package/docs/src/pages/components/hero.js +0 -123
- package/docs/src/pages/components/icons.js +0 -410
- package/docs/src/pages/components/image.js +0 -71
- package/docs/src/pages/components/input.js +0 -51
- package/docs/src/pages/components/media.js +0 -51
- package/docs/src/pages/components/modal.js +0 -111
- package/docs/src/pages/components/nav.js +0 -136
- package/docs/src/pages/components/phone-frame.js +0 -74
- package/docs/src/pages/components/progress.js +0 -102
- package/docs/src/pages/components/prose.js +0 -111
- package/docs/src/pages/components/pullquote.js +0 -71
- package/docs/src/pages/components/radio.js +0 -194
- package/docs/src/pages/components/rating.js +0 -106
- package/docs/src/pages/components/search.js +0 -61
- package/docs/src/pages/components/section.js +0 -98
- package/docs/src/pages/components/segmented.js +0 -121
- package/docs/src/pages/components/select.js +0 -45
- package/docs/src/pages/components/slider.js +0 -115
- package/docs/src/pages/components/spinner.js +0 -73
- package/docs/src/pages/components/stack.js +0 -48
- package/docs/src/pages/components/stat.js +0 -55
- package/docs/src/pages/components/stepper.js +0 -66
- package/docs/src/pages/components/table.js +0 -45
- package/docs/src/pages/components/textarea.js +0 -31
- package/docs/src/pages/components/toggle.js +0 -84
- package/docs/src/pages/components/tooltip.js +0 -48
- package/docs/src/pages/components.js +0 -200
- package/docs/src/pages/config.js +0 -193
- package/docs/src/pages/constraints.js +0 -100
- package/docs/src/pages/deploy.js +0 -233
- package/docs/src/pages/error-pages.js +0 -99
- package/docs/src/pages/extending.js +0 -198
- package/docs/src/pages/faq.js +0 -99
- package/docs/src/pages/forms.js +0 -129
- package/docs/src/pages/getting-started.js +0 -147
- package/docs/src/pages/guard.js +0 -120
- package/docs/src/pages/home.js +0 -350
- package/docs/src/pages/how-it-works.js +0 -244
- package/docs/src/pages/hydration.js +0 -108
- package/docs/src/pages/images.js +0 -120
- package/docs/src/pages/markdown.js +0 -156
- package/docs/src/pages/meta.js +0 -193
- package/docs/src/pages/mutations.js +0 -107
- package/docs/src/pages/navigation.js +0 -85
- package/docs/src/pages/performance.js +0 -157
- package/docs/src/pages/persist.js +0 -89
- package/docs/src/pages/project-structure.js +0 -91
- package/docs/src/pages/prompt-examples.js +0 -186
- package/docs/src/pages/raw-responses.js +0 -122
- package/docs/src/pages/routing.js +0 -120
- package/docs/src/pages/server-api.js +0 -291
- package/docs/src/pages/server-data.js +0 -203
- package/docs/src/pages/slash-commands.js +0 -55
- package/docs/src/pages/spec.js +0 -207
- package/docs/src/pages/state.js +0 -102
- package/docs/src/pages/store.js +0 -203
- package/docs/src/pages/streaming.js +0 -107
- package/docs/src/pages/stripe.js +0 -194
- package/docs/src/pages/supabase.js +0 -324
- package/docs/src/pages/templates/andypixel.js +0 -260
- package/docs/src/pages/templates/blog-post.js +0 -226
- package/docs/src/pages/templates/bookworms.js +0 -227
- package/docs/src/pages/templates/coutumes.js +0 -237
- package/docs/src/pages/templates/journey.js +0 -186
- package/docs/src/pages/templates/mobile-app.js +0 -371
- package/docs/src/pages/templates/retrowheels.js +0 -355
- package/docs/src/pages/templates/shop.js +0 -268
- package/docs/src/pages/templates/sparkys.js +0 -264
- package/docs/src/pages/templates/suzukibento.js +0 -200
- package/docs/src/pages/templates/swish.js +0 -313
- package/docs/src/pages/templates.js +0 -534
- package/docs/src/pages/testing.js +0 -199
- package/docs/src/pages/validation.js +0 -138
- package/examples/.claude/commands/pulse-build.md +0 -7
- package/examples/.claude/commands/pulse-dev.md +0 -7
- package/examples/.claude/commands/pulse-start.md +0 -9
- package/examples/.claude/commands/pulse-stop.md +0 -5
- package/examples/.claude/commands/verify.md +0 -84
- package/examples/.claude/coverage-check.js +0 -196
- package/examples/.claude/pulse-checklist.md +0 -189
- package/examples/andypixel.css +0 -237
- package/examples/andypixel.js +0 -258
- package/examples/bookworms.css +0 -422
- package/examples/bookworms.js +0 -225
- package/examples/contact.js +0 -166
- package/examples/counter.js +0 -94
- package/examples/dev.server.js +0 -110
- package/examples/examples.test.js +0 -394
- package/examples/journey.css +0 -528
- package/examples/journey.js +0 -186
- package/examples/pricing.js +0 -262
- package/examples/products.js +0 -190
- package/examples/public/.pulse-ui-version +0 -1
- package/examples/public/pulse-ui.css +0 -3028
- package/examples/public/pulse-ui.js +0 -257
- package/examples/quiz.js +0 -208
- package/examples/retrowheels.css +0 -425
- package/examples/retrowheels.js +0 -353
- package/examples/shared.js +0 -84
- package/examples/suzukibento.css +0 -513
- package/examples/suzukibento.js +0 -200
- package/examples/swish.css +0 -394
- package/examples/swish.js +0 -311
- package/examples/todos.js +0 -162
- package/src/ui/fieldset.js +0 -37
- package/src/ui/phone-frame.js +0 -40
- package/src/ui/search.js +0 -78
- package/src/ui/segmented.js +0 -50
- package/src/ui/tooltip.js +0 -28
package/docs/src/pages/deploy.js
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
|
|
2
|
-
import { prevNext } from '../lib/nav.js'
|
|
3
|
-
import { highlight } from '../lib/highlight.js'
|
|
4
|
-
|
|
5
|
-
const { prev, next } = prevNext('/deploy')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/deploy',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Deployment — Pulse Docs',
|
|
12
|
-
description: 'Deploy a Pulse app to a VPS, Docker, Fly.io, Railway, or Render.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/deploy',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Deployment')}
|
|
21
|
-
${lead('Pulse deploys as a single Node.js process. No adapters, no serverless wrapping, no separate static file server required. Build once, run anywhere Node 22+ runs. All guarantees — security headers, brotli compression, immutable asset caching — are active in production automatically.')}
|
|
22
|
-
|
|
23
|
-
${section('build', 'Build')}
|
|
24
|
-
<p>Run the production build before deploying:</p>
|
|
25
|
-
${codeBlock(highlight(`npm run build`, 'bash'))}
|
|
26
|
-
<p>This generates content-hashed bundles in <code>public/dist/</code> and writes <code>public/dist/manifest.json</code>. The server reads the manifest at startup to resolve hydration script paths.</p>
|
|
27
|
-
${callout('warning', 'Without a manifest, the server falls back to serving source files directly — no compression, no content-hashed filenames, and no <code>immutable</code> cache headers. Always run <code>npm run build</code> before deploying to production.')}
|
|
28
|
-
|
|
29
|
-
${section('files', 'What to deploy')}
|
|
30
|
-
${table(
|
|
31
|
-
['Include', 'Reason'],
|
|
32
|
-
[
|
|
33
|
-
['<code>src/</code>', 'Page specs — imported by the server at runtime'],
|
|
34
|
-
['<code>public/</code>', 'Static assets and built bundles (<code>public/dist/</code>)'],
|
|
35
|
-
['<code>server.js</code>', 'Entry point'],
|
|
36
|
-
['<code>pulse.config.js</code>', 'Server config'],
|
|
37
|
-
['<code>package.json</code> + <code>node_modules/</code>', 'Runtime dependencies'],
|
|
38
|
-
]
|
|
39
|
-
)}
|
|
40
|
-
${table(
|
|
41
|
-
['Exclude', 'Reason'],
|
|
42
|
-
[
|
|
43
|
-
['<code>.claude/</code>', 'AI agent config — not needed at runtime'],
|
|
44
|
-
['<code>.pulse/</code>', 'Local report data — not needed at runtime'],
|
|
45
|
-
]
|
|
46
|
-
)}
|
|
47
|
-
|
|
48
|
-
${section('env-vars', 'Environment variables')}
|
|
49
|
-
${table(
|
|
50
|
-
['Variable', 'Default', 'Description'],
|
|
51
|
-
[
|
|
52
|
-
['<code>NODE_ENV</code>', '<code>development</code>', 'Set to <code>production</code> to enable HSTS headers and production cache behaviour.'],
|
|
53
|
-
['<code>PORT</code>', 'Value in <code>pulse.config.js</code> (default <code>3000</code>)', 'Override the listening port. Most PaaS platforms set this automatically.'],
|
|
54
|
-
]
|
|
55
|
-
)}
|
|
56
|
-
${codeBlock(highlight(`NODE_ENV=production pulse start`, 'bash'))}
|
|
57
|
-
|
|
58
|
-
${section('pm2', 'VPS with PM2')}
|
|
59
|
-
<p>PM2 keeps the process alive, restarts it on crash, and manages logs.</p>
|
|
60
|
-
${codeBlock(highlight(`# Install PM2 globally
|
|
61
|
-
npm install -g pm2
|
|
62
|
-
|
|
63
|
-
# Start the app
|
|
64
|
-
NODE_ENV=production pm2 start server.js --name myapp
|
|
65
|
-
|
|
66
|
-
# Persist across reboots
|
|
67
|
-
pm2 save
|
|
68
|
-
pm2 startup
|
|
69
|
-
|
|
70
|
-
# Zero-downtime reload after a deploy
|
|
71
|
-
pm2 reload myapp`, 'bash'))}
|
|
72
|
-
<p>For repeatable deployments, check an <code>ecosystem.config.cjs</code> into version control:</p>
|
|
73
|
-
${codeBlock(highlight(`// ecosystem.config.cjs
|
|
74
|
-
module.exports = {
|
|
75
|
-
apps: [{
|
|
76
|
-
name: 'myapp',
|
|
77
|
-
script: 'server.js',
|
|
78
|
-
env_production: {
|
|
79
|
-
NODE_ENV: 'production',
|
|
80
|
-
PORT: 3000,
|
|
81
|
-
},
|
|
82
|
-
}],
|
|
83
|
-
}`, 'js'))}
|
|
84
|
-
${codeBlock(highlight(`pm2 start ecosystem.config.cjs --env production`, 'bash'))}
|
|
85
|
-
|
|
86
|
-
${section('docker', 'Docker')}
|
|
87
|
-
<p>A two-stage build keeps the image small — build tools stay in the first stage.</p>
|
|
88
|
-
${codeBlock(highlight(`# ---- build stage ----
|
|
89
|
-
FROM node:22-alpine AS build
|
|
90
|
-
WORKDIR /app
|
|
91
|
-
COPY package*.json ./
|
|
92
|
-
RUN npm ci
|
|
93
|
-
COPY . .
|
|
94
|
-
RUN npx pulse build
|
|
95
|
-
|
|
96
|
-
# ---- runtime stage ----
|
|
97
|
-
FROM node:22-alpine
|
|
98
|
-
WORKDIR /app
|
|
99
|
-
ENV NODE_ENV=production
|
|
100
|
-
COPY package*.json ./
|
|
101
|
-
RUN npm ci --omit=dev
|
|
102
|
-
COPY --from=build /app/src ./src
|
|
103
|
-
COPY --from=build /app/public ./public
|
|
104
|
-
COPY --from=build /app/server.js ./server.js
|
|
105
|
-
COPY pulse.config.js ./
|
|
106
|
-
EXPOSE 3000
|
|
107
|
-
CMD ["node", "server.js"]`, 'bash'))}
|
|
108
|
-
${codeBlock(highlight(`docker build -t myapp .
|
|
109
|
-
docker run -p 3000:3000 --env NODE_ENV=production myapp`, 'bash'))}
|
|
110
|
-
|
|
111
|
-
${section('fly', 'Fly.io')}
|
|
112
|
-
${codeBlock(highlight(`# fly.toml
|
|
113
|
-
app = 'myapp'
|
|
114
|
-
primary_region = 'lhr'
|
|
115
|
-
|
|
116
|
-
[env]
|
|
117
|
-
NODE_ENV = 'production'
|
|
118
|
-
|
|
119
|
-
[build]
|
|
120
|
-
[build.args]
|
|
121
|
-
NODE_VERSION = '22'
|
|
122
|
-
|
|
123
|
-
[deploy]
|
|
124
|
-
release_command = 'npx pulse build'
|
|
125
|
-
|
|
126
|
-
[http_service]
|
|
127
|
-
internal_port = 3000
|
|
128
|
-
force_https = true
|
|
129
|
-
auto_stop_machines = 'stop'
|
|
130
|
-
auto_start_machines = true
|
|
131
|
-
|
|
132
|
-
[[vm]]
|
|
133
|
-
memory = '256mb'
|
|
134
|
-
cpu_kind = 'shared'
|
|
135
|
-
cpus = 1`, 'bash'))}
|
|
136
|
-
${codeBlock(highlight(`# First deploy
|
|
137
|
-
fly launch
|
|
138
|
-
|
|
139
|
-
# Subsequent deploys
|
|
140
|
-
fly deploy`, 'bash'))}
|
|
141
|
-
|
|
142
|
-
${section('railway', 'Railway')}
|
|
143
|
-
<p>Railway auto-detects Node apps. Add a <code>railway.json</code> to set the build and start commands:</p>
|
|
144
|
-
${codeBlock(highlight(`{
|
|
145
|
-
"$schema": "https://railway.app/railway.schema.json",
|
|
146
|
-
"build": {
|
|
147
|
-
"builder": "NIXPACKS",
|
|
148
|
-
"buildCommand": "npm run build"
|
|
149
|
-
},
|
|
150
|
-
"deploy": {
|
|
151
|
-
"startCommand": "NODE_ENV=production node server.js",
|
|
152
|
-
"healthcheckPath": "/",
|
|
153
|
-
"restartPolicyType": "ON_FAILURE"
|
|
154
|
-
}
|
|
155
|
-
}`, 'js'))}
|
|
156
|
-
|
|
157
|
-
${section('render', 'Render')}
|
|
158
|
-
${table(
|
|
159
|
-
['Setting', 'Value'],
|
|
160
|
-
[
|
|
161
|
-
['Environment', 'Node'],
|
|
162
|
-
['Build command', '<code>npm install && npm run build</code>'],
|
|
163
|
-
['Start command', '<code>NODE_ENV=production node server.js</code>'],
|
|
164
|
-
['Node version', '<code>22.x</code>'],
|
|
165
|
-
]
|
|
166
|
-
)}
|
|
167
|
-
<p>Set <code>NODE_ENV=production</code> in the Render environment variables dashboard.</p>
|
|
168
|
-
|
|
169
|
-
${section('serverless', 'Vercel, Cloudflare, and edge platforms')}
|
|
170
|
-
<p>These platforms each have multiple products with very different runtimes — the compatibility story varies significantly between them.</p>
|
|
171
|
-
|
|
172
|
-
${section('vercel', 'Vercel')}
|
|
173
|
-
<p>Vercel has two distinct runtimes:</p>
|
|
174
|
-
${table(
|
|
175
|
-
['Product', 'Runtime', 'Pulse compatible?'],
|
|
176
|
-
[
|
|
177
|
-
['<strong>Functions</strong> (Node.js)', 'Full Node.js — same built-ins as a VPS', 'Partially — see below'],
|
|
178
|
-
['<strong>Edge Functions</strong>', 'V8 isolates (no Node built-ins)', 'No'],
|
|
179
|
-
]
|
|
180
|
-
)}
|
|
181
|
-
<p><strong>Vercel Functions (Node.js)</strong> can run Pulse with some differences in behaviour:</p>
|
|
182
|
-
${table(
|
|
183
|
-
['Feature', 'Behaviour on Vercel Functions'],
|
|
184
|
-
[
|
|
185
|
-
['<code>serverTtl</code> cache', 'Works within a warm instance, but cold starts reset it. Not reliable for expensive queries.'],
|
|
186
|
-
['Streaming SSR', 'Vercel Functions support streaming responses, but require explicit configuration via <code>supportsResponseStreaming</code>.'],
|
|
187
|
-
['Static files', 'Vercel serves <code>public/</code> automatically via its CDN — Pulse\'s static file serving is bypassed.'],
|
|
188
|
-
['Security headers', 'Work as normal — Pulse adds them to every response.'],
|
|
189
|
-
]
|
|
190
|
-
)}
|
|
191
|
-
${callout('warning', 'Vercel Functions are not a tested or officially supported deployment target for Pulse. The adapter pattern (exporting a request handler rather than starting a server) is not yet documented. Railway, Render, or Fly.io are simpler choices with no adaptation required.')}
|
|
192
|
-
|
|
193
|
-
${section('cloudflare', 'Cloudflare')}
|
|
194
|
-
${table(
|
|
195
|
-
['Product', 'Runtime', 'Pulse compatible?'],
|
|
196
|
-
[
|
|
197
|
-
['<strong>Workers</strong>', 'V8 isolates — no <code>node:http</code>, <code>node:fs</code>, <code>node:zlib</code>', 'No'],
|
|
198
|
-
['<strong>Pages Functions</strong>', 'Same V8 isolate runtime as Workers', 'No'],
|
|
199
|
-
['<strong>CDN / proxy</strong>', 'Sits in front of your origin server', 'Yes — works great with Fly.io or a VPS behind it'],
|
|
200
|
-
]
|
|
201
|
-
)}
|
|
202
|
-
${callout('tip', 'The recommended pattern for edge performance: deploy Pulse to <strong>Fly.io</strong> (which runs real VMs in many regions) and put <strong>Cloudflare as a CDN/proxy</strong> in front of it. Static assets and cached HTML are served from Cloudflare\'s edge; dynamic requests are proxied to the nearest Fly VM.')}
|
|
203
|
-
|
|
204
|
-
${section('https', 'HTTPS and reverse proxy')}
|
|
205
|
-
<p>Pulse detects TLS automatically. When a request arrives with an <code>x-forwarded-proto: https</code> header or over a direct TLS socket, <code>Strict-Transport-Security: max-age=31536000; includeSubDomains</code> is added to the response. All four platforms above forward this header — no Pulse config is needed.</p>
|
|
206
|
-
<p>If running behind nginx for TLS termination:</p>
|
|
207
|
-
${codeBlock(highlight(`# nginx — TLS termination, proxy to Pulse
|
|
208
|
-
server {
|
|
209
|
-
listen 443 ssl;
|
|
210
|
-
server_name myapp.com;
|
|
211
|
-
|
|
212
|
-
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
|
|
213
|
-
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
|
|
214
|
-
|
|
215
|
-
location / {
|
|
216
|
-
proxy_pass http://localhost:3000;
|
|
217
|
-
proxy_http_version 1.1;
|
|
218
|
-
proxy_set_header Host $host;
|
|
219
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
220
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
# Redirect HTTP to HTTPS
|
|
225
|
-
server {
|
|
226
|
-
listen 80;
|
|
227
|
-
server_name myapp.com;
|
|
228
|
-
return 301 https://$host$request_uri;
|
|
229
|
-
}`, 'bash'))}
|
|
230
|
-
${callout('tip', 'Use <a href="https://certbot.eff.org">Certbot</a> to obtain and auto-renew a free Let\'s Encrypt certificate: <code>certbot --nginx -d myapp.com</code>.')}
|
|
231
|
-
`,
|
|
232
|
-
}),
|
|
233
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
|
|
2
|
-
import { prevNext } from '../lib/nav.js'
|
|
3
|
-
import { highlight } from '../lib/highlight.js'
|
|
4
|
-
|
|
5
|
-
const { prev, next } = prevNext('/error-pages')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/error-pages',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Error Pages — Pulse Docs',
|
|
12
|
-
description: 'Custom 404 pages in Pulse. A spec with route "*" renders through the normal pipeline with status 404 whenever no route matches.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/error-pages',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Error Pages')}
|
|
21
|
-
${lead('A spec with <code>route: \'*\'</code> is your site\'s custom 404 page. It renders through the normal pipeline — layout, styles, components, hydration — with status <strong>404</strong> whenever no route matches. Without one, visitors to a bad URL get the framework\'s unbranded default.')}
|
|
22
|
-
|
|
23
|
-
${section('not-found', 'The custom 404 page')}
|
|
24
|
-
<p>Create a spec like any other page, with <code>'*'</code> as the route. Use the shared <code>layout()</code> so the page carries the site's nav and footer:</p>
|
|
25
|
-
${codeBlock(highlight(`import { layout } from '../components/layout.js'
|
|
26
|
-
|
|
27
|
-
export default {
|
|
28
|
-
route: '*',
|
|
29
|
-
meta: {
|
|
30
|
-
title: 'Page not found',
|
|
31
|
-
styles: ['/pulse-ui.css', '/theme.css', '/app.css'],
|
|
32
|
-
},
|
|
33
|
-
view: () => layout({
|
|
34
|
-
content: \`
|
|
35
|
-
<h1>Page not found</h1>
|
|
36
|
-
<p>That page doesn't exist. <a href="/">Back home</a></p>
|
|
37
|
-
\`,
|
|
38
|
-
}),
|
|
39
|
-
}`, 'js', ), 'src/pages/not-found.js')}
|
|
40
|
-
<p>The filename doesn't matter — <code>not-found.js</code> is just a readable convention. The <code>route: '*'</code> property is what registers it as the fallback.</p>
|
|
41
|
-
|
|
42
|
-
${callout('tip', 'Every site should have one. The framework\'s default 404 is a bare unstyled page with no navigation — a dead end for visitors and a missed signal for search engines crawling old URLs.')}
|
|
43
|
-
|
|
44
|
-
${section('behaviour', 'How it behaves')}
|
|
45
|
-
${table(
|
|
46
|
-
['Aspect', 'Behaviour'],
|
|
47
|
-
[
|
|
48
|
-
['Status code', 'Always <strong>404</strong> — correct for SEO; crawlers de-index dead URLs'],
|
|
49
|
-
['Pipeline', 'Full normal render: <code>meta</code>, styles, components, hydration if it has mutations/actions'],
|
|
50
|
-
['Validation', 'Validated at startup like every spec — a broken 404 page fails fast'],
|
|
51
|
-
['Matching', 'Only when no other route matches. It is a fallback, not a route — it never shadows real pages'],
|
|
52
|
-
['Client-side navigation', 'A bad link falls back to a full page load, which renders the custom 404'],
|
|
53
|
-
['Caching', '404 responses are never stored in the in-process page cache'],
|
|
54
|
-
]
|
|
55
|
-
)}
|
|
56
|
-
|
|
57
|
-
${section('useful-404', 'Making the 404 useful')}
|
|
58
|
-
<p>The best 404 pages help the visitor recover. Since the page is a normal spec, it can use any component — search, popular links, even server data:</p>
|
|
59
|
-
${codeBlock(highlight(`export default {
|
|
60
|
-
route: '*',
|
|
61
|
-
meta: { title: 'Page not found', styles: ['/pulse-ui.css', '/theme.css', '/app.css'] },
|
|
62
|
-
server: {
|
|
63
|
-
popular: async () => getPopularPages(), // suggest somewhere to go
|
|
64
|
-
},
|
|
65
|
-
view: (state, server) => layout({
|
|
66
|
-
content: \`
|
|
67
|
-
<h1>Page not found</h1>
|
|
68
|
-
<p>Try one of these instead:</p>
|
|
69
|
-
\${list({ items: (server.popular ?? []).map(p => \`<a href="\${p.href}">\${p.title}</a>\`) })}
|
|
70
|
-
\`,
|
|
71
|
-
}),
|
|
72
|
-
}`, 'js'))}
|
|
73
|
-
|
|
74
|
-
${section('server-errors', '500s and other errors')}
|
|
75
|
-
<p>Server errors are handled by <code>createServer</code>'s <code>onError</code> option, not a spec — when a render throws, the spec pipeline itself may be the thing that is broken, so the handler works at the HTTP level:</p>
|
|
76
|
-
${codeBlock(highlight(`await createServer(pages, {
|
|
77
|
-
onError: (err, req, res) => {
|
|
78
|
-
console.error(err)
|
|
79
|
-
if (res.headersSent) return
|
|
80
|
-
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
81
|
-
res.end('<h1>Something went wrong</h1><p><a href="/">Back home</a></p>')
|
|
82
|
-
},
|
|
83
|
-
})`, 'js'))}
|
|
84
|
-
<p>For errors thrown inside a <em>view</em> specifically, prefer <code>onViewError</code> on the spec — it returns a 200 with fallback HTML instead of a 500, keeping the page alive when one piece of data is malformed.</p>
|
|
85
|
-
|
|
86
|
-
${callout('note', 'Three layers, three jobs: <code>route: \'*\'</code> for URLs that don\'t exist, <code>onViewError</code> for views that throw on bad data, <code>onError</code> for everything else that goes wrong server-side.')}
|
|
87
|
-
|
|
88
|
-
${section('reference', 'Reference')}
|
|
89
|
-
${table(
|
|
90
|
-
['Property', 'Where', 'Notes'],
|
|
91
|
-
[
|
|
92
|
-
['<code>route: \'*\'</code>', 'spec', 'Custom not-found page — rendered with status 404 when no route matches'],
|
|
93
|
-
['<code>onViewError</code>', 'spec', 'Fallback HTML when the view throws — returns 200'],
|
|
94
|
-
['<code>onError</code>', '<code>createServer</code> option', 'HTTP-level handler for unhandled server errors (500s)'],
|
|
95
|
-
]
|
|
96
|
-
)}
|
|
97
|
-
`,
|
|
98
|
-
}),
|
|
99
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
|
|
2
|
-
import { prevNext } from '../lib/nav.js'
|
|
3
|
-
import { highlight } from '../lib/highlight.js'
|
|
4
|
-
|
|
5
|
-
const { prev, next } = prevNext('/extending')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/extending',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Extending Pulse — Pulse Docs',
|
|
12
|
-
description: 'Escape hatches for features outside the Pulse spec — onRequest middleware, raw server access, WebSockets, SSE, and custom error handling.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/extending',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Extending Pulse')}
|
|
21
|
-
${lead('Pulse handles the standard request-response lifecycle through specs. For requirements outside that model — middleware, WebSockets, SSE, custom error pages — Pulse exposes deliberate integration points directly on the underlying Node.js server. There is no abstraction layer to fight through.')}
|
|
22
|
-
|
|
23
|
-
${section('onrequest', 'Request interception with onRequest')}
|
|
24
|
-
<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>
|
|
25
|
-
${codeBlock(highlight(`// server.js
|
|
26
|
-
import { createServer } from '@invisibleloop/pulse'
|
|
27
|
-
import home from './src/pages/home.js'
|
|
28
|
-
|
|
29
|
-
createServer([home], {
|
|
30
|
-
port: 3000,
|
|
31
|
-
onRequest(req, res) {
|
|
32
|
-
// Logging
|
|
33
|
-
console.log(req.method, req.url)
|
|
34
|
-
|
|
35
|
-
// Block a path entirely
|
|
36
|
-
if (req.url === '/internal') {
|
|
37
|
-
res.writeHead(403)
|
|
38
|
-
res.end('Forbidden')
|
|
39
|
-
return false // stops Pulse processing this request
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Custom header on every response
|
|
43
|
-
res.setHeader('X-App-Version', '1.0.0')
|
|
44
|
-
// returning nothing (or undefined) lets Pulse continue normally
|
|
45
|
-
},
|
|
46
|
-
})`, 'js'))}
|
|
47
|
-
${callout('note', '<code>onRequest</code> runs before routing, guard, and all server fetchers. Returning <code>false</code> gives full control of the response — Pulse steps aside entirely for that request.')}
|
|
48
|
-
<p>Common uses:</p>
|
|
49
|
-
${table(
|
|
50
|
-
['Use case', 'Approach'],
|
|
51
|
-
[
|
|
52
|
-
['Request logging', 'Log <code>req.method</code>, <code>req.url</code>, timing'],
|
|
53
|
-
['IP allowlisting', 'Check <code>req.socket.remoteAddress</code>, return <code>false</code> with 403 if blocked'],
|
|
54
|
-
['Rate limiting', 'Track request counts in a <code>Map</code>, return <code>false</code> with 429 when exceeded'],
|
|
55
|
-
['Custom response headers', 'Call <code>res.setHeader()</code> before returning'],
|
|
56
|
-
['Health check endpoint', 'Match <code>/healthz</code>, write <code>200 ok</code>, return <code>false</code>'],
|
|
57
|
-
]
|
|
58
|
-
)}
|
|
59
|
-
|
|
60
|
-
${section('raw-server', 'Accessing the raw server')}
|
|
61
|
-
<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>
|
|
62
|
-
${codeBlock(highlight(`const { server } = createServer([home], { port: 3000 })
|
|
63
|
-
|
|
64
|
-
// The server instance is available immediately after createServer() returns.
|
|
65
|
-
// It starts listening automatically — no need to call server.listen().
|
|
66
|
-
server.on('listening', () => {
|
|
67
|
-
console.log('ready')
|
|
68
|
-
})`, 'js'))}
|
|
69
|
-
|
|
70
|
-
${section('websockets', 'WebSockets')}
|
|
71
|
-
<p>Use the <code>upgrade</code> event on the server instance. The <code>ws</code> package handles the WebSocket handshake and framing.</p>
|
|
72
|
-
${codeBlock(highlight(`npm install ws`, 'bash'))}
|
|
73
|
-
${codeBlock(highlight(`// server.js
|
|
74
|
-
import { WebSocketServer } from 'ws'
|
|
75
|
-
import { createServer } from '@invisibleloop/pulse'
|
|
76
|
-
import home from './src/pages/home.js'
|
|
77
|
-
|
|
78
|
-
const { server } = createServer([home], { port: 3000 })
|
|
79
|
-
|
|
80
|
-
const wss = new WebSocketServer({ noServer: true })
|
|
81
|
-
|
|
82
|
-
server.on('upgrade', (req, socket, head) => {
|
|
83
|
-
if (req.url === '/ws') {
|
|
84
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
85
|
-
wss.emit('connection', ws, req)
|
|
86
|
-
})
|
|
87
|
-
} else {
|
|
88
|
-
socket.destroy()
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
wss.on('connection', (ws) => {
|
|
93
|
-
ws.send('connected')
|
|
94
|
-
ws.on('message', (msg) => ws.send(\`echo: \${msg}\`))
|
|
95
|
-
})`, 'js'))}
|
|
96
|
-
<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>
|
|
97
|
-
${codeBlock(highlight(`server: {
|
|
98
|
-
meta: async (ctx) => ({ nonce: ctx.nonce }),
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
view: (_state, server) => \`
|
|
102
|
-
<main id="main-content">
|
|
103
|
-
<p id="status">Connecting…</p>
|
|
104
|
-
<script nonce="\${server.meta.nonce}">
|
|
105
|
-
const ws = new WebSocket('ws://' + location.host + '/ws')
|
|
106
|
-
ws.onmessage = (e) => {
|
|
107
|
-
document.getElementById('status').textContent = e.data
|
|
108
|
-
}
|
|
109
|
-
</script>
|
|
110
|
-
</main>
|
|
111
|
-
\`,`, 'js'))}
|
|
112
|
-
|
|
113
|
-
${section('sse', 'Server-Sent Events')}
|
|
114
|
-
<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>
|
|
115
|
-
${codeBlock(highlight(`onRequest(req, res) {
|
|
116
|
-
if (req.url !== '/events') return // let Pulse handle everything else
|
|
117
|
-
|
|
118
|
-
res.writeHead(200, {
|
|
119
|
-
'Content-Type': 'text/event-stream',
|
|
120
|
-
'Cache-Control': 'no-cache',
|
|
121
|
-
'Connection': 'keep-alive',
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
// Send a keepalive comment every 15 seconds
|
|
125
|
-
const keepalive = setInterval(() => res.write(': keepalive\\n\\n'), 15000)
|
|
126
|
-
|
|
127
|
-
// Send an event
|
|
128
|
-
function send(event, data) {
|
|
129
|
-
res.write(\`event: \${event}\\ndata: \${JSON.stringify(data)}\\n\\n\`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
send('connected', { time: Date.now() })
|
|
133
|
-
|
|
134
|
-
req.on('close', () => clearInterval(keepalive))
|
|
135
|
-
|
|
136
|
-
return false
|
|
137
|
-
},`, 'js'))}
|
|
138
|
-
|
|
139
|
-
${section('error', 'Custom error handling')}
|
|
140
|
-
<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>
|
|
141
|
-
${codeBlock(highlight(`createServer([home], {
|
|
142
|
-
port: 3000,
|
|
143
|
-
|
|
144
|
-
onError(err, req, res) {
|
|
145
|
-
// Log to your error tracking service
|
|
146
|
-
console.error(err)
|
|
147
|
-
|
|
148
|
-
// Respond with a custom error page
|
|
149
|
-
res.writeHead(500, { 'Content-Type': 'text/html' })
|
|
150
|
-
res.end(\`
|
|
151
|
-
<!doctype html>
|
|
152
|
-
<html lang="en">
|
|
153
|
-
<head><title>Error</title></head>
|
|
154
|
-
<body>
|
|
155
|
-
<main id="main-content">
|
|
156
|
-
<h1>Something went wrong</h1>
|
|
157
|
-
<p>We've been notified and are looking into it.</p>
|
|
158
|
-
</main>
|
|
159
|
-
</body>
|
|
160
|
-
</html>
|
|
161
|
-
\`)
|
|
162
|
-
},
|
|
163
|
-
})`, 'js'))}
|
|
164
|
-
|
|
165
|
-
${section('client-js', 'Custom client-side JavaScript')}
|
|
166
|
-
<p>Pulse has no client-side JS of its own beyond the hydration runtime. For behaviour that genuinely needs to run in the browser — third-party widgets, analytics, canvas — 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>
|
|
167
|
-
${codeBlock(highlight(`server: {
|
|
168
|
-
meta: async (ctx) => ({ nonce: ctx.nonce }),
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
view: (_state, server) => \`
|
|
172
|
-
<main id="main-content">
|
|
173
|
-
<canvas id="chart" width="600" height="300"></canvas>
|
|
174
|
-
<script nonce="\${server.meta.nonce}">
|
|
175
|
-
const ctx = document.getElementById('chart').getContext('2d')
|
|
176
|
-
// draw directly with Canvas API — no library needed for simple charts
|
|
177
|
-
ctx.fillStyle = '#9b8dff'
|
|
178
|
-
ctx.fillRect(10, 10, 100, 80)
|
|
179
|
-
</script>
|
|
180
|
-
</main>
|
|
181
|
-
\`,`, 'js'))}
|
|
182
|
-
${callout('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.')}
|
|
183
|
-
|
|
184
|
-
${section('when', 'Choosing the right approach')}
|
|
185
|
-
${table(
|
|
186
|
-
['What you need', 'Reach for'],
|
|
187
|
-
[
|
|
188
|
-
['Middleware — logging, rate limiting, custom headers', '<code>onRequest</code>'],
|
|
189
|
-
['Non-HTML responses — JSON APIs, webhooks, RSS, sitemaps', 'Raw response spec (<code>contentType</code> + <code>render</code>)'],
|
|
190
|
-
['Real-time bidirectional communication', 'WebSockets via <code>server.on(\'upgrade\')</code>'],
|
|
191
|
-
['Server-pushed updates (read-only stream)', 'SSE via <code>onRequest</code>'],
|
|
192
|
-
['Custom error pages', '<code>onError</code>'],
|
|
193
|
-
['Browser-only behaviour', 'Inline <code><script nonce></code> in the view'],
|
|
194
|
-
]
|
|
195
|
-
)}
|
|
196
|
-
`,
|
|
197
|
-
}),
|
|
198
|
-
}
|
package/docs/src/pages/faq.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead } from '../lib/layout.js'
|
|
2
|
-
import { prevNext } from '../lib/nav.js'
|
|
3
|
-
import pkg from '../../../package.json' with { type: 'json' }
|
|
4
|
-
|
|
5
|
-
const { version } = pkg
|
|
6
|
-
|
|
7
|
-
const { prev, next } = prevNext('/faq')
|
|
8
|
-
|
|
9
|
-
function q(question, answer) {
|
|
10
|
-
return `
|
|
11
|
-
<div class="faq-item">
|
|
12
|
-
<h2 class="faq-q">${question}</h2>
|
|
13
|
-
<div class="faq-a">${answer}</div>
|
|
14
|
-
</div>`
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export default {
|
|
18
|
-
route: '/faq',
|
|
19
|
-
meta: {
|
|
20
|
-
theme: 'light',
|
|
21
|
-
title: 'FAQ — Pulse Docs',
|
|
22
|
-
description: 'Frequently asked questions about the Pulse framework.',
|
|
23
|
-
styles: ['/pulse-ui.css', '/theme.css', '/docs.css'],
|
|
24
|
-
},
|
|
25
|
-
view: () => renderLayout({
|
|
26
|
-
currentHref: '/faq',
|
|
27
|
-
prev,
|
|
28
|
-
next,
|
|
29
|
-
content: `
|
|
30
|
-
${h1('FAQ')}
|
|
31
|
-
${lead('Common questions about Pulse — what it is, what it isn\'t, and whether it\'s right for your project.')}
|
|
32
|
-
|
|
33
|
-
${q(
|
|
34
|
-
'Is Pulse ready for production?',
|
|
35
|
-
`<p>The architecture is production-quality — 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>
|
|
36
|
-
<p>That said, Pulse is v${version} 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>`
|
|
37
|
-
)}
|
|
38
|
-
|
|
39
|
-
${q(
|
|
40
|
-
'Why plain JS objects instead of JSX or components?',
|
|
41
|
-
`<p>JSX and component trees are designed for incremental DOM updates — 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>
|
|
42
|
-
<p>Plain JS objects are also unambiguous for AI agents. There is no syntax to learn, no component abstraction to navigate — just a JS object with well-defined keys. An agent can read, write, and validate a spec without understanding any framework-specific idioms.</p>`
|
|
43
|
-
)}
|
|
44
|
-
|
|
45
|
-
${q(
|
|
46
|
-
'Why no virtual DOM?',
|
|
47
|
-
`<p>A virtual DOM solves the problem of efficient incremental updates to a large, complex component tree. Pulse pages are server-rendered HTML strings — 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>
|
|
48
|
-
<p>Eliminating the virtual DOM means eliminating the ~40–100 kB runtime that comes with it. Pulse ships ~4 kB brotli to the browser on first visit (shared runtime + page bundle). That is not a compression trick — there is genuinely no framework runtime on the client.</p>`
|
|
49
|
-
)}
|
|
50
|
-
|
|
51
|
-
${q(
|
|
52
|
-
'Can I use npm packages in my specs?',
|
|
53
|
-
`<p>Yes, on the server side. Server fetchers run in Node.js and can import any npm package — database clients, API SDKs, utility libraries. These never reach the browser.</p>
|
|
54
|
-
<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 — 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>`
|
|
55
|
-
)}
|
|
56
|
-
|
|
57
|
-
${q(
|
|
58
|
-
'Does Pulse work with TypeScript?',
|
|
59
|
-
`<p>Type definitions are included for all public APIs — <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>
|
|
60
|
-
<p>The examples and docs are in plain JS for readability, but TypeScript is a first-class option.</p>`
|
|
61
|
-
)}
|
|
62
|
-
|
|
63
|
-
${q(
|
|
64
|
-
'Can I use a database directly in Pulse?',
|
|
65
|
-
`<p>Yes. Server fetchers are plain async functions — 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>
|
|
66
|
-
<pre class="code-block"><code>server: {
|
|
67
|
-
posts: async (ctx) => {
|
|
68
|
-
return db.posts.findMany({ where: { published: true } })
|
|
69
|
-
},
|
|
70
|
-
}</code></pre>
|
|
71
|
-
<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>`
|
|
72
|
-
)}
|
|
73
|
-
|
|
74
|
-
${q(
|
|
75
|
-
'How does Pulse compare to Next.js?',
|
|
76
|
-
`<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>
|
|
77
|
-
<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 — 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>`
|
|
78
|
-
)}
|
|
79
|
-
|
|
80
|
-
${q(
|
|
81
|
-
'Can I build a multi-page app with client-side navigation?',
|
|
82
|
-
`<p>Yes. <code>initNavigation</code> intercepts same-origin link clicks, fetches the new page, swaps the DOM, and re-mounts the spec — 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>
|
|
83
|
-
<p>See the <a href="/navigation">Navigation</a> docs for details.</p>`
|
|
84
|
-
)}
|
|
85
|
-
|
|
86
|
-
${q(
|
|
87
|
-
'Does Pulse handle authentication?',
|
|
88
|
-
`<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 — the guard runs before data is fetched.</p>
|
|
89
|
-
<p>For a complete auth integration, see the <a href="/auth">Auth0 guide</a>. The pattern works the same with any auth provider.</p>`
|
|
90
|
-
)}
|
|
91
|
-
|
|
92
|
-
${q(
|
|
93
|
-
'What is the AI-native claim actually about?',
|
|
94
|
-
`<p>Two things. First, the spec format was designed to be easy for an AI agent to generate correctly — 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>
|
|
95
|
-
<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 — without leaving the editor. The agent can build and verify a page end-to-end without human intervention on the tooling side.</p>`
|
|
96
|
-
)}
|
|
97
|
-
`,
|
|
98
|
-
}),
|
|
99
|
-
}
|