@storystick/preview 0.0.0-stage → 0.2.0-rc.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/LICENSE +112 -0
- package/PUBLIC-API.md +55 -0
- package/README.md +42 -2
- package/dist/.source-hash +1 -0
- package/dist/authoring/runtime-contract.d.ts +89 -0
- package/dist/authoring/runtime-contract.js +28 -0
- package/dist/channel-taxonomy.d.ts +17 -0
- package/dist/channel-taxonomy.js +46 -0
- package/dist/context-command.d.ts +18 -0
- package/dist/context-command.js +38 -0
- package/dist/dev/suspicious-codeword-detector.d.ts +15 -0
- package/dist/dev/suspicious-codeword-detector.js +220 -0
- package/dist/dev/suspicious-codeword-detector.noop.d.ts +3 -0
- package/dist/dev/suspicious-codeword-detector.noop.js +4 -0
- package/dist/framework/browser-context.d.ts +2 -0
- package/dist/framework/browser-context.js +2 -0
- package/dist/framework/generation-refresh.d.ts +23 -0
- package/dist/framework/generation-refresh.js +82 -0
- package/dist/framework/generation-signal.d.ts +8 -0
- package/dist/framework/generation-signal.js +17 -0
- package/dist/framework/projection-generation-watch.d.ts +5 -0
- package/dist/framework/projection-generation-watch.js +53 -0
- package/dist/framework/render-receipt.d.ts +8 -0
- package/dist/framework/render-receipt.js +10 -0
- package/dist/framework/runtime-adapter-store.d.ts +30 -0
- package/dist/framework/runtime-adapter-store.js +81 -0
- package/dist/grant-coordinator.d.ts +53 -0
- package/dist/grant-coordinator.js +329 -0
- package/dist/grant-lifecycle.d.ts +52 -0
- package/dist/grant-lifecycle.js +74 -0
- package/dist/identity/__test-adapter.d.ts +34 -0
- package/dist/identity/__test-adapter.js +59 -0
- package/dist/identity/browser-context-gate.d.ts +4 -0
- package/dist/identity/browser-context-gate.js +22 -0
- package/dist/identity/browser-context.d.ts +13 -0
- package/dist/identity/browser-context.js +22 -0
- package/dist/identity/carriers/postmessage.d.ts +84 -0
- package/dist/identity/carriers/postmessage.js +177 -0
- package/dist/identity/carriers/storage-state.d.ts +41 -0
- package/dist/identity/carriers/storage-state.js +99 -0
- package/dist/identity/carriers/storage.d.ts +39 -0
- package/dist/identity/carriers/storage.js +182 -0
- package/dist/identity/carriers/url.d.ts +19 -0
- package/dist/identity/carriers/url.js +38 -0
- package/dist/identity/identity-state-machine.d.ts +70 -0
- package/dist/identity/identity-state-machine.js +244 -0
- package/dist/identity/identity-types.d.ts +192 -0
- package/dist/identity/identity-types.js +22 -0
- package/dist/identity/index.d.ts +18 -0
- package/dist/identity/index.js +16 -0
- package/dist/identity/preview-cookie.d.ts +9 -0
- package/dist/identity/preview-cookie.js +12 -0
- package/dist/identity/preview-draft.d.ts +6 -0
- package/dist/identity/preview-draft.js +25 -0
- package/dist/identity/preview-identity.d.ts +28 -0
- package/dist/identity/preview-identity.js +84 -0
- package/dist/identity/preview-scope.d.ts +28 -0
- package/dist/identity/preview-scope.js +47 -0
- package/dist/identity/resolver.d.ts +110 -0
- package/dist/identity/resolver.js +334 -0
- package/dist/identity/session-query.d.ts +4 -0
- package/dist/identity/session-query.js +19 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +788 -0
- package/dist/interaction-mode.d.ts +58 -0
- package/dist/interaction-mode.js +93 -0
- package/dist/nav-handler.d.ts +95 -0
- package/dist/nav-handler.js +84 -0
- package/dist/overlay-label.d.ts +79 -0
- package/dist/overlay-label.js +73 -0
- package/dist/provenance/anchor-map.d.ts +95 -0
- package/dist/provenance/anchor-map.js +177 -0
- package/dist/provenance/carrier-plan.d.ts +51 -0
- package/dist/provenance/carrier-plan.js +90 -0
- package/dist/provenance/coded-text.d.ts +20 -0
- package/dist/provenance/coded-text.js +302 -0
- package/dist/provenance/direct-edit-locator.d.ts +29 -0
- package/dist/provenance/direct-edit-locator.js +218 -0
- package/dist/provenance/editable-leaf.d.ts +134 -0
- package/dist/provenance/editable-leaf.js +483 -0
- package/dist/provenance/editor-target.d.ts +29 -0
- package/dist/provenance/editor-target.js +46 -0
- package/dist/provenance/field-leaf-walker.d.ts +21 -0
- package/dist/provenance/field-leaf-walker.js +98 -0
- package/dist/provenance/frame-dependencies.d.ts +212 -0
- package/dist/provenance/frame-dependencies.js +550 -0
- package/dist/provenance/index.d.ts +26 -0
- package/dist/provenance/index.js +26 -0
- package/dist/provenance/local-events.d.ts +1 -0
- package/dist/provenance/local-events.js +1 -0
- package/dist/provenance/markers.d.ts +21 -0
- package/dist/provenance/markers.js +70 -0
- package/dist/provenance/multi-frame-anchor-map.d.ts +54 -0
- package/dist/provenance/multi-frame-anchor-map.js +109 -0
- package/dist/provenance/normalize.d.ts +2 -0
- package/dist/provenance/normalize.js +19 -0
- package/dist/provenance/overlay-focus-echo.d.ts +17 -0
- package/dist/provenance/overlay-focus-echo.js +16 -0
- package/dist/provenance/overlay-projection.d.ts +65 -0
- package/dist/provenance/overlay-projection.js +201 -0
- package/dist/provenance/owner-map.d.ts +55 -0
- package/dist/provenance/owner-map.js +239 -0
- package/dist/provenance/read-carrier.d.ts +22 -0
- package/dist/provenance/read-carrier.js +92 -0
- package/dist/provenance/recorder.d.ts +38 -0
- package/dist/provenance/recorder.js +82 -0
- package/dist/provenance/render-context.d.ts +106 -0
- package/dist/provenance/render-context.js +338 -0
- package/dist/provenance/reveal-contract.d.ts +44 -0
- package/dist/provenance/reveal-contract.js +1 -0
- package/dist/provenance/richtext-slice-embed-address.d.ts +20 -0
- package/dist/provenance/richtext-slice-embed-address.js +35 -0
- package/dist/provenance/runtime-script.d.ts +1 -0
- package/dist/provenance/runtime-script.js +2217 -0
- package/dist/provenance/sidecar-store.d.ts +63 -0
- package/dist/provenance/sidecar-store.js +151 -0
- package/dist/provenance/sidecar.d.ts +34 -0
- package/dist/provenance/sidecar.js +227 -0
- package/dist/provenance/slice-model.d.ts +28 -0
- package/dist/provenance/slice-model.js +86 -0
- package/dist/provenance/source-identity.d.ts +3 -0
- package/dist/provenance/source-identity.js +7 -0
- package/dist/provenance/source-map-materializer.d.ts +76 -0
- package/dist/provenance/source-map-materializer.js +286 -0
- package/dist/provenance/source-registry.d.ts +103 -0
- package/dist/provenance/source-registry.js +562 -0
- package/dist/provenance/transformed-output-gate.d.ts +161 -0
- package/dist/provenance/transformed-output-gate.js +171 -0
- package/dist/provenance/types.d.ts +102 -0
- package/dist/provenance/types.js +1 -0
- package/dist/refresh-policy.d.ts +11 -0
- package/dist/refresh-policy.js +14 -0
- package/dist/reveal-target.d.ts +8 -0
- package/dist/reveal-target.js +47 -0
- package/dist/route-paths.d.ts +11 -0
- package/dist/route-paths.js +14 -0
- package/dist/router-subscriber.d.ts +92 -0
- package/dist/router-subscriber.js +44 -0
- package/dist/runtime/anchor-types.d.ts +5 -0
- package/dist/runtime/anchor-types.js +1 -0
- package/dist/runtime/authority-store.d.ts +31 -0
- package/dist/runtime/authority-store.js +56 -0
- package/dist/runtime/boot.d.ts +52 -0
- package/dist/runtime/boot.js +379 -0
- package/dist/runtime/content-refresh.d.ts +30 -0
- package/dist/runtime/content-refresh.js +68 -0
- package/dist/runtime/emit-runtime-error.d.ts +51 -0
- package/dist/runtime/emit-runtime-error.js +64 -0
- package/dist/runtime/field-proxy.d.ts +112 -0
- package/dist/runtime/field-proxy.js +471 -0
- package/dist/runtime/form-contract-reporter.d.ts +99 -0
- package/dist/runtime/form-contract-reporter.js +391 -0
- package/dist/runtime/invalidation-adapter.d.ts +17 -0
- package/dist/runtime/invalidation-adapter.js +42 -0
- package/dist/runtime/live-query-overlay.d.ts +39 -0
- package/dist/runtime/live-query-overlay.js +345 -0
- package/dist/runtime/momentary-navigate.d.ts +16 -0
- package/dist/runtime/momentary-navigate.js +106 -0
- package/dist/runtime/observation-scheduler.d.ts +15 -0
- package/dist/runtime/observation-scheduler.js +42 -0
- package/dist/runtime/perf-metrics.d.ts +28 -0
- package/dist/runtime/perf-metrics.js +90 -0
- package/dist/runtime/preview-interaction-policy.d.ts +1 -0
- package/dist/runtime/preview-interaction-policy.js +103 -0
- package/dist/runtime/preview-overlay-painter.d.ts +27 -0
- package/dist/runtime/preview-overlay-painter.js +1732 -0
- package/dist/runtime/preview-overlay-sidecar.d.ts +26 -0
- package/dist/runtime/preview-overlay-sidecar.js +45 -0
- package/dist/runtime/preview-perspective-alignment.d.ts +15 -0
- package/dist/runtime/preview-perspective-alignment.js +40 -0
- package/dist/runtime/preview-perspective-intent-state.d.ts +23 -0
- package/dist/runtime/preview-perspective-intent-state.js +5 -0
- package/dist/runtime/preview-transition-state.d.ts +142 -0
- package/dist/runtime/preview-transition-state.js +257 -0
- package/dist/runtime/render-mode-state.d.ts +51 -0
- package/dist/runtime/render-mode-state.js +76 -0
- package/dist/runtime/request-id.d.ts +4 -0
- package/dist/runtime/request-id.js +8 -0
- package/dist/runtime/resolve-schema-field.d.ts +99 -0
- package/dist/runtime/resolve-schema-field.js +356 -0
- package/dist/runtime/runtime-error-dispatch.d.ts +23 -0
- package/dist/runtime/runtime-error-dispatch.js +75 -0
- package/dist/runtime/schema-store.d.ts +121 -0
- package/dist/runtime/schema-store.js +667 -0
- package/dist/runtime/snapshot-assembly.d.ts +104 -0
- package/dist/runtime/snapshot-assembly.js +553 -0
- package/dist/runtime/snapshot-reporter.d.ts +55 -0
- package/dist/runtime/snapshot-reporter.js +375 -0
- package/dist/runtime/standalone-overlay-controller.d.ts +33 -0
- package/dist/runtime/standalone-overlay-controller.js +281 -0
- package/dist/runtime/store.d.ts +307 -0
- package/dist/runtime/store.js +1026 -0
- package/dist/runtime/windowed-minimap-projection.d.ts +28 -0
- package/dist/runtime/windowed-minimap-projection.js +189 -0
- package/dist/runtime/workspace-shortcuts.d.ts +30 -0
- package/dist/runtime/workspace-shortcuts.js +188 -0
- package/dist/sdk-error-copy.d.ts +48 -0
- package/dist/sdk-error-copy.js +182 -0
- package/dist/select-cursor.d.ts +2 -0
- package/dist/select-cursor.js +23 -0
- package/dist/server/admission-cookie.d.ts +38 -0
- package/dist/server/admission-cookie.js +122 -0
- package/dist/server/control-client.d.ts +29 -0
- package/dist/server/control-client.js +22 -0
- package/dist/server/control-errors.d.ts +22 -0
- package/dist/server/control-errors.js +165 -0
- package/dist/server/draft-route.d.ts +12 -0
- package/dist/server/draft-route.js +45 -0
- package/dist/server/extract-token.d.ts +30 -0
- package/dist/server/extract-token.js +98 -0
- package/dist/server/grant.d.ts +128 -0
- package/dist/server/grant.js +395 -0
- package/dist/server/ordinary-read-provenance.d.ts +11 -0
- package/dist/server/ordinary-read-provenance.js +124 -0
- package/dist/server/preview-read-metadata.d.ts +10 -0
- package/dist/server/preview-read-metadata.js +27 -0
- package/dist/server/provenance-frame-builder.d.ts +53 -0
- package/dist/server/provenance-frame-builder.js +102 -0
- package/dist/server/provenance-runtime.d.ts +70 -0
- package/dist/server/provenance-runtime.js +46 -0
- package/dist/server/read-context-command.d.ts +11 -0
- package/dist/server/read-context-command.js +53 -0
- package/dist/server/read-identity.d.ts +3 -0
- package/dist/server/read-identity.js +63 -0
- package/dist/server/read-lifecycle.d.ts +17 -0
- package/dist/server/read-lifecycle.js +52 -0
- package/dist/server/render-frame-provenance.d.ts +133 -0
- package/dist/server/render-frame-provenance.js +676 -0
- package/dist/server/render-id.d.ts +15 -0
- package/dist/server/render-id.js +22 -0
- package/dist/server/route-document-provenance.d.ts +77 -0
- package/dist/server/route-document-provenance.js +1327 -0
- package/dist/server/route-session-cookie.d.ts +33 -0
- package/dist/server/route-session-cookie.js +54 -0
- package/dist/session-projections.d.ts +33 -0
- package/dist/session-projections.js +43 -0
- package/dist/session-subscriber.d.ts +124 -0
- package/dist/session-subscriber.js +79 -0
- package/dist/sha256-fallback.d.ts +30 -0
- package/dist/sha256-fallback.js +126 -0
- package/dist/types.d.ts +446 -0
- package/dist/types.js +85 -0
- package/dist/validators.d.ts +61 -0
- package/dist/validators.js +110 -0
- package/package.json +206 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import { readStandalonePreviewSessionId } from './identity/browser-context.js';
|
|
2
|
+
import { createPreviewRequestId } from './runtime/request-id.js';
|
|
3
|
+
/**
|
|
4
|
+
* @storystick/preview — Visual editing overlay
|
|
5
|
+
*
|
|
6
|
+
* Framework-independent preview system for Storystick.
|
|
7
|
+
* Used by @storystick/react (and future @storystick/astro, etc.)
|
|
8
|
+
* via dynamic import — only loaded in preview mode.
|
|
9
|
+
*
|
|
10
|
+
* This package provides:
|
|
11
|
+
* - Proxy field tracking (wraps data, records field access)
|
|
12
|
+
* - DOM scanning (MutationObserver matches values to elements)
|
|
13
|
+
* - Overlay rendering (selection, drag handles, "+" buttons)
|
|
14
|
+
* - PostMessage bridge (iframe ↔ Studio communication)
|
|
15
|
+
* - Preview session management
|
|
16
|
+
*/
|
|
17
|
+
import { startSnapshotReporter, } from './runtime/snapshot-reporter.js';
|
|
18
|
+
import { installPreviewWorkspaceShortcuts } from './runtime/workspace-shortcuts.js';
|
|
19
|
+
import { previewRuntime } from './runtime/store.js';
|
|
20
|
+
import { previewPerfNow, recordPreviewPerfMeasure, recordPreviewPerfMetric } from './runtime/perf-metrics.js';
|
|
21
|
+
import { startStandaloneOverlayController } from './runtime/standalone-overlay-controller.js';
|
|
22
|
+
/**
|
|
23
|
+
* SHA-256 hex of `routeUrl`, shared with the SDK document-observation
|
|
24
|
+
* tap and Studio route scoping.
|
|
25
|
+
*
|
|
26
|
+
* Web Crypto is the fast path. When `crypto.subtle` is unavailable —
|
|
27
|
+
* specifically in dev environments where the embedder chain is not a
|
|
28
|
+
* secure context (Studio on `http://storystick.dojo` embedding an
|
|
29
|
+
* iframe at `http://localhost` ; secure context inherits from the
|
|
30
|
+
* chain per W3C Secure Contexts §4.3) — we fall back to the pure-JS
|
|
31
|
+
* SHA-256 in `sha256-fallback.ts`. Output is byte-equivalent to Web
|
|
32
|
+
* Crypto's SHA-256 hex.
|
|
33
|
+
*
|
|
34
|
+
* The fallback is unit-tested for FIPS compliance ; it's a transcription
|
|
35
|
+
* of FIPS PUB 180-4 §6.2. Production paths (HTTPS↔HTTPS) always hit
|
|
36
|
+
* the Web Crypto branch.
|
|
37
|
+
*/
|
|
38
|
+
async function computeRouteHash(routeUrl) {
|
|
39
|
+
// crypto.subtle returns undefined in non-secure contexts ; guard
|
|
40
|
+
// explicitly rather than letting `crypto.subtle.digest` throw a
|
|
41
|
+
// `TypeError: Cannot read properties of undefined` every snapshot tick.
|
|
42
|
+
if (typeof crypto !== 'undefined' &&
|
|
43
|
+
crypto.subtle !== undefined &&
|
|
44
|
+
typeof crypto.subtle.digest === 'function') {
|
|
45
|
+
const bytes = new TextEncoder().encode(routeUrl);
|
|
46
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
47
|
+
return Array.from(new Uint8Array(digest))
|
|
48
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
49
|
+
.join('');
|
|
50
|
+
}
|
|
51
|
+
const { sha256HexFallback } = await import('./sha256-fallback.js');
|
|
52
|
+
return sha256HexFallback(routeUrl);
|
|
53
|
+
}
|
|
54
|
+
import { startFormContractReporter, } from './runtime/form-contract-reporter.js';
|
|
55
|
+
import { validateNavigate, validateOverlayClear, validateOverlayMap, } from './validators.js';
|
|
56
|
+
import { applyPreviewOverlayMap, clearPreviewOverlayPaint, installPreviewEditInteractionGuard, } from './runtime/preview-overlay-painter.js';
|
|
57
|
+
import { getAnchorFromPreviewClickEvent, installMomentaryNavigateKeyRelay, normalizeMomentaryNavigateLinkClick, } from './runtime/momentary-navigate.js';
|
|
58
|
+
import { getNavigateHandler, getReloadHandler, hasNavigateHandler, } from './nav-handler.js';
|
|
59
|
+
import { setRouterSubscriber, } from './router-subscriber.js';
|
|
60
|
+
import { getInteractionMode, setInteractionMode } from './interaction-mode.js';
|
|
61
|
+
import { PREVIEW_NAMESPACE, PREVIEW_PROTOCOL_VERSION, validatePreviewEnvelope, LOCATION_PATHNAME_MAX, LOCATION_SEARCH_MAX, LOCATION_HASH_MAX, ROUTE_RENDERED_CODE_VERSION_MAX, } from '@storystick/transport-allowlist';
|
|
62
|
+
export { isIframeToStudio, isStudioToIframe } from './types.js';
|
|
63
|
+
// ─────────────────────────────────────────────
|
|
64
|
+
// Runtime — in-iframe SDK pieces (PR2', ADR-059 §Technical commitments)
|
|
65
|
+
// ─────────────────────────────────────────────
|
|
66
|
+
// ADR-129 — the atomic-snapshot reporter started by `activate()`.
|
|
67
|
+
export { startSnapshotReporter, } from './runtime/snapshot-reporter.js';
|
|
68
|
+
export { assembleSnapshot, } from './runtime/snapshot-assembly.js';
|
|
69
|
+
export { startFormContractReporter, getStorystickViewport, FORM_PREVIEW_ATTR, } from './runtime/form-contract-reporter.js';
|
|
70
|
+
export { getRenderMode, setRenderMode, subscribeRenderMode, debounceForRenderMode, } from './runtime/render-mode-state.js';
|
|
71
|
+
// ─────────────────────────────────────────────
|
|
72
|
+
// Preview grant lifecycle + control-plane surface
|
|
73
|
+
// ─────────────────────────────────────────────
|
|
74
|
+
//
|
|
75
|
+
// The lifecycle, coordinator, validators, nav-handler, and session-subscriber
|
|
76
|
+
// are split into their own
|
|
77
|
+
// files (per-/plan-eng-review CQ1). The package re-exports the
|
|
78
|
+
// public surface so `@storystick/react`'s provider has a single
|
|
79
|
+
// import.
|
|
80
|
+
export { validateNavigate, validateOverlayClear, validateOverlayMap, } from './validators.js';
|
|
81
|
+
export { legacyPreviewAuthState, transitionPreviewGrantLifecycle, } from './grant-lifecycle.js';
|
|
82
|
+
export { PREVIEW_TOKEN_ROUTE, PREVIEW_GRANT_RETRY_BACKOFF_MS, PreviewGrantRequestError, acquirePreviewBrowserGrant, createPreviewGrantCoordinator, } from './grant-coordinator.js';
|
|
83
|
+
export { setNavigateHandler, getNavigateHandler, setReloadHandler, getReloadHandler, _resetNavigateHandlerForTests, } from './nav-handler.js';
|
|
84
|
+
// ─── ADR-108 — router-subscriber + interaction-mode ──────────────
|
|
85
|
+
export { setRouterSubscriber, getRouterSubscriber, notifyRouterTransition, _resetRouterSubscriberForTests, } from './router-subscriber.js';
|
|
86
|
+
export { getInteractionMode, setInteractionMode, _resetInteractionModeForTests, } from './interaction-mode.js';
|
|
87
|
+
export { subscribePreviewSession, mapPreviewSessionError, } from './session-subscriber.js';
|
|
88
|
+
export { wrapWithProxy, getTrackedFields, clearTrackedFields, collectFieldModel,
|
|
89
|
+
// ADR-126 — RSC render-path field-model island plumbing.
|
|
90
|
+
collectFieldModelFromRoots, registerIslandRoots, clearIslandRoots, isFieldTrackingActive, __setFieldTrackingForcedForTests, } from './runtime/field-proxy.js';
|
|
91
|
+
// ─────────────────────────────────────────────
|
|
92
|
+
// Activate — entry point for framework SDKs
|
|
93
|
+
// ─────────────────────────────────────────────
|
|
94
|
+
/**
|
|
95
|
+
* Activate the preview overlay system.
|
|
96
|
+
* Returns a cleanup function to tear down on unmount.
|
|
97
|
+
*
|
|
98
|
+
* Called by StorystickProvider when preview mode is detected.
|
|
99
|
+
* Auto-starts the in-iframe snapshot reporter so consumers get the
|
|
100
|
+
* postMessage bridge for free.
|
|
101
|
+
*
|
|
102
|
+
* Idempotency: a module-level refcount ensures repeated calls share
|
|
103
|
+
* a single reporter instance. The first activate() starts the
|
|
104
|
+
* reporter; subsequent calls only increment a counter. Each returned
|
|
105
|
+
* cleanup decrements; the reporter stops once the count reaches zero.
|
|
106
|
+
* This makes the function safe under React StrictMode's double-mount
|
|
107
|
+
* pattern and tolerant of accidental multi-Provider setups.
|
|
108
|
+
*
|
|
109
|
+
* A top-level page is eligible only when its scrubbed URL carries an explicit
|
|
110
|
+
* `_ss_session` selector. The reporter starts after the same-origin token route
|
|
111
|
+
* proves the HttpOnly capability; the public selector alone cannot paint
|
|
112
|
+
* overlays. In standalone mode the reporter stays local, without postMessage.
|
|
113
|
+
* Ordinary customer pages remain inert and do not load this package through
|
|
114
|
+
* the public adapter.
|
|
115
|
+
*/
|
|
116
|
+
let activeReporter = null;
|
|
117
|
+
let activeFormContractReporter = null;
|
|
118
|
+
let activeStandaloneOverlayCleanup = null;
|
|
119
|
+
let activeRefCount = 0;
|
|
120
|
+
// A document's Navigation Timing entry outlives activate/cleanup (including
|
|
121
|
+
// StrictMode and grant renewal). Its initial history proof is considered once.
|
|
122
|
+
let initialNavigationObserved = false;
|
|
123
|
+
/**
|
|
124
|
+
* ADR-108 D1 — strip Storystick-internal preview params from a
|
|
125
|
+
* search string before serialising into a `location` envelope OR
|
|
126
|
+
* before comparing observed vs expected tuples for the
|
|
127
|
+
* pendingNavigate match. `_ss_session` is the load-bearing preview
|
|
128
|
+
* gate (drives `detectPreviewMode()`), `_ss_variant` is the
|
|
129
|
+
* variant-shadowing param. Customer-app params (`?utm=...`,
|
|
130
|
+
* `?id=42`) are preserved as-is so the chrome URL display and
|
|
131
|
+
* reverse doc lookup see the actual customer URL.
|
|
132
|
+
*
|
|
133
|
+
* Input: search WITHOUT the leading `?`. Output: same form, with
|
|
134
|
+
* the internal params removed. Empty string → empty string.
|
|
135
|
+
*/
|
|
136
|
+
function stripStorystickPreviewParams(search) {
|
|
137
|
+
if (search === '')
|
|
138
|
+
return '';
|
|
139
|
+
// Defensive: callers normalise to "no leading ?" but tolerate
|
|
140
|
+
// both for the comparison path.
|
|
141
|
+
const cleaned = search.startsWith('?') ? search.slice(1) : search;
|
|
142
|
+
if (cleaned === '')
|
|
143
|
+
return '';
|
|
144
|
+
const params = new URLSearchParams(cleaned);
|
|
145
|
+
for (const key of [...params.keys()]) {
|
|
146
|
+
// Includes the one-shot `_ss_activation` capability. It must never be
|
|
147
|
+
// reflected into Studio location state, navigation commands, or logs.
|
|
148
|
+
if (key.startsWith('_ss_'))
|
|
149
|
+
params.delete(key);
|
|
150
|
+
}
|
|
151
|
+
return params.toString();
|
|
152
|
+
}
|
|
153
|
+
function navigationStartPayloadFromBrowseClick(event, target, win = window) {
|
|
154
|
+
if (event.defaultPrevented)
|
|
155
|
+
return null;
|
|
156
|
+
if (event.button !== 0)
|
|
157
|
+
return null;
|
|
158
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (target.hasAttribute('download'))
|
|
162
|
+
return null;
|
|
163
|
+
if (target.target.length > 0 && target.target !== '_self')
|
|
164
|
+
return null;
|
|
165
|
+
let url;
|
|
166
|
+
try {
|
|
167
|
+
url = new URL(target.href, win.location.href);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (url.origin !== win.location.origin)
|
|
173
|
+
return null;
|
|
174
|
+
const search = stripStorystickPreviewParams(url.search.replace(/^\?/, ''));
|
|
175
|
+
const currentSearch = stripStorystickPreviewParams(win.location.search.replace(/^\?/, ''));
|
|
176
|
+
if (url.pathname === win.location.pathname &&
|
|
177
|
+
search === currentSearch) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
pathname: url.pathname.slice(0, LOCATION_PATHNAME_MAX),
|
|
182
|
+
search: search.slice(0, LOCATION_SEARCH_MAX),
|
|
183
|
+
hash: url.hash.slice(0, LOCATION_HASH_MAX),
|
|
184
|
+
trigger: 'link-click',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* PR-1 — best-effort read of the customer page's code-bundle version,
|
|
189
|
+
* mirroring the same window global the snapshot/overlay path reads
|
|
190
|
+
* (`__STORYSTICK_CODE_VERSION__`). Returns `undefined` when unset so
|
|
191
|
+
* the route-rendered payload omits the optional field rather than
|
|
192
|
+
* sending an empty string the validator would reject.
|
|
193
|
+
*/
|
|
194
|
+
function readCodeVersion() {
|
|
195
|
+
if (typeof window === 'undefined')
|
|
196
|
+
return undefined;
|
|
197
|
+
const v = window
|
|
198
|
+
.__STORYSTICK_CODE_VERSION__;
|
|
199
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
200
|
+
}
|
|
201
|
+
export function activate(config) {
|
|
202
|
+
if (typeof window === 'undefined') {
|
|
203
|
+
return () => {
|
|
204
|
+
// No-op cleanup when there is no DOM (SSR).
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const standalone = readStandalonePreviewSessionId(config.projectId) === config.sessionId && config.standaloneCapability === true;
|
|
208
|
+
if (window.parent === window && !standalone) {
|
|
209
|
+
return () => {
|
|
210
|
+
// No-op cleanup when not embedded — postMessage would loop.
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (standalone && activeStandaloneOverlayCleanup === null) {
|
|
214
|
+
activeStandaloneOverlayCleanup = startStandaloneOverlayController({
|
|
215
|
+
sessionId: config.sessionId,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (!standalone && activeReporter === null) {
|
|
219
|
+
// ADR-129 — atomic slice/field snapshot reporting.
|
|
220
|
+
// The route hash is cached per href to avoid recomputing SHA-256 on
|
|
221
|
+
// every observer-driven publish.
|
|
222
|
+
let cachedRouteHash = null;
|
|
223
|
+
activeReporter = startSnapshotReporter({
|
|
224
|
+
routeHashRef: () => {
|
|
225
|
+
if (typeof window === 'undefined')
|
|
226
|
+
return null;
|
|
227
|
+
const href = window.location.href;
|
|
228
|
+
if (cachedRouteHash !== null && cachedRouteHash.href === href) {
|
|
229
|
+
return cachedRouteHash.hash;
|
|
230
|
+
}
|
|
231
|
+
// Compute lazily on the next microtask ; first publish under
|
|
232
|
+
// a brand-new URL will see null (no overlay) until the SHA
|
|
233
|
+
// resolves, then subsequent ones hit the cache.
|
|
234
|
+
void computeRouteHash(href).then((hash) => {
|
|
235
|
+
cachedRouteHash = { href, hash };
|
|
236
|
+
// Wake the reporter once routeHash settles so Studio receives
|
|
237
|
+
// the scoped snapshot without waiting for another mutation.
|
|
238
|
+
activeReporter?.kick('route-hash-ready');
|
|
239
|
+
});
|
|
240
|
+
return null;
|
|
241
|
+
},
|
|
242
|
+
routeUrlRef: () => typeof window !== 'undefined' ? window.location.href : null,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// ADR-119 D2/D3 — Form-preview viewport-contract reporter. Same
|
|
246
|
+
// refcount + iframe-gate as the siblings: every framework SDK gets
|
|
247
|
+
// the `data-storystick-form-preview` contract + height ACK for free.
|
|
248
|
+
if (!standalone && activeFormContractReporter === null) {
|
|
249
|
+
activeFormContractReporter = startFormContractReporter();
|
|
250
|
+
}
|
|
251
|
+
activeRefCount += 1;
|
|
252
|
+
// The postMessage channel is a secret-free control plane. Browser grants are
|
|
253
|
+
// acquired independently by runtime/store through the same-origin token route.
|
|
254
|
+
let controlCleanup = null;
|
|
255
|
+
let aborted = false;
|
|
256
|
+
if (!standalone && config.projectId) {
|
|
257
|
+
const projectId = config.projectId;
|
|
258
|
+
const parentOriginLock = config.parentOrigin ?? null;
|
|
259
|
+
let teardownEditInteractionGuard = null;
|
|
260
|
+
let teardownMomentaryNavigateKeyRelay = null;
|
|
261
|
+
const navigations = new Map();
|
|
262
|
+
let pendingNavigate = null;
|
|
263
|
+
let nativeNavigation = null;
|
|
264
|
+
let lastNavigateDispatch = null;
|
|
265
|
+
/**
|
|
266
|
+
* Emit a `storystick.preview.location` envelope to the parent
|
|
267
|
+
* Studio. Strips `_ss_session`, `_ss_variant`, and `_ss_release` from the search
|
|
268
|
+
* before sending (preview-channel internals, not customer-app
|
|
269
|
+
* query state). Defensive length-cap pre-truncation via slice —
|
|
270
|
+
* Studio also validates, this is belt-and-braces.
|
|
271
|
+
*/
|
|
272
|
+
function emitLocation(pathname, search, hash, commandId) {
|
|
273
|
+
if (parentOriginLock === null)
|
|
274
|
+
return;
|
|
275
|
+
const sanitizedSearch = stripStorystickPreviewParams(search);
|
|
276
|
+
const envelope = {
|
|
277
|
+
namespace: PREVIEW_NAMESPACE,
|
|
278
|
+
version: PREVIEW_PROTOCOL_VERSION,
|
|
279
|
+
type: 'storystick.preview.location',
|
|
280
|
+
sessionId: config.sessionId,
|
|
281
|
+
projectId,
|
|
282
|
+
requestId: createPreviewRequestId(),
|
|
283
|
+
nonce: createPreviewRequestId(),
|
|
284
|
+
sentAt: Date.now(),
|
|
285
|
+
payload: {
|
|
286
|
+
pathname: pathname.slice(0, LOCATION_PATHNAME_MAX),
|
|
287
|
+
search: sanitizedSearch.slice(0, LOCATION_SEARCH_MAX),
|
|
288
|
+
hash: hash.slice(0, LOCATION_HASH_MAX),
|
|
289
|
+
...(commandId !== undefined && { commandId }),
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
window.parent.postMessage(envelope, parentOriginLock);
|
|
293
|
+
}
|
|
294
|
+
function emitNavigationStart(payload) {
|
|
295
|
+
if (parentOriginLock === null)
|
|
296
|
+
return;
|
|
297
|
+
const envelope = {
|
|
298
|
+
namespace: PREVIEW_NAMESPACE,
|
|
299
|
+
version: PREVIEW_PROTOCOL_VERSION,
|
|
300
|
+
type: 'storystick.preview.navigation-start',
|
|
301
|
+
sessionId: config.sessionId,
|
|
302
|
+
projectId,
|
|
303
|
+
requestId: createPreviewRequestId(),
|
|
304
|
+
nonce: createPreviewRequestId(),
|
|
305
|
+
sentAt: Date.now(),
|
|
306
|
+
payload,
|
|
307
|
+
};
|
|
308
|
+
window.parent.postMessage(envelope, parentOriginLock);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* PR-1 — emit a `storystick.preview.route-rendered` ack for a
|
|
312
|
+
* Studio-commanded navigation that has reached AND painted its
|
|
313
|
+
* destination. Always carries the matched `commandId` (the caller
|
|
314
|
+
* only invokes this when a commandId was attached). `aborted` +
|
|
315
|
+
* `parentOriginLock` guards mirror `emitLocation` so a deferred
|
|
316
|
+
* emit after teardown is a silent no-op.
|
|
317
|
+
*/
|
|
318
|
+
function emitRouteRendered(commandId, presentation) {
|
|
319
|
+
if (aborted)
|
|
320
|
+
return;
|
|
321
|
+
if (lastNavigateDispatch?.commandId === commandId)
|
|
322
|
+
lastNavigateDispatch.rendered = true;
|
|
323
|
+
if (parentOriginLock === null)
|
|
324
|
+
return;
|
|
325
|
+
// codeVersion is optional telemetry. If the customer page set a
|
|
326
|
+
// pathologically long `__STORYSTICK_CODE_VERSION__`, including it
|
|
327
|
+
// would push the payload past the Studio validator's bound and
|
|
328
|
+
// DROP the whole envelope — losing the load-bearing commandId ack.
|
|
329
|
+
// Omit it instead so the confirmation signal always survives
|
|
330
|
+
// (codex PR-1 review §P2).
|
|
331
|
+
const codeVersionRaw = readCodeVersion();
|
|
332
|
+
const codeVersion = codeVersionRaw !== undefined &&
|
|
333
|
+
codeVersionRaw.length <= ROUTE_RENDERED_CODE_VERSION_MAX
|
|
334
|
+
? codeVersionRaw
|
|
335
|
+
: undefined;
|
|
336
|
+
const envelope = {
|
|
337
|
+
namespace: PREVIEW_NAMESPACE,
|
|
338
|
+
version: PREVIEW_PROTOCOL_VERSION,
|
|
339
|
+
type: 'storystick.preview.route-rendered',
|
|
340
|
+
sessionId: config.sessionId,
|
|
341
|
+
projectId,
|
|
342
|
+
requestId: createPreviewRequestId(),
|
|
343
|
+
nonce: createPreviewRequestId(),
|
|
344
|
+
sentAt: Date.now(),
|
|
345
|
+
payload: {
|
|
346
|
+
commandId,
|
|
347
|
+
...(presentation ? { presentation } : {}),
|
|
348
|
+
...(codeVersion !== undefined && { codeVersion }),
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
window.parent.postMessage(envelope, parentOriginLock);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Defer the route-rendered ack until after the browser has painted
|
|
355
|
+
* the committed route. Uses a DOUBLE rAF: the second callback runs
|
|
356
|
+
* after the paint of the frame the first callback observed, so the
|
|
357
|
+
* ack is post-paint REGARDLESS of how the adapter delivered the
|
|
358
|
+
* transition — from inside its own rAF (Next App Router), from a
|
|
359
|
+
* router event (Pages Router `routeChangeComplete`), or
|
|
360
|
+
* synchronously during a commit (a future adapter). A single rAF is
|
|
361
|
+
* post-paint ONLY when the transition was itself delivered from a
|
|
362
|
+
* prior rAF, so it could clear the stale mask pre-paint on the other
|
|
363
|
+
* delivery paths (codex PR-1 review §P1). SSR / jsdom-without-rAF
|
|
364
|
+
* emits synchronously so tests stay deterministic.
|
|
365
|
+
*/
|
|
366
|
+
function scheduleRouteRenderedAck(commandId, presentation) {
|
|
367
|
+
if (parentOriginLock === null)
|
|
368
|
+
return;
|
|
369
|
+
if (typeof window === 'undefined' ||
|
|
370
|
+
typeof window.requestAnimationFrame !== 'function') {
|
|
371
|
+
emitRouteRendered(commandId, presentation);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const startedAt = previewPerfNow(window);
|
|
375
|
+
window.requestAnimationFrame(() => {
|
|
376
|
+
window.requestAnimationFrame(() => {
|
|
377
|
+
if (!aborted)
|
|
378
|
+
recordPreviewPerfMeasure(window, 'preview.navigate.paintAck', startedAt, { hasPresentation: presentation !== undefined });
|
|
379
|
+
emitRouteRendered(commandId, presentation);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function emitOverlayReady(payload) {
|
|
384
|
+
if (parentOriginLock === null)
|
|
385
|
+
return;
|
|
386
|
+
const envelope = {
|
|
387
|
+
namespace: PREVIEW_NAMESPACE,
|
|
388
|
+
version: PREVIEW_PROTOCOL_VERSION,
|
|
389
|
+
type: 'storystick.preview.overlay-ready',
|
|
390
|
+
sessionId: config.sessionId,
|
|
391
|
+
projectId,
|
|
392
|
+
requestId: createPreviewRequestId(),
|
|
393
|
+
nonce: createPreviewRequestId(),
|
|
394
|
+
sentAt: Date.now(),
|
|
395
|
+
payload,
|
|
396
|
+
};
|
|
397
|
+
window.parent.postMessage(envelope, parentOriginLock);
|
|
398
|
+
}
|
|
399
|
+
function emitOverlayClick(payload) {
|
|
400
|
+
if (parentOriginLock === null)
|
|
401
|
+
return;
|
|
402
|
+
const envelope = {
|
|
403
|
+
namespace: PREVIEW_NAMESPACE,
|
|
404
|
+
version: PREVIEW_PROTOCOL_VERSION,
|
|
405
|
+
type: 'storystick.preview.overlay-click',
|
|
406
|
+
sessionId: config.sessionId,
|
|
407
|
+
projectId,
|
|
408
|
+
requestId: createPreviewRequestId(),
|
|
409
|
+
nonce: createPreviewRequestId(),
|
|
410
|
+
sentAt: Date.now(),
|
|
411
|
+
payload,
|
|
412
|
+
};
|
|
413
|
+
window.parent.postMessage(envelope, parentOriginLock);
|
|
414
|
+
}
|
|
415
|
+
function emitIframeInteraction(payload) {
|
|
416
|
+
if (parentOriginLock === null)
|
|
417
|
+
return;
|
|
418
|
+
const envelope = {
|
|
419
|
+
namespace: PREVIEW_NAMESPACE,
|
|
420
|
+
version: PREVIEW_PROTOCOL_VERSION,
|
|
421
|
+
type: 'storystick.preview.iframe-interaction',
|
|
422
|
+
sessionId: config.sessionId,
|
|
423
|
+
projectId,
|
|
424
|
+
requestId: createPreviewRequestId(),
|
|
425
|
+
nonce: createPreviewRequestId(),
|
|
426
|
+
sentAt: Date.now(),
|
|
427
|
+
payload,
|
|
428
|
+
};
|
|
429
|
+
window.parent.postMessage(envelope, parentOriginLock);
|
|
430
|
+
}
|
|
431
|
+
let lastRouterLocation = {
|
|
432
|
+
pathname: window.location.pathname,
|
|
433
|
+
search: stripStorystickPreviewParams(window.location.search),
|
|
434
|
+
hash: window.location.hash,
|
|
435
|
+
};
|
|
436
|
+
function onRouterTransition(transition) {
|
|
437
|
+
const committed = {
|
|
438
|
+
pathname: transition.pathname,
|
|
439
|
+
search: stripStorystickPreviewParams(transition.search),
|
|
440
|
+
hash: transition.hash,
|
|
441
|
+
};
|
|
442
|
+
if (transition.commandId !== undefined) {
|
|
443
|
+
navigations.delete(transition.commandId);
|
|
444
|
+
if (pendingNavigate?.commandId === transition.commandId)
|
|
445
|
+
pendingNavigate = null;
|
|
446
|
+
lastRouterLocation = committed;
|
|
447
|
+
emitLocation(transition.pathname, transition.search, transition.hash, transition.commandId);
|
|
448
|
+
scheduleRouteRenderedAck(transition.commandId, transition.presentation);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const matchesNative = nativeNavigation !== null &&
|
|
452
|
+
(nativeNavigation.pathname === committed.pathname || nativeNavigation.pathname === transition.browserPathname) &&
|
|
453
|
+
nativeNavigation.search === committed.search &&
|
|
454
|
+
nativeNavigation.hash === committed.hash;
|
|
455
|
+
const matches = (command) => command.expectedPathname === committed.pathname &&
|
|
456
|
+
stripStorystickPreviewParams(command.expectedSearch) === committed.search &&
|
|
457
|
+
command.expectedHash === committed.hash;
|
|
458
|
+
const matched = matchesNative ? undefined :
|
|
459
|
+
[...navigations.values()].reverse().find(matches);
|
|
460
|
+
const command = matchesNative ? null : matched ?? pendingNavigate;
|
|
461
|
+
const commandId = command?.commandId;
|
|
462
|
+
if (matchesNative) {
|
|
463
|
+
// Browser gestures include a framework basePath; adapter commits use
|
|
464
|
+
// customer route coordinates. Correlate the normalized intent too.
|
|
465
|
+
if (nativeNavigation.pathname !== committed.pathname) {
|
|
466
|
+
emitNavigationStart({ ...committed, trigger: nativeNavigation.trigger });
|
|
467
|
+
}
|
|
468
|
+
nativeNavigation = null;
|
|
469
|
+
pendingNavigate = null;
|
|
470
|
+
}
|
|
471
|
+
else if (matched?.commandId === pendingNavigate?.commandId) {
|
|
472
|
+
pendingNavigate = null;
|
|
473
|
+
}
|
|
474
|
+
if (matched !== undefined)
|
|
475
|
+
navigations.delete(matched.commandId);
|
|
476
|
+
const changed = committed.pathname !== lastRouterLocation.pathname ||
|
|
477
|
+
committed.search !== lastRouterLocation.search ||
|
|
478
|
+
committed.hash !== lastRouterLocation.hash;
|
|
479
|
+
lastRouterLocation = committed;
|
|
480
|
+
if (changed && commandId === undefined && !matchesNative) {
|
|
481
|
+
emitNavigationStart({ ...committed, trigger: 'router-transition' });
|
|
482
|
+
}
|
|
483
|
+
emitLocation(transition.pathname, transition.search, transition.hash, commandId);
|
|
484
|
+
// A redirect retains provenance but does not confirm the requested URL.
|
|
485
|
+
if (matched !== undefined)
|
|
486
|
+
scheduleRouteRenderedAck(matched.commandId);
|
|
487
|
+
}
|
|
488
|
+
setRouterSubscriber(onRouterTransition);
|
|
489
|
+
const onBrowseLinkClick = (event) => {
|
|
490
|
+
if (event.defaultPrevented)
|
|
491
|
+
return;
|
|
492
|
+
const target = getAnchorFromPreviewClickEvent(event, window);
|
|
493
|
+
if (target === null)
|
|
494
|
+
return;
|
|
495
|
+
if (getInteractionMode() !== 'browse')
|
|
496
|
+
return;
|
|
497
|
+
normalizeMomentaryNavigateLinkClick(event, target, window);
|
|
498
|
+
const navigationStart = navigationStartPayloadFromBrowseClick(event, target, window);
|
|
499
|
+
if (navigationStart !== null) {
|
|
500
|
+
nativeNavigation = navigationStart;
|
|
501
|
+
pendingNavigate = null;
|
|
502
|
+
emitNavigationStart(navigationStart);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
window.document.addEventListener('click', onBrowseLinkClick, true);
|
|
506
|
+
const onHistoryNavigation = () => {
|
|
507
|
+
nativeNavigation = {
|
|
508
|
+
pathname: window.location.pathname,
|
|
509
|
+
search: stripStorystickPreviewParams(window.location.search),
|
|
510
|
+
hash: window.location.hash,
|
|
511
|
+
trigger: 'router-transition',
|
|
512
|
+
};
|
|
513
|
+
pendingNavigate = null;
|
|
514
|
+
emitNavigationStart(nativeNavigation);
|
|
515
|
+
// History already committed the browser URL. A restored React tree may
|
|
516
|
+
// not rerender, so no adapter callback is guaranteed to report it again.
|
|
517
|
+
emitLocation(nativeNavigation.pathname, nativeNavigation.search, nativeNavigation.hash, undefined);
|
|
518
|
+
};
|
|
519
|
+
window.addEventListener('popstate', onHistoryNavigation);
|
|
520
|
+
const onPageShow = (event) => {
|
|
521
|
+
if (event.persisted)
|
|
522
|
+
onHistoryNavigation();
|
|
523
|
+
};
|
|
524
|
+
window.addEventListener('pageshow', onPageShow);
|
|
525
|
+
teardownEditInteractionGuard = installPreviewEditInteractionGuard(window, {
|
|
526
|
+
onInteraction: emitIframeInteraction,
|
|
527
|
+
});
|
|
528
|
+
const teardownWorkspaceShortcuts = installPreviewWorkspaceShortcuts(window, () => {
|
|
529
|
+
const state = previewRuntime.getState();
|
|
530
|
+
return state.previewEnded ? null : state.identity;
|
|
531
|
+
}, undefined, previewRuntime.subscribeIdentity);
|
|
532
|
+
teardownMomentaryNavigateKeyRelay = installMomentaryNavigateKeyRelay(window, {
|
|
533
|
+
getTargetOrigin: () => parentOriginLock,
|
|
534
|
+
});
|
|
535
|
+
let initialHistoryNavigation = false;
|
|
536
|
+
if (!initialNavigationObserved && parentOriginLock !== null) {
|
|
537
|
+
initialNavigationObserved = true;
|
|
538
|
+
const navigation = window.performance.getEntriesByType?.('navigation')?.[0];
|
|
539
|
+
if (navigation?.type === 'back_forward') {
|
|
540
|
+
try {
|
|
541
|
+
const loaded = new URL(navigation.name);
|
|
542
|
+
// A late activation after SPA navigation cannot lend an older
|
|
543
|
+
// document's history proof to its new address. Internal selectors
|
|
544
|
+
// and the current fragment do not change the loaded document.
|
|
545
|
+
initialHistoryNavigation = loaded.origin === window.location.origin &&
|
|
546
|
+
loaded.pathname === window.location.pathname &&
|
|
547
|
+
stripStorystickPreviewParams(loaded.search) === stripStorystickPreviewParams(window.location.search);
|
|
548
|
+
}
|
|
549
|
+
catch { /* An unavailable browser proof leaves boot observational. */ }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (initialHistoryNavigation)
|
|
553
|
+
onHistoryNavigation();
|
|
554
|
+
else
|
|
555
|
+
emitLocation(window.location.pathname, window.location.search.replace(/^\?/, ''), window.location.hash, undefined);
|
|
556
|
+
// Listen for cross-frame `navigate` messages. The validator
|
|
557
|
+
// rejects pre-lock messages outright (codex Lane D round 1 P1
|
|
558
|
+
// fix) so a same-host script with a valid envelope shape can't
|
|
559
|
+
// drive `history.pushState` before the legitimate Studio
|
|
560
|
+
// handshake finishes.
|
|
561
|
+
//
|
|
562
|
+
// ADR-108 — same listener also routes `interaction-mode` envelopes
|
|
563
|
+
// (Studio→iframe click-routing flag).
|
|
564
|
+
const onMessage = (event) => {
|
|
565
|
+
const base = validatePreviewEnvelope(event.data);
|
|
566
|
+
if (base.ok &&
|
|
567
|
+
base.message.type === 'storystick.preview.session-broadcast' &&
|
|
568
|
+
parentOriginLock !== null &&
|
|
569
|
+
event.origin === parentOriginLock &&
|
|
570
|
+
base.message.sessionId === config.sessionId &&
|
|
571
|
+
base.message.projectId === projectId) {
|
|
572
|
+
config.onSessionBroadcast?.();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (window.__storystickPreviewOverlayMapOwner !== 'boot') {
|
|
576
|
+
const overlayMap = validateOverlayMap(event.data, event.origin, config.sessionId, projectId, parentOriginLock);
|
|
577
|
+
if (overlayMap.ok) {
|
|
578
|
+
const result = applyPreviewOverlayMap(overlayMap.envelope.payload, window, {
|
|
579
|
+
onClick: emitOverlayClick,
|
|
580
|
+
});
|
|
581
|
+
emitOverlayReady({
|
|
582
|
+
generation: result.generation,
|
|
583
|
+
paintedCount: result.paintedCount,
|
|
584
|
+
missingSnapshotItemKeys: result.missingSnapshotItemKeys,
|
|
585
|
+
routeHash: overlayMap.envelope.payload.routeHash,
|
|
586
|
+
codeVersion: overlayMap.envelope.payload.codeVersion,
|
|
587
|
+
renderId: overlayMap.envelope.payload.renderId,
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const overlayClear = validateOverlayClear(event.data, event.origin, config.sessionId, projectId, parentOriginLock);
|
|
592
|
+
if (overlayClear.ok) {
|
|
593
|
+
clearPreviewOverlayPaint();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// ADR-108 D2 — interaction-mode envelope routed first because
|
|
598
|
+
// it has its own discriminator + parentOrigin lock guard. The
|
|
599
|
+
// `validateNavigate` path below short-circuits on the wrong
|
|
600
|
+
// type so this ordering doesn't conflict.
|
|
601
|
+
if (base.ok &&
|
|
602
|
+
base.message.type === 'storystick.preview.interaction-mode') {
|
|
603
|
+
if (parentOriginLock === null)
|
|
604
|
+
return;
|
|
605
|
+
if (event.origin !== parentOriginLock)
|
|
606
|
+
return;
|
|
607
|
+
if (base.message.sessionId !== config.sessionId)
|
|
608
|
+
return;
|
|
609
|
+
if (base.message.projectId !== projectId)
|
|
610
|
+
return;
|
|
611
|
+
setInteractionMode(base.message.payload.mode);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (base.ok && base.message.type === 'storystick.preview.reload') {
|
|
615
|
+
if (parentOriginLock === null)
|
|
616
|
+
return;
|
|
617
|
+
if (event.origin !== parentOriginLock)
|
|
618
|
+
return;
|
|
619
|
+
if (base.message.sessionId !== config.sessionId)
|
|
620
|
+
return;
|
|
621
|
+
if (base.message.projectId !== projectId)
|
|
622
|
+
return;
|
|
623
|
+
const handler = getReloadHandler();
|
|
624
|
+
handler(base.message.payload.mode, {
|
|
625
|
+
commandId: base.message.payload.commandId,
|
|
626
|
+
});
|
|
627
|
+
// C3 — a soft reload is commonly implemented by framework
|
|
628
|
+
// adapters as `router.refresh()`. The displayed URL does not
|
|
629
|
+
// change in that case, so the router-location subscriber has no
|
|
630
|
+
// transition to report. Studio deliberately clears its observed
|
|
631
|
+
// renderer state before dispatching reload; explicitly re-observe
|
|
632
|
+
// the current route so a later overlay-ready ACK can be matched to
|
|
633
|
+
// the unchanged displayed epoch instead of being rejected as
|
|
634
|
+
// route-drift. This is route evidence only — paint readiness still
|
|
635
|
+
// requires the generation-scoped overlay-ready ACK.
|
|
636
|
+
if (base.message.payload.mode === 'soft') {
|
|
637
|
+
emitLocation(window.location.pathname, window.location.search.replace(/^\?/, ''), window.location.hash, undefined);
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const check = validateNavigate(event.data, event.origin, config.sessionId, projectId, parentOriginLock);
|
|
642
|
+
if (!check.ok)
|
|
643
|
+
return;
|
|
644
|
+
// ADR-108 D3 — capture the expected-destination tuple BEFORE
|
|
645
|
+
// invoking the framework adapter's handler so the next
|
|
646
|
+
// observed router transition can match-and-attach. The
|
|
647
|
+
// navigate envelope's `pathname` may include `?...#...`
|
|
648
|
+
// (validator allows /path?q=1#h); split it into the three
|
|
649
|
+
// parts so the subscriber-reported tuple can be compared
|
|
650
|
+
// directly.
|
|
651
|
+
const rawPath = check.envelope.payload.pathname;
|
|
652
|
+
const hashIdx = rawPath.indexOf('#');
|
|
653
|
+
const hash = hashIdx >= 0 ? rawPath.slice(hashIdx) : '';
|
|
654
|
+
const pathWithQuery = hashIdx >= 0 ? rawPath.slice(0, hashIdx) : rawPath;
|
|
655
|
+
const queryIdx = pathWithQuery.indexOf('?');
|
|
656
|
+
const expectedPathname = queryIdx >= 0 ? pathWithQuery.slice(0, queryIdx) : pathWithQuery;
|
|
657
|
+
const expectedSearch = queryIdx >= 0 ? pathWithQuery.slice(queryIdx + 1) : '';
|
|
658
|
+
const handler = getNavigateHandler();
|
|
659
|
+
if (lastNavigateDispatch?.commandId === check.envelope.payload.commandId &&
|
|
660
|
+
lastNavigateDispatch.handler === handler) {
|
|
661
|
+
// A lost ACK may cause an already-rendered command to be delivered again.
|
|
662
|
+
// Re-ack it without re-arming a navigation that could capture a future Browse.
|
|
663
|
+
if (lastNavigateDispatch.rendered && lastRouterLocation.pathname === expectedPathname &&
|
|
664
|
+
lastRouterLocation.search === stripStorystickPreviewParams(expectedSearch) &&
|
|
665
|
+
lastRouterLocation.hash === hash) {
|
|
666
|
+
emitLocation(lastRouterLocation.pathname, lastRouterLocation.search, hash, lastNavigateDispatch.commandId);
|
|
667
|
+
scheduleRouteRenderedAck(lastNavigateDispatch.commandId);
|
|
668
|
+
}
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
pendingNavigate = {
|
|
672
|
+
commandId: check.envelope.payload.commandId,
|
|
673
|
+
expectedPathname,
|
|
674
|
+
expectedSearch,
|
|
675
|
+
expectedHash: hash,
|
|
676
|
+
};
|
|
677
|
+
navigations.set(pendingNavigate.commandId, pendingNavigate);
|
|
678
|
+
while (navigations.size > 64)
|
|
679
|
+
navigations.delete(navigations.keys().next().value);
|
|
680
|
+
nativeNavigation = null;
|
|
681
|
+
// Studio retries until route-rendered because postMessage acceptance is
|
|
682
|
+
// not delivery proof. Do not restart a slow framework navigation for the
|
|
683
|
+
// same adapter; do retry when no adapter was ready yet or HMR installed a
|
|
684
|
+
// new handler function for the still-pending command.
|
|
685
|
+
if (!hasNavigateHandler())
|
|
686
|
+
return;
|
|
687
|
+
lastNavigateDispatch = {
|
|
688
|
+
commandId: check.envelope.payload.commandId,
|
|
689
|
+
handler,
|
|
690
|
+
rendered: false,
|
|
691
|
+
};
|
|
692
|
+
recordPreviewPerfMetric(window, { name: 'preview.navigate.received', detail: {
|
|
693
|
+
refresh: check.envelope.payload.refresh === true,
|
|
694
|
+
contextCommand: check.envelope.payload.contextCommand !== undefined,
|
|
695
|
+
} });
|
|
696
|
+
// ADR-111 — forward the optional `refresh` flag so App Router
|
|
697
|
+
// adapters can invalidate the client Router Cache on layout-
|
|
698
|
+
// preview-state changes. Validator guarantees `refresh` is a
|
|
699
|
+
// boolean when present; we coerce to a strict boolean here so
|
|
700
|
+
// adapters never see `undefined`-as-truthy.
|
|
701
|
+
const refresh = check.envelope.payload.refresh === true;
|
|
702
|
+
handler(check.envelope.payload.pathname, {
|
|
703
|
+
refresh,
|
|
704
|
+
scope: check.envelope.payload.scope,
|
|
705
|
+
commandId: check.envelope.payload.commandId,
|
|
706
|
+
...(check.envelope.payload.contextCommand ? {
|
|
707
|
+
contextCommand: check.envelope.payload.contextCommand,
|
|
708
|
+
} : {}),
|
|
709
|
+
});
|
|
710
|
+
};
|
|
711
|
+
window.addEventListener('message', onMessage);
|
|
712
|
+
controlCleanup = () => {
|
|
713
|
+
aborted = true;
|
|
714
|
+
window.removeEventListener('message', onMessage);
|
|
715
|
+
window.document.removeEventListener('click', onBrowseLinkClick, true);
|
|
716
|
+
window.removeEventListener('popstate', onHistoryNavigation);
|
|
717
|
+
window.removeEventListener('pageshow', onPageShow);
|
|
718
|
+
teardownWorkspaceShortcuts();
|
|
719
|
+
teardownMomentaryNavigateKeyRelay?.();
|
|
720
|
+
teardownMomentaryNavigateKeyRelay = null;
|
|
721
|
+
teardownEditInteractionGuard?.();
|
|
722
|
+
teardownEditInteractionGuard = null;
|
|
723
|
+
// ADR-108 — clear router subscriber so StrictMode remount or
|
|
724
|
+
// future activate() doesn't see a stale handler from the
|
|
725
|
+
// teardown lane.
|
|
726
|
+
setRouterSubscriber(null);
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
let released = false;
|
|
730
|
+
return () => {
|
|
731
|
+
if (released)
|
|
732
|
+
return;
|
|
733
|
+
released = true;
|
|
734
|
+
if (controlCleanup !== null) {
|
|
735
|
+
controlCleanup();
|
|
736
|
+
controlCleanup = null;
|
|
737
|
+
}
|
|
738
|
+
activeRefCount -= 1;
|
|
739
|
+
if (activeRefCount <= 0) {
|
|
740
|
+
activeRefCount = 0;
|
|
741
|
+
activeReporter?.stop();
|
|
742
|
+
activeReporter = null;
|
|
743
|
+
activeFormContractReporter?.stop();
|
|
744
|
+
activeFormContractReporter = null;
|
|
745
|
+
activeStandaloneOverlayCleanup?.();
|
|
746
|
+
activeStandaloneOverlayCleanup = null;
|
|
747
|
+
clearPreviewOverlayPaint();
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Test-only hook to drop the singleton between cases. Vitest modules
|
|
753
|
+
* are isolated per file, but a single file with multiple `activate`
|
|
754
|
+
* calls would share state without this reset.
|
|
755
|
+
*/
|
|
756
|
+
export function _resetActivateForTests() {
|
|
757
|
+
activeReporter?.stop();
|
|
758
|
+
activeReporter = null;
|
|
759
|
+
activeFormContractReporter?.stop();
|
|
760
|
+
activeFormContractReporter = null;
|
|
761
|
+
activeStandaloneOverlayCleanup?.();
|
|
762
|
+
activeStandaloneOverlayCleanup = null;
|
|
763
|
+
clearPreviewOverlayPaint();
|
|
764
|
+
activeRefCount = 0;
|
|
765
|
+
initialNavigationObserved = false;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Emit a `PreviewNavMessage` to the parent Studio. No-op when not
|
|
769
|
+
* embedded (top-level window or SSR) — same guard as `activate`.
|
|
770
|
+
*
|
|
771
|
+
* Origin :
|
|
772
|
+
* - When `targetOrigin` is omitted, defaults to `window.location.origin`.
|
|
773
|
+
* Per ADR-041 the studio + preview share a cookie domain, which means
|
|
774
|
+
* they share an origin in practice — same-origin posting works.
|
|
775
|
+
* - Pass an explicit `targetOrigin` if a future deployment puts the
|
|
776
|
+
* preview on a different origin (cross-domain) ; the studio listener
|
|
777
|
+
* would then need a documented allowlist on its side.
|
|
778
|
+
*/
|
|
779
|
+
export function postNavigateToParent(msg, targetOrigin) {
|
|
780
|
+
if (typeof window === 'undefined')
|
|
781
|
+
return;
|
|
782
|
+
if (window.parent === window)
|
|
783
|
+
return;
|
|
784
|
+
const origin = targetOrigin ?? window.location.origin;
|
|
785
|
+
window.parent.postMessage(msg, origin);
|
|
786
|
+
}
|
|
787
|
+
// The Proxy tracking surface is implemented in `runtime/field-proxy.ts`
|
|
788
|
+
// and re-exported near the top of this module.
|