@invisibleloop/pulse 0.21.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisibleloop/pulse",
3
- "version": "0.21.2",
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"
@@ -1 +1 @@
1
- 0.21.2
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
  })
@@ -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
- initNavigation(root, mount)
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
 
@@ -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 m = mount(spec, root, window.__PULSE_SERVER__ ?? {}, { ssr: true })
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
- ): void
40
+ ): {
41
+ /** Register the first page's mount so the first navigation destroys it. */
42
+ setMount(instance: MountResult | null | undefined): void
43
+ }