@invisibleloop/pulse 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -1
- package/docs/src/pages/spec.js +2 -2
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/src/runtime/index.js +4 -4
- package/src/spec/schema.js +1 -3
- package/src/spec/schema.test.js +7 -2
package/CLAUDE.md
CHANGED
|
@@ -35,7 +35,7 @@ public/
|
|
|
35
35
|
|
|
36
36
|
## The Spec
|
|
37
37
|
|
|
38
|
-
A spec is a plain JS object. Every property is optional except `route
|
|
38
|
+
A spec is a plain JS object. Every property is optional except `route` and `view`. `state` defaults to `{}` — omit it on purely server-rendered pages.
|
|
39
39
|
|
|
40
40
|
```js
|
|
41
41
|
export const mySpec = {
|
|
@@ -62,6 +62,7 @@ export const mySpec = {
|
|
|
62
62
|
store: ['user', 'cart'],
|
|
63
63
|
|
|
64
64
|
// Initial client state — deep cloned on mount, never mutated directly
|
|
65
|
+
// Optional — omit entirely on purely server-rendered pages (no mutations/actions/persist)
|
|
65
66
|
state: { count: 0 },
|
|
66
67
|
|
|
67
68
|
// Declarative min/max bounds — always enforced after mutations
|
package/docs/src/pages/spec.js
CHANGED
|
@@ -25,7 +25,7 @@ export default {
|
|
|
25
25
|
['Field', 'Type', 'Required', 'Description'],
|
|
26
26
|
[
|
|
27
27
|
['<code>route</code>', '<code>string</code>', 'Yes', 'URL pattern for this page. Supports <code>:param</code> segments.'],
|
|
28
|
-
['<code>state</code>', '<code>object</code>', '
|
|
28
|
+
['<code>state</code>', '<code>object</code>', 'No', 'Initial client-side state. Deep-cloned on mount. Defaults to <code>{}</code> — omit on purely server-rendered pages.'],
|
|
29
29
|
['<code>view</code>', '<code>function</code>', 'Yes', 'Returns an HTML string. Receives <code>(state, serverState)</code>.'],
|
|
30
30
|
['<code>meta</code>', '<code>object</code>', 'No', 'Page metadata: title, description, styles, OG tags, schema.'],
|
|
31
31
|
['<code>hydrate</code>', '<code>string</code>', 'Auto', 'Auto-derived from the URL entry in <code>createServer</code>. Do not set manually.'],
|
|
@@ -53,7 +53,7 @@ route: '/blog/:year/:slug'`, 'js'))}
|
|
|
53
53
|
<p>Dynamic segments are available in server data and actions via <code>ctx.params</code>. See <a href="/routing">Routing</a> for more.</p>
|
|
54
54
|
|
|
55
55
|
${section('state', 'state')}
|
|
56
|
-
<p>The initial client-side state for the page. Always a plain object. Pulse deep-clones it on every mount — mutations never affect the original spec, and state cannot leak between page loads.</p>
|
|
56
|
+
<p>The initial client-side state for the page. Always a plain object. Defaults to <code>{}</code> — you can omit it entirely on purely server-rendered pages with no <code>mutations</code>, <code>actions</code>, or <code>persist</code>. Pulse deep-clones it on every mount — mutations never affect the original spec, and state cannot leak between page loads.</p>
|
|
57
57
|
${codeBlock(highlight(`state: {
|
|
58
58
|
count: 0,
|
|
59
59
|
user: { name: '', email: '' },
|
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.5.0
|
package/src/runtime/index.js
CHANGED
|
@@ -24,8 +24,8 @@ const showToast = (opts) => import('./toast.js').then(m => m.showToast(opts))
|
|
|
24
24
|
* @param {Object} [serverState] - Serialised server state from SSR
|
|
25
25
|
*/
|
|
26
26
|
export function mount(spec, el, serverState = {}, options = {}) {
|
|
27
|
-
if (!spec ||
|
|
28
|
-
throw new Error('[Pulse] mount: spec must have
|
|
27
|
+
if (!spec || !spec.view) {
|
|
28
|
+
throw new Error('[Pulse] mount: spec must have a view')
|
|
29
29
|
}
|
|
30
30
|
// Spec is validated server-side at startup — no need to re-validate in the browser
|
|
31
31
|
// Initialise the client store from SSR data (no-op after the first page mount).
|
|
@@ -60,7 +60,7 @@ export function mount(spec, el, serverState = {}, options = {}) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
// Deep clone initial state — never mutate the spec itself
|
|
63
|
-
let state = deepClone(spec.state)
|
|
63
|
+
let state = deepClone(spec.state ?? {})
|
|
64
64
|
|
|
65
65
|
// Restore persisted keys from localStorage
|
|
66
66
|
const persistKey = spec.persist?.length ? `pulse:${spec.route || location.pathname}` : null
|
|
@@ -69,7 +69,7 @@ export function mount(spec, el, serverState = {}, options = {}) {
|
|
|
69
69
|
try {
|
|
70
70
|
const saved = JSON.parse(localStorage.getItem(persistKey) || '{}')
|
|
71
71
|
spec.persist.forEach(k => {
|
|
72
|
-
if (saved[k] !== undefined && saved[k] !== spec.state[k]) {
|
|
72
|
+
if (saved[k] !== undefined && saved[k] !== (spec.state ?? {})[k]) {
|
|
73
73
|
state[k] = saved[k]
|
|
74
74
|
restoredFromPersist = true
|
|
75
75
|
}
|
package/src/spec/schema.js
CHANGED
|
@@ -159,9 +159,7 @@ export function validateSpec(spec) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// state
|
|
162
|
-
if (spec.state
|
|
163
|
-
errors.push('spec.state is required — use {} if there is no client state')
|
|
164
|
-
} else if (typeof spec.state !== 'object' || Array.isArray(spec.state)) {
|
|
162
|
+
if (spec.state !== undefined && (typeof spec.state !== 'object' || Array.isArray(spec.state))) {
|
|
165
163
|
errors.push('spec.state must be a plain object')
|
|
166
164
|
}
|
|
167
165
|
|
package/src/spec/schema.test.js
CHANGED
|
@@ -54,8 +54,13 @@ test('rejects route that does not start with /', () => {
|
|
|
54
54
|
assertErrors({ route: 'contact', state: {}, view: () => '' }, 'must start with "/"')
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
test('
|
|
58
|
-
|
|
57
|
+
test('state is optional — omitting it is valid', () => {
|
|
58
|
+
const { valid, errors } = validateSpec({ route: '/contact', view: () => '' })
|
|
59
|
+
if (!valid) throw new Error(`Expected valid but got: ${errors.join(', ')}`)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('state must be a plain object when provided', () => {
|
|
63
|
+
assertErrors({ route: '/contact', state: 'bad', view: () => '' }, 'spec.state must be a plain object')
|
|
59
64
|
})
|
|
60
65
|
|
|
61
66
|
test('requires view', () => {
|