@invisibleloop/pulse 0.21.1 → 0.21.3
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 +2 -2
- package/public/.pulse-ui-version +1 -1
- package/scripts/bootstrap.js +25 -0
- package/scripts/build.js +2 -14
- package/src/runtime/ssr.js +3 -2
- package/src/spec/schema.js +5 -4
- package/types/navigate.d.ts +9 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@invisibleloop/pulse",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI-first frontend framework. The spec is the source of truth.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"scripts": {
|
|
77
77
|
"dev": "node --watch src/cli/dev.js",
|
|
78
78
|
"build": "node scripts/build.js",
|
|
79
|
-
"test": "node src/spec/schema.test.js && node src/runtime/runtime.test.js && node src/runtime/navigate.test.js && node src/runtime/morph.test.js && node src/runtime/ssr.test.js && node src/runtime/image.test.js && node src/server/server.test.js && node src/cli/cli.test.js && node src/cli/validate.test.js && node src/cli/diagnose.test.js && node src/cli/check-bundles.test.js && node src/cli/review.test.js && node src/cli/process-utils.test.js && node src/cli/stamp.test.js && node src/cli/feedback.test.js && node src/cli/update.test.js && node src/cli/dev-host.test.js && node src/cli/dev-reload.test.js && node src/ui/ui.test.js && node src/testing/testing.test.js && node src/store/store.test.js && node src/md/md.test.js && node scripts/strip-server.test.js && node scripts/css-purge.test.js && node scripts/release-version.test.js && node src/mcp/mcp.test.js && node src/mcp/guards.test.js",
|
|
79
|
+
"test": "node src/spec/schema.test.js && node src/runtime/runtime.test.js && node src/runtime/navigate.test.js && node src/runtime/morph.test.js && node src/runtime/ssr.test.js && node src/runtime/image.test.js && node src/server/server.test.js && node src/cli/cli.test.js && node src/cli/validate.test.js && node src/cli/diagnose.test.js && node src/cli/check-bundles.test.js && node src/cli/review.test.js && node src/cli/process-utils.test.js && node src/cli/stamp.test.js && node src/cli/feedback.test.js && node src/cli/update.test.js && node src/cli/dev-host.test.js && node src/cli/dev-reload.test.js && node src/ui/ui.test.js && node src/testing/testing.test.js && node src/store/store.test.js && node src/md/md.test.js && node scripts/strip-server.test.js && node scripts/css-purge.test.js && node scripts/bootstrap.test.js && node scripts/release-version.test.js && node src/mcp/mcp.test.js && node src/mcp/guards.test.js",
|
|
80
80
|
"typecheck": "tsc --noEmit",
|
|
81
81
|
"docs": "node --watch src/cli/dev.js --root ../../docs --port 4000",
|
|
82
82
|
"docs:start": "node ../../docs/server.js"
|
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.21.
|
|
1
|
+
0.21.3
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source of the production boot bundle entry generated for each hydrated page by
|
|
3
|
+
* scripts/build.js. Its own module (build.js runs the whole build when imported) so the
|
|
4
|
+
* generated code can be unit-tested.
|
|
5
|
+
*
|
|
6
|
+
* The first page's mount() instance is handed to initNavigation so the first client
|
|
7
|
+
* navigation destroys it; without that its listeners outlive the page (#168).
|
|
8
|
+
*/
|
|
9
|
+
export function buildBootstrapSource({ relSpec, relRuntime, relNavigate, storeImport = '', mountOpts = '{ ssr: true }' }) {
|
|
10
|
+
return `\
|
|
11
|
+
import spec from '${relSpec}'
|
|
12
|
+
import { mount } from '${relRuntime}'
|
|
13
|
+
import { initNavigation } from '${relNavigate}'${storeImport}
|
|
14
|
+
|
|
15
|
+
const root = document.getElementById('pulse-root')
|
|
16
|
+
if (root && !root.dataset.pulseMounted) {
|
|
17
|
+
root.dataset.pulseMounted = '1'
|
|
18
|
+
const instance = mount(spec, root, window.__PULSE_SERVER__ || {}, ${mountOpts})
|
|
19
|
+
// Register the first page's instance so the first client navigation destroys it
|
|
20
|
+
initNavigation(root, mount).setMount(instance)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default spec
|
|
24
|
+
`
|
|
25
|
+
}
|
package/scripts/build.js
CHANGED
|
@@ -21,6 +21,7 @@ import { discoverPages } from '../src/cli/discover.js'
|
|
|
21
21
|
import { renderToString } from '../src/runtime/ssr.js'
|
|
22
22
|
import { c, header, table, ok, fail, info, warn, elapsed, icon } from '../src/cli/fmt.js'
|
|
23
23
|
import { extractUsedClasses, purgeCss, minifyCss } from './css-purge.js'
|
|
24
|
+
import { buildBootstrapSource } from './bootstrap.js'
|
|
24
25
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Server-only property stripping
|
|
@@ -499,20 +500,7 @@ const bootstrapFiles = pages.flatMap(({ filePath }) => {
|
|
|
499
500
|
const storeImport = STORE_PATH ? `\nimport store from '${path.relative(TMP_DIR, STORE_PATH)}'` : ''
|
|
500
501
|
const mountOpts = STORE_PATH ? `{ ssr: true, store }` : `{ ssr: true }`
|
|
501
502
|
|
|
502
|
-
fs.writeFileSync(bootstrapPath,
|
|
503
|
-
import spec from '${relSpec}'
|
|
504
|
-
import { mount } from '${relRuntime}'
|
|
505
|
-
import { initNavigation } from '${relNavigate}'${storeImport}
|
|
506
|
-
|
|
507
|
-
const root = document.getElementById('pulse-root')
|
|
508
|
-
if (root && !root.dataset.pulseMounted) {
|
|
509
|
-
root.dataset.pulseMounted = '1'
|
|
510
|
-
mount(spec, root, window.__PULSE_SERVER__ || {}, ${mountOpts})
|
|
511
|
-
initNavigation(root, mount)
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
export default spec
|
|
515
|
-
`)
|
|
503
|
+
fs.writeFileSync(bootstrapPath, buildBootstrapSource({ relSpec, relRuntime, relNavigate, storeImport, mountOpts }))
|
|
516
504
|
|
|
517
505
|
return [{ filePath, bootstrapPath, name }]
|
|
518
506
|
})
|
package/src/runtime/ssr.js
CHANGED
|
@@ -298,8 +298,9 @@ export function buildHydrateScript(spec, storeDef, nonce) {
|
|
|
298
298
|
import { mount } from '/@pulse/runtime/index.js'
|
|
299
299
|
import { initNavigation } from '/@pulse/runtime/navigate.js'${storeImport}
|
|
300
300
|
const root = document.getElementById('pulse-root')
|
|
301
|
-
mount(spec, root, window.__PULSE_SERVER__ || {}${storeArg})
|
|
302
|
-
|
|
301
|
+
const instance = mount(spec, root, window.__PULSE_SERVER__ || {}${storeArg})
|
|
302
|
+
// Register the first page's instance so the first client navigation destroys it
|
|
303
|
+
initNavigation(root, mount).setMount(instance)
|
|
303
304
|
</script>`
|
|
304
305
|
}
|
|
305
306
|
|
package/src/spec/schema.js
CHANGED
|
@@ -124,17 +124,18 @@
|
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Validate a spec object against the schema.
|
|
127
|
-
* Returns { valid
|
|
127
|
+
* Returns { valid, errors, warnings } — `warnings` is always an array, including on the early
|
|
128
|
+
* returns (non-object spec, raw-response spec), because callers spread it.
|
|
128
129
|
*
|
|
129
130
|
* @param {unknown} spec
|
|
130
|
-
* @returns {{ valid: boolean, errors: string[] }}
|
|
131
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
131
132
|
*/
|
|
132
133
|
export function validateSpec(spec) {
|
|
133
134
|
const errors = []
|
|
134
135
|
const warnings = []
|
|
135
136
|
|
|
136
137
|
if (!spec || typeof spec !== 'object') {
|
|
137
|
-
return { valid: false, errors: ['Spec must be a plain object'] }
|
|
138
|
+
return { valid: false, errors: ['Spec must be a plain object'], warnings }
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
// route — '*' is the custom not-found page, rendered with status 404 when no route matches
|
|
@@ -191,7 +192,7 @@ export function validateSpec(spec) {
|
|
|
191
192
|
errors.push('spec.render is required when spec.contentType is set — (ctx, server) => string')
|
|
192
193
|
}
|
|
193
194
|
// Raw specs don't need state or view — skip those checks
|
|
194
|
-
return { valid: errors.length === 0, errors }
|
|
195
|
+
return { valid: errors.length === 0, errors, warnings }
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
// state
|
package/types/navigate.d.ts
CHANGED
|
@@ -14,7 +14,9 @@ import type { PulseSpec } from './schema.js'
|
|
|
14
14
|
* swaps #pulse-root, and re-mounts the new spec without a full page reload.
|
|
15
15
|
* Falls back to location.href on any fetch or import error.
|
|
16
16
|
*
|
|
17
|
-
* Call once after the initial mount()
|
|
17
|
+
* Call once after the initial mount(), and pass that instance to the returned
|
|
18
|
+
* `setMount` so the first navigation destroys it. Without it the first page's
|
|
19
|
+
* listeners stay alive and every handler present on both pages runs twice.
|
|
18
20
|
*
|
|
19
21
|
* @param root The #pulse-root element
|
|
20
22
|
* @param mountFn The mount() function from @invisibleloop/pulse/runtime
|
|
@@ -24,8 +26,8 @@ import type { PulseSpec } from './schema.js'
|
|
|
24
26
|
* import { initNavigation } from '@invisibleloop/pulse/navigate'
|
|
25
27
|
*
|
|
26
28
|
* const root = document.getElementById('pulse-root')
|
|
27
|
-
* const
|
|
28
|
-
* initNavigation(root, mount)
|
|
29
|
+
* const instance = mount(spec, root, window.__PULSE_SERVER__ ?? {}, { ssr: true })
|
|
30
|
+
* initNavigation(root, mount).setMount(instance)
|
|
29
31
|
*/
|
|
30
32
|
export function initNavigation(
|
|
31
33
|
root: HTMLElement,
|
|
@@ -35,4 +37,7 @@ export function initNavigation(
|
|
|
35
37
|
serverState?: Record<string, unknown>,
|
|
36
38
|
options?: { ssr?: boolean; store?: unknown }
|
|
37
39
|
) => MountResult
|
|
38
|
-
):
|
|
40
|
+
): {
|
|
41
|
+
/** Register the first page's mount so the first navigation destroys it. */
|
|
42
|
+
setMount(instance: MountResult | null | undefined): void
|
|
43
|
+
}
|