@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/forms.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, sub, 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('/forms')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/forms',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Forms — Pulse Docs',
|
|
12
|
-
description: 'Server-side form handling in Pulse. spec.submit processes POST requests without client JavaScript, with automatic CSRF protection and POST-redirect-GET.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/forms',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Forms')}
|
|
21
|
-
${lead('A spec with a <code>submit</code> handler accepts POST requests on its own route — the form works with JavaScript disabled. CSRF protection is automatic, and successful submissions follow POST-redirect-GET. Client-side <code>actions</code> become an enhancement, not a requirement.')}
|
|
22
|
-
|
|
23
|
-
${section('basics', 'Basic usage')}
|
|
24
|
-
<p>Add a <code>submit</code> function to the spec and render a plain <code><form method="POST"></code>. The route accepts POST automatically — no <code>methods</code> declaration needed.</p>
|
|
25
|
-
${codeBlock(highlight(`export default {
|
|
26
|
-
route: '/contact',
|
|
27
|
-
|
|
28
|
-
submit: async (ctx) => {
|
|
29
|
-
const data = await ctx.formData()
|
|
30
|
-
if (!data?.email) return { errors: { email: 'Email is required' }, values: data ?? {} }
|
|
31
|
-
await sendMessage(data)
|
|
32
|
-
return { redirect: '/contact?sent=1' } // 303 POST-redirect-GET
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
view: (state, server) => \`
|
|
36
|
-
<main id="main-content">
|
|
37
|
-
\${server.form?.errors?.email ? \`<p class="error">\${escHtml(server.form.errors.email)}</p>\` : ''}
|
|
38
|
-
<form method="POST">
|
|
39
|
-
\${server.csrf}
|
|
40
|
-
\${input({ label: 'Email', name: 'email', type: 'email', required: true,
|
|
41
|
-
value: server.form?.values?.email ?? '' })}
|
|
42
|
-
\${button({ label: 'Send', type: 'submit' })}
|
|
43
|
-
</form>
|
|
44
|
-
</main>
|
|
45
|
-
\`,
|
|
46
|
-
}`, 'js'))}
|
|
47
|
-
<p>The flow: the browser POSTs the form, <code>submit(ctx)</code> runs server-side, and either redirects (success) or re-renders the page with the returned value available to the view as <code>server.form</code> (validation failure).</p>
|
|
48
|
-
|
|
49
|
-
${callout('warning', '<strong><code>\${server.csrf}</code> must be inside every POST form.</strong> It renders the hidden CSRF token input — submissions without a valid token are rejected with <strong>403</strong> before <code>submit</code> runs.')}
|
|
50
|
-
|
|
51
|
-
${section('return-values', 'Return values')}
|
|
52
|
-
${table(
|
|
53
|
-
['Return', 'Response', 'Use for'],
|
|
54
|
-
[
|
|
55
|
-
['<code>{ redirect: \'/path\' }</code>', '<strong>303 See Other</strong>', 'Successful submissions — POST-redirect-GET means refresh never resubmits'],
|
|
56
|
-
['<code>{ errors, values, … }</code>', 'Page re-renders (200), value exposed as <code>server.form</code>', 'Validation failures — show errors, echo values back into inputs'],
|
|
57
|
-
['<code>{ status: 422, errors, … }</code>', 'Same re-render with status <strong>422</strong>', 'Validation failures where the status code matters (API clients, tests)'],
|
|
58
|
-
['<code>undefined</code>', 'Page re-renders (200), <code>server.form</code> is <code>{}</code>', 'Fire-and-forget side effects'],
|
|
59
|
-
]
|
|
60
|
-
)}
|
|
61
|
-
<p><strong>Always redirect after a successful mutation.</strong> Rendering a success message directly from the POST means a browser refresh resubmits the form. Redirect to a URL that shows the confirmation instead (e.g. <code>/contact?sent=1</code>).</p>
|
|
62
|
-
|
|
63
|
-
${section('server-form', 'server.form — errors and echoed values')}
|
|
64
|
-
<p>On a re-render, whatever <code>submit</code> returned is available to the view as <code>server.form</code>. On a plain GET it is <code>undefined</code> — always use optional chaining.</p>
|
|
65
|
-
${codeBlock(highlight(`// In submit — return errors AND the submitted values:
|
|
66
|
-
return { errors: { email: 'Invalid email' }, values: data }
|
|
67
|
-
|
|
68
|
-
// In the view — show the error, echo the value back so the form isn't wiped:
|
|
69
|
-
\${server.form?.errors?.email ? \`<p class="error">\${escHtml(server.form.errors.email)}</p>\` : ''}
|
|
70
|
-
\${input({ name: 'email', label: 'Email', value: server.form?.values?.email ?? '' })}`, 'js'))}
|
|
71
|
-
${callout('warning', 'Escape user-submitted values before interpolating them into HTML — <code>server.form.values</code> is raw user input. Component props like <code>input({ value })</code> are auto-escaped; manual interpolation needs <code>escHtml()</code>.')}
|
|
72
|
-
|
|
73
|
-
${section('csrf', 'CSRF protection')}
|
|
74
|
-
<p>Every <code>submit</code> spec gets CSRF protection automatically — there is nothing to configure. Under the hood Pulse uses an HMAC double-submit pattern:</p>
|
|
75
|
-
${table(
|
|
76
|
-
['Piece', 'Where it lives', 'What it does'],
|
|
77
|
-
[
|
|
78
|
-
['Random id', '<code>pulse_csrf</code> cookie (HttpOnly, SameSite=Lax)', 'Set on the first GET of the form page'],
|
|
79
|
-
['Token', 'Hidden <code>_csrf</code> input via <code>\${server.csrf}</code>', '<code>HMAC-SHA256(secret, id)</code> — embedded into the form server-side'],
|
|
80
|
-
['Verification', 'On every POST, before <code>submit</code> runs', 'Recomputes the HMAC from the cookie and compares in constant time — mismatch is 403'],
|
|
81
|
-
]
|
|
82
|
-
)}
|
|
83
|
-
<p>An attacker on another origin can cause the cookie to be sent, but cannot read it or compute the HMAC — so they cannot forge the pair.</p>
|
|
84
|
-
|
|
85
|
-
${sub('multi-instance', 'Multiple server instances')}
|
|
86
|
-
<p>The HMAC secret defaults to a random value per server boot. Behind a load balancer, pin it so tokens issued by one instance validate on another:</p>
|
|
87
|
-
${codeBlock(highlight(`await createServer(pages, {
|
|
88
|
-
secret: process.env.PULSE_SECRET, // stable across instances and restarts
|
|
89
|
-
})`, 'js'))}
|
|
90
|
-
|
|
91
|
-
${sub('opt-out', 'Opting out')}
|
|
92
|
-
<p>Set <code>csrf: false</code> on the spec to disable protection — only for endpoints with their own authentication, such as signed webhooks:</p>
|
|
93
|
-
${codeBlock(highlight(`export default {
|
|
94
|
-
route: '/webhooks/stripe',
|
|
95
|
-
csrf: false, // Stripe signs its requests — verify the signature instead
|
|
96
|
-
submit: async (ctx) => {
|
|
97
|
-
verifyStripeSignature(ctx.headers['stripe-signature'], await ctx.text())
|
|
98
|
-
// ...
|
|
99
|
-
},
|
|
100
|
-
view: () => '<main id="main-content">ok</main>',
|
|
101
|
-
}`, 'js'))}
|
|
102
|
-
${callout('warning', 'Never set <code>csrf: false</code> to "fix" a 403. The 403 means the form is missing <code>\${server.csrf}</code> — add the field, don\'t remove the protection.')}
|
|
103
|
-
|
|
104
|
-
${section('progressive-enhancement', 'Progressive enhancement')}
|
|
105
|
-
<p>The same form can serve both worlds: <code>method="POST"</code> is the no-JS fallback, <code>data-action</code> is the hydrated path. With JavaScript enabled the runtime intercepts the submit and runs the async action (no page reload); without it the browser POSTs to <code>submit</code>.</p>
|
|
106
|
-
${codeBlock(highlight(`<form method="POST" data-action="send">
|
|
107
|
-
\${server.csrf}
|
|
108
|
-
\${input({ label: 'Email', name: 'email', type: 'email', required: true })}
|
|
109
|
-
\${button({ label: 'Send', type: 'submit' })}
|
|
110
|
-
</form>`, 'html'))}
|
|
111
|
-
<p>Keep the two paths consistent: <code>submit</code> and the action's <code>run</code> should call the same underlying function so validation and side effects cannot drift apart.</p>
|
|
112
|
-
|
|
113
|
-
${section('caching', 'Interaction with caching')}
|
|
114
|
-
<p>Pages with <code>submit</code> are automatically excluded from the in-process page cache — the CSRF token is per-visitor, and a cached page would serve one visitor's token to everyone. POST responses are never cached.</p>
|
|
115
|
-
|
|
116
|
-
${section('reference', 'Reference')}
|
|
117
|
-
${table(
|
|
118
|
-
['Property', 'Type', 'Notes'],
|
|
119
|
-
[
|
|
120
|
-
['<code>submit</code>', '<code>async (ctx) => { redirect } | object | void</code>', 'Handles POST on the route. Guard runs first.'],
|
|
121
|
-
['<code>csrf</code>', '<code>boolean</code>', 'Default <code>true</code>. Set <code>false</code> only for self-authenticated endpoints.'],
|
|
122
|
-
['<code>server.csrf</code>', 'string (view)', 'Hidden <code>_csrf</code> input — required inside every POST form.'],
|
|
123
|
-
['<code>server.form</code>', 'object | undefined (view)', 'Return value of <code>submit</code> on a re-render; <code>undefined</code> on GET.'],
|
|
124
|
-
['<code>secret</code>', 'string (createServer option)', 'Stable HMAC secret for multi-instance deployments.'],
|
|
125
|
-
]
|
|
126
|
-
)}
|
|
127
|
-
`,
|
|
128
|
-
}),
|
|
129
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, codeBlock, callout } from '../lib/layout.js'
|
|
2
|
-
import { prevNext } from '../lib/nav.js'
|
|
3
|
-
import { highlight } from '../lib/highlight.js'
|
|
4
|
-
|
|
5
|
-
const { prev, next } = prevNext('/getting-started')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/getting-started',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Getting Started — Pulse Docs',
|
|
12
|
-
description: 'Install Pulse and build your first page with an AI agent in minutes.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/getting-started',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Getting Started')}
|
|
21
|
-
${lead('Install Pulse, run one command, and have an AI agent building your first page — with streaming SSR, security headers, and a 100 Lighthouse score already in place.')}
|
|
22
|
-
|
|
23
|
-
${section('requirements', 'Requirements')}
|
|
24
|
-
<ul>
|
|
25
|
-
<li><strong>Node.js 22 or later</strong> — <a href="https://nodejs.org" target="_blank" rel="noopener" aria-label="nodejs.org (opens in new tab)">nodejs.org</a></li>
|
|
26
|
-
<li><strong>Google Chrome</strong> — used by the agent for screenshots and Lighthouse audits — <a href="https://www.google.com/chrome" target="_blank" rel="noopener" aria-label="google.com/chrome (opens in new tab)">google.com/chrome</a></li>
|
|
27
|
-
<li><strong>An AI agent</strong> — Claude Code or GitHub Copilot CLI (pick one):</li>
|
|
28
|
-
</ul>
|
|
29
|
-
<ul>
|
|
30
|
-
<li><strong>Claude Code</strong> — <a href="https://docs.anthropic.com/en/docs/claude-code/getting-started" target="_blank" rel="noopener" aria-label="Claude Code installation guide (opens in new tab)">installation guide</a> — the default; no extra config needed</li>
|
|
31
|
-
<li><strong>GitHub Copilot CLI</strong> — <a href="https://docs.github.com/copilot/concepts/agents/about-copilot-cli" target="_blank" rel="noopener" aria-label="GitHub Copilot CLI documentation (opens in new tab)">documentation</a> — use with the <code>--agent copilot</code> flag</li>
|
|
32
|
-
</ul>
|
|
33
|
-
<p>Pulse launches your chosen agent automatically with the Pulse MCP server wired in — so the agent has instant access to the framework reference, your project structure, and all Pulse tools without any manual configuration.</p>
|
|
34
|
-
|
|
35
|
-
${section('install', 'Install Pulse')}
|
|
36
|
-
<p>Install the Pulse CLI globally:</p>
|
|
37
|
-
${codeBlock(highlight('npm install -g @invisibleloop/pulse', 'bash'))}
|
|
38
|
-
|
|
39
|
-
${section('create', 'Create your project')}
|
|
40
|
-
<p>Run <code>pulse</code> in any empty directory:</p>
|
|
41
|
-
${codeBlock(highlight(`mkdir my-app
|
|
42
|
-
cd my-app
|
|
43
|
-
pulse`, 'bash'))}
|
|
44
|
-
<p>Pulse detects the directory is empty and scaffolds a project there. It creates the project files, installs dependencies, and exits with:</p>
|
|
45
|
-
${codeBlock(highlight('✓ Project ready. Run `pulse` again to start your AI session.', 'bash'))}
|
|
46
|
-
${callout('tip', 'Running <code>pulse</code> in a non-empty directory prompts for a project name, then creates and scaffolds a subdirectory with that name — so you can run it from anywhere.')}
|
|
47
|
-
|
|
48
|
-
${section('session', 'Start a session')}
|
|
49
|
-
<p>Run <code>pulse</code> again from inside your project directory:</p>
|
|
50
|
-
${codeBlock(highlight('pulse', 'bash'))}
|
|
51
|
-
<p>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 — ready to build immediately.</p>
|
|
52
|
-
${callout('note', 'Run <code>pulse</code> every time you open a working session. It handles starting the agent and wiring up the MCP server. Once the agent is open, use <code>/pulse-dev</code> (Claude) or the <code>build-page</code> skill (Copilot) to start the dev server.')}
|
|
53
|
-
|
|
54
|
-
${section('copilot', 'Using GitHub Copilot')}
|
|
55
|
-
<p>To use GitHub Copilot CLI instead of Claude, pass the <code>--agent</code> flag when scaffolding:</p>
|
|
56
|
-
${codeBlock(highlight('pulse --agent copilot', 'bash'))}
|
|
57
|
-
<p>This scaffolds the project and writes <code>agent: \'copilot\'</code> into <code>pulse.config.js</code> — so subsequent <code>pulse</code> runs pick up the preference automatically. You can also set it manually in <code>pulse.config.js</code> at any time:</p>
|
|
58
|
-
${codeBlock(highlight(`export default {
|
|
59
|
-
agent: 'copilot',
|
|
60
|
-
}`, 'js'))}
|
|
61
|
-
<p>When using Copilot, project skills (<code>build-page</code>, <code>verify</code>, <code>new-doc-page</code>) are available via the <code>/skills</code> command inside the Copilot CLI session. The Pulse MCP server is automatically injected into your Copilot MCP config for the duration of the session and removed cleanly on exit.</p>
|
|
62
|
-
|
|
63
|
-
${section('first-build', 'Build your first page')}
|
|
64
|
-
<p>Once Claude opens, start the dev server and ask for something:</p>
|
|
65
|
-
${codeBlock(highlight(`/pulse-dev
|
|
66
|
-
|
|
67
|
-
"Create a contact form with name, email, and message fields.
|
|
68
|
-
Validate the email format before submitting."`, 'bash'))}
|
|
69
|
-
<p>The agent will:</p>
|
|
70
|
-
<ol>
|
|
71
|
-
<li>Fetch the Pulse guide from the MCP server — spec format, component library, quality rules</li>
|
|
72
|
-
<li>Check what pages already exist in your project</li>
|
|
73
|
-
<li>Write the spec — route, state, validation, action lifecycle, view</li>
|
|
74
|
-
<li>Validate it against the schema and fix every error and warning</li>
|
|
75
|
-
<li>Open the page in the browser and confirm it looks right</li>
|
|
76
|
-
</ol>
|
|
77
|
-
<p>You do not need to explain Pulse to the agent. The MCP server supplies the reference. Just describe what you want.</p>
|
|
78
|
-
|
|
79
|
-
${section('what-was-created', 'What got created')}
|
|
80
|
-
<p>When you ran <code>pulse</code> in step 2, these files were written to your directory:</p>
|
|
81
|
-
${codeBlock(highlight(`my-app/
|
|
82
|
-
├── src/
|
|
83
|
-
│ ├── pages/
|
|
84
|
-
│ │ └── home.js ← your first page spec (a working counter)
|
|
85
|
-
│ └── components/ ← shared view components go here
|
|
86
|
-
├── public/
|
|
87
|
-
│ ├── app.css ← global stylesheet
|
|
88
|
-
│ ├── pulse-ui.css ← Pulse component library styles
|
|
89
|
-
│ └── pulse-ui.js ← Pulse component library behaviour
|
|
90
|
-
├── .claude/ ← Claude Code agent context
|
|
91
|
-
│ ├── CLAUDE.md ← session instructions Claude reads on startup
|
|
92
|
-
│ ├── settings.json ← hooks: syntax checks, colour guards, package blocklist
|
|
93
|
-
│ ├── pulse-checklist.md ← spec review checklist, kept in sync by Pulse
|
|
94
|
-
│ └── commands/ ← slash commands available inside Claude
|
|
95
|
-
│ ├── pulse-dev.md
|
|
96
|
-
│ ├── pulse-stop.md
|
|
97
|
-
│ ├── pulse-build.md
|
|
98
|
-
│ └── pulse-start.md
|
|
99
|
-
├── .copilot/ ← GitHub Copilot CLI agent context
|
|
100
|
-
│ └── skills/ ← project skills available inside Copilot
|
|
101
|
-
│ ├── build-page/
|
|
102
|
-
│ ├── verify/
|
|
103
|
-
│ └── new-doc-page/
|
|
104
|
-
├── .github/
|
|
105
|
-
│ ├── copilot-instructions.md ← session instructions for Copilot
|
|
106
|
-
│ └── instructions/
|
|
107
|
-
│ └── pulse-checklist.instructions.md
|
|
108
|
-
├── package.json
|
|
109
|
-
└── pulse.config.js ← port, agent, and project settings`, 'bash'))}
|
|
110
|
-
<p><code>src/pages/home.js</code> is a complete working spec — a counter with increment and decrement buttons. Open <a href="http://localhost:3000">localhost:3000</a> after starting the dev server to see it. Every new page you create goes into <code>src/pages/</code> and is discovered automatically.</p>
|
|
111
|
-
<p>The <code>.claude/</code> directory contains the Claude agent's operating context. The <code>.copilot/</code> directory contains the equivalent context for GitHub Copilot — project skills and instructions. Both are scaffolded by default so you can switch agents at any time.</p>
|
|
112
|
-
|
|
113
|
-
${section('commands', 'Agent commands')}
|
|
114
|
-
<p>These slash commands are available once Claude is open:</p>
|
|
115
|
-
${codeBlock(highlight(`/pulse-dev # start (or restart) the dev server
|
|
116
|
-
/pulse-stop # stop the dev server
|
|
117
|
-
/pulse-build # production build → public/dist/
|
|
118
|
-
/pulse-start # run the production server
|
|
119
|
-
/pulse-report # Lighthouse audit + performance report`, 'bash'))}
|
|
120
|
-
<p>When using GitHub Copilot CLI, the equivalent capabilities are available as project skills. Type <code>/skills</code> inside Copilot to browse them — <code>build-page</code>, <code>verify</code>, and <code>new-doc-page</code> are all pre-installed.</p>
|
|
121
|
-
<p>You can also skip commands and skills entirely — just describe what you want and the agent handles the rest.</p>
|
|
122
|
-
|
|
123
|
-
${section('update', 'Keeping up to date')}
|
|
124
|
-
<p>When a new version of Pulse is released, update the package and then run <code>pulse update</code> from your project root:</p>
|
|
125
|
-
${codeBlock(highlight(`npm update @invisibleloop/pulse
|
|
126
|
-
pulse update`, 'bash'))}
|
|
127
|
-
<p><code>pulse update</code> copies the latest UI assets and agent files into your project:</p>
|
|
128
|
-
<ul>
|
|
129
|
-
<li><code>public/pulse-ui.css</code> and <code>public/pulse-ui.js</code> — component library styles and behaviour</li>
|
|
130
|
-
<li><code>.claude/pulse-checklist.md</code> and <code>.claude/commands/</code> — Claude agent files</li>
|
|
131
|
-
<li><code>.copilot/skills/</code> — Copilot project skills</li>
|
|
132
|
-
<li><code>.github/instructions/pulse-checklist.instructions.md</code> — Copilot checklist</li>
|
|
133
|
-
</ul>
|
|
134
|
-
<p>Your own source files, <code>CLAUDE.md</code>, <code>.github/copilot-instructions.md</code>, and <code>pulse.config.js</code> are never touched.</p>
|
|
135
|
-
|
|
136
|
-
${section('next-steps', 'Next steps')}
|
|
137
|
-
<ul>
|
|
138
|
-
<li><a href="/how-it-works">How It Works</a> — the MCP server, what the agent knows, and the full build cycle</li>
|
|
139
|
-
<li><a href="/project-structure">Project Structure</a> — where files live and how pages are discovered</li>
|
|
140
|
-
<li><a href="/spec">Spec Reference</a> — every field the spec supports</li>
|
|
141
|
-
<li><a href="/state">State</a> — client state and mutations</li>
|
|
142
|
-
<li><a href="/server-data">Server Data</a> — fetch data on the server before the page renders</li>
|
|
143
|
-
<li><a href="/prompt-examples">Prompt Examples</a> — real prompts with the output they produce</li>
|
|
144
|
-
</ul>
|
|
145
|
-
`,
|
|
146
|
-
}),
|
|
147
|
-
}
|
package/docs/src/pages/guard.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { renderLayout, h1, lead, section, sub, 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('/guard')
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
route: '/guard',
|
|
9
|
-
meta: {
|
|
10
|
-
theme: 'light',
|
|
11
|
-
title: 'Guard — Pulse Docs',
|
|
12
|
-
description: 'Per-route authorization in Pulse. Guard functions run before server data fetchers and redirect unauthorized requests.',
|
|
13
|
-
styles: ['/theme.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderLayout({
|
|
16
|
-
currentHref: '/guard',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
content: `
|
|
20
|
-
${h1('Guard')}
|
|
21
|
-
${lead('A <code>guard</code> function runs on every request to a route, before any server data is fetched. It is the enforced access control point — unauthorized requests are redirected before any database queries or data fetchers execute.')}
|
|
22
|
-
|
|
23
|
-
${section('basics', 'Basic usage')}
|
|
24
|
-
<p>A <code>guard</code> function on any spec receives the same <code>ctx</code> object as server data fetchers — params, query, headers, and cookies.</p>
|
|
25
|
-
${codeBlock(highlight(`export default {
|
|
26
|
-
route: '/dashboard',
|
|
27
|
-
|
|
28
|
-
guard: async (ctx) => {
|
|
29
|
-
if (!ctx.cookies.session) return { redirect: '/login' }
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
server: {
|
|
33
|
-
user: async (ctx) => getCurrentUser(ctx.cookies.session),
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
view: (state, server) => \`
|
|
37
|
-
<main id="main-content">
|
|
38
|
-
<h1>Welcome, \${server.user.name}</h1>
|
|
39
|
-
</main>
|
|
40
|
-
\`,
|
|
41
|
-
}`, 'js'))}
|
|
42
|
-
|
|
43
|
-
<p>When the guard returns <code>{ redirect }</code>, the server responds with a <strong>302</strong> and all server data fetchers are skipped — no data is fetched for unauthorized requests. When the guard returns nothing, the request proceeds normally.</p>
|
|
44
|
-
|
|
45
|
-
${section('ctx', 'What ctx contains')}
|
|
46
|
-
${table(
|
|
47
|
-
['Property / Method', 'Type', 'Description'],
|
|
48
|
-
[
|
|
49
|
-
['<code>ctx.cookies</code>', 'object', 'Parsed cookies from the <code>Cookie</code> header'],
|
|
50
|
-
['<code>ctx.headers</code>', 'object', 'Raw request headers'],
|
|
51
|
-
['<code>ctx.params</code>', 'object', 'Route params e.g. <code>{ id: "42" }</code>'],
|
|
52
|
-
['<code>ctx.query</code>', 'object', 'Parsed query string'],
|
|
53
|
-
['<code>ctx.pathname</code>','string', 'URL path e.g. <code>/dashboard</code>'],
|
|
54
|
-
['<code>ctx.method</code>', 'string', 'HTTP method e.g. <code>GET</code>, <code>POST</code>'],
|
|
55
|
-
['<code>ctx.store</code>', 'object', 'Resolved global store state (if a store is registered)'],
|
|
56
|
-
['<code>ctx.nonce</code>', 'string', 'CSP nonce for the current request'],
|
|
57
|
-
['<code>await ctx.json()</code>', 'object | null', 'Parse a JSON request body'],
|
|
58
|
-
['<code>await ctx.text()</code>', 'string', 'Read the body as a plain string'],
|
|
59
|
-
['<code>await ctx.formData()</code>', 'object | null', 'Parse a URL-encoded body into a plain object'],
|
|
60
|
-
['<code>await ctx.buffer()</code>', 'Buffer', 'Read the raw body as a Node.js Buffer'],
|
|
61
|
-
]
|
|
62
|
-
)}
|
|
63
|
-
|
|
64
|
-
${section('examples', 'Common patterns')}
|
|
65
|
-
|
|
66
|
-
${sub('Session check')}
|
|
67
|
-
<p>Redirect to login when no session cookie is present.</p>
|
|
68
|
-
${codeBlock(highlight(`guard: async (ctx) => {
|
|
69
|
-
if (!ctx.cookies.session) return { redirect: '/login' }
|
|
70
|
-
}`, 'js'))}
|
|
71
|
-
|
|
72
|
-
${sub('Role-based access')}
|
|
73
|
-
<p>Fetch the user from the session and check their role. Keep the lookup fast — guard runs on every request to the route.</p>
|
|
74
|
-
${codeBlock(highlight(`guard: async (ctx) => {
|
|
75
|
-
const user = await getUserFromSession(ctx.cookies.session)
|
|
76
|
-
if (!user) return { redirect: '/login' }
|
|
77
|
-
if (!user.isAdmin) return { redirect: '/403' }
|
|
78
|
-
}`, 'js'))}
|
|
79
|
-
|
|
80
|
-
${sub('Redirect authenticated users away from login')}
|
|
81
|
-
<p>Useful for login and signup pages — send already-authenticated users somewhere useful.</p>
|
|
82
|
-
${codeBlock(highlight(`export default {
|
|
83
|
-
route: '/login',
|
|
84
|
-
|
|
85
|
-
guard: async (ctx) => {
|
|
86
|
-
if (ctx.cookies.session) return { redirect: '/dashboard' }
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
view: () => \`<main id="main-content">...</main>\`,
|
|
90
|
-
}`, 'js'))}
|
|
91
|
-
|
|
92
|
-
${callout('info', 'Guard runs server-side on every request, including client-side navigation requests — those go through the same server pipeline. There is no way to bypass guard from the browser.')}
|
|
93
|
-
|
|
94
|
-
${section('custom-responses', 'Custom status responses')}
|
|
95
|
-
<p>Guard can return a custom HTTP response instead of (or alongside) a redirect. Return <code>{ status, json?, body?, headers? }</code> to send any status code with an optional JSON or text body. This is useful for POST handlers that need to signal validation errors or API-style rejections:</p>
|
|
96
|
-
${codeBlock(highlight(`guard: async (ctx) => {
|
|
97
|
-
const token = ctx.headers.authorization
|
|
98
|
-
if (!token) return { status: 401, json: { error: 'Unauthorized' } }
|
|
99
|
-
|
|
100
|
-
if (ctx.method === 'POST') {
|
|
101
|
-
const data = await ctx.formData()
|
|
102
|
-
if (!data.email) return { status: 422, json: { error: 'Email required' } }
|
|
103
|
-
await db.leads.create(data)
|
|
104
|
-
return { redirect: '/contact?sent=1' }
|
|
105
|
-
}
|
|
106
|
-
// return nothing to let a GET request proceed to the view
|
|
107
|
-
}`, 'js'))}
|
|
108
|
-
${callout('note', 'To use <code>guard</code> as a POST handler, the spec must declare <code>methods: [\'GET\', \'POST\']</code>. Without it, POST requests are rejected with 405 before guard runs.')}
|
|
109
|
-
${callout('tip', 'For HTML form submissions, prefer <a href="/forms"><code>spec.submit</code></a> — it accepts POST automatically, gets CSRF protection for free, and re-renders the page with validation errors. Guard-based POST handling is best for API-style endpoints.')}
|
|
110
|
-
|
|
111
|
-
${section('reference', 'Reference')}
|
|
112
|
-
${table(
|
|
113
|
-
['Property', 'Type', 'Required'],
|
|
114
|
-
[
|
|
115
|
-
['<code>guard</code>', '<code>async (ctx) => { redirect?: string } | { status, json?, body?, headers? } | void</code>', 'No'],
|
|
116
|
-
]
|
|
117
|
-
)}
|
|
118
|
-
`,
|
|
119
|
-
}),
|
|
120
|
-
}
|