@dfosco/storyboard 0.6.13 → 0.6.14
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
|
@@ -399,12 +399,7 @@ export async function mountStoryboardCore(config = {}, options = {}) {
|
|
|
399
399
|
if (currentHref !== lastHref) {
|
|
400
400
|
lastHref = currentHref
|
|
401
401
|
const basePath = (import.meta.env?.BASE_URL || '/').replace(/\/$/, '')
|
|
402
|
-
const
|
|
403
|
-
const hash = window.location.hash
|
|
404
|
-
const stripped = basePath && pathname.startsWith(basePath)
|
|
405
|
-
? pathname.slice(basePath.length) || '/'
|
|
406
|
-
: pathname.replace(/^\/branch--[^/]+/, '') || '/'
|
|
407
|
-
const src = stripped + hash
|
|
402
|
+
const src = computeEmbedNavigationSrc(window.location.pathname, window.location.hash, basePath)
|
|
408
403
|
window.parent.postMessage({ type: 'storyboard:embed:navigate', src }, '*')
|
|
409
404
|
}
|
|
410
405
|
}
|
|
@@ -462,6 +457,36 @@ export async function mountStoryboardCore(config = {}, options = {}) {
|
|
|
462
457
|
handlePendingNavigation()
|
|
463
458
|
}
|
|
464
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Compute the route src to broadcast to the parent canvas from a prototype embed iframe.
|
|
462
|
+
*
|
|
463
|
+
* In **dev mode**, the embed iframe is loaded via the isolation entry
|
|
464
|
+
* (`/prototypes.html#/<route>`), so `pathname` is `/prototypes.html` and the
|
|
465
|
+
* actual route lives in the hash. We strip the loader path and report the
|
|
466
|
+
* hash content as the src. Otherwise, the parent canvas would persist
|
|
467
|
+
* `/prototypes.html#/<route>` as the widget src — and then re-build the
|
|
468
|
+
* iframe URL as `/prototypes.html?proto=prototypes.html#/...`, which loads
|
|
469
|
+
* a blank page. Duplicating that widget (cmd+D) inherits the broken src.
|
|
470
|
+
*
|
|
471
|
+
* In **prod mode**, the iframe loads the prototype route directly through
|
|
472
|
+
* the canvas SPA, so `pathname` already contains the route — we just strip
|
|
473
|
+
* the base path (and any `/branch--xxx/` deploy prefix) and append the hash.
|
|
474
|
+
*
|
|
475
|
+
* Exported for tests.
|
|
476
|
+
*/
|
|
477
|
+
export function computeEmbedNavigationSrc(pathname, hash, basePath = '') {
|
|
478
|
+
const cleanBase = (basePath || '').replace(/\/$/, '')
|
|
479
|
+
const stripped = cleanBase && pathname.startsWith(cleanBase)
|
|
480
|
+
? pathname.slice(cleanBase.length) || '/'
|
|
481
|
+
: pathname.replace(/^\/branch--[^/]+/, '') || '/'
|
|
482
|
+
if (stripped === '/prototypes.html' || stripped.endsWith('/prototypes.html')) {
|
|
483
|
+
const inner = (hash || '').startsWith('#') ? hash.slice(1) : (hash || '')
|
|
484
|
+
if (!inner) return '/'
|
|
485
|
+
return inner.startsWith('/') ? inner : `/${inner}`
|
|
486
|
+
}
|
|
487
|
+
return stripped + (hash || '')
|
|
488
|
+
}
|
|
489
|
+
|
|
465
490
|
/**
|
|
466
491
|
* Check sessionStorage for a pending navigation target.
|
|
467
492
|
* Used by PageSelector when creating a new canvas page — Vite does a full-reload
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { computeEmbedNavigationSrc } from './mountStoryboardCore.js'
|
|
2
|
+
|
|
3
|
+
describe('computeEmbedNavigationSrc', () => {
|
|
4
|
+
describe('prod mode (direct route in pathname)', () => {
|
|
5
|
+
it('strips basePath and appends hash', () => {
|
|
6
|
+
expect(computeEmbedNavigationSrc('/cq-org-enablement', '#sectionA', '/')).toBe(
|
|
7
|
+
'/cq-org-enablement#sectionA',
|
|
8
|
+
)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('handles non-root basePath', () => {
|
|
12
|
+
expect(
|
|
13
|
+
computeEmbedNavigationSrc('/storyboard/cq-org-enablement', '#x', '/storyboard/'),
|
|
14
|
+
).toBe('/cq-org-enablement#x')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('strips /branch--xxx/ deploy prefix', () => {
|
|
18
|
+
expect(
|
|
19
|
+
computeEmbedNavigationSrc('/branch--my-feature/cq-org-enablement', '#x', '/'),
|
|
20
|
+
).toBe('/cq-org-enablement#x')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns "/" when pathname equals basePath', () => {
|
|
24
|
+
expect(computeEmbedNavigationSrc('/storyboard', '', '/storyboard/')).toBe('/')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('handles missing hash', () => {
|
|
28
|
+
expect(computeEmbedNavigationSrc('/MyProto', '', '/')).toBe('/MyProto')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('dev mode (prototypes.html isolation entry)', () => {
|
|
33
|
+
it('uses hash content as src when pathname is /prototypes.html', () => {
|
|
34
|
+
expect(
|
|
35
|
+
computeEmbedNavigationSrc('/prototypes.html', '#/cq-org-enablement', '/'),
|
|
36
|
+
).toBe('/cq-org-enablement')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('preserves query string inside hash', () => {
|
|
40
|
+
expect(
|
|
41
|
+
computeEmbedNavigationSrc(
|
|
42
|
+
'/prototypes.html',
|
|
43
|
+
'#/cq-org-enablement?flow=cq-org-enablement/default&cqConfirmOpen=true',
|
|
44
|
+
'/',
|
|
45
|
+
),
|
|
46
|
+
).toBe('/cq-org-enablement?flow=cq-org-enablement/default&cqConfirmOpen=true')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('handles non-root basePath', () => {
|
|
50
|
+
expect(
|
|
51
|
+
computeEmbedNavigationSrc('/storyboard/prototypes.html', '#/MyProto', '/storyboard/'),
|
|
52
|
+
).toBe('/MyProto')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('handles branch deploy basePath', () => {
|
|
56
|
+
expect(
|
|
57
|
+
computeEmbedNavigationSrc(
|
|
58
|
+
'/branch--feature-x/prototypes.html',
|
|
59
|
+
'#/MyProto',
|
|
60
|
+
'/branch--feature-x/',
|
|
61
|
+
),
|
|
62
|
+
).toBe('/MyProto')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('prefixes inner hash with "/" if missing', () => {
|
|
66
|
+
expect(
|
|
67
|
+
computeEmbedNavigationSrc('/prototypes.html', '#MyProto', '/'),
|
|
68
|
+
).toBe('/MyProto')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns "/" for empty hash', () => {
|
|
72
|
+
expect(computeEmbedNavigationSrc('/prototypes.html', '', '/')).toBe('/')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('returns "/" for hash that is just "#"', () => {
|
|
76
|
+
expect(computeEmbedNavigationSrc('/prototypes.html', '#', '/')).toBe('/')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('cmd+D regression — must never produce /prototypes.html#... as src', () => {
|
|
81
|
+
// The original bug: dev embed broadcast pathname+hash, so the parent
|
|
82
|
+
// canvas persisted `/prototypes.html#/route` as the widget src. The
|
|
83
|
+
// URL builder then rebuilt the iframe URL as
|
|
84
|
+
// `/prototypes.html?proto=prototypes.html#/prototypes.html#/route`,
|
|
85
|
+
// which loaded a blank page. Duplicating that widget (cmd+D) carried
|
|
86
|
+
// the broken src to every clone.
|
|
87
|
+
it('never returns a string starting with /prototypes.html#', () => {
|
|
88
|
+
const cases = [
|
|
89
|
+
['/prototypes.html', '#/foo'],
|
|
90
|
+
['/prototypes.html', '#/foo?bar=1'],
|
|
91
|
+
['/storyboard/prototypes.html', '#/bar'],
|
|
92
|
+
['/branch--x/prototypes.html', '#/baz'],
|
|
93
|
+
]
|
|
94
|
+
for (const [pathname, hash] of cases) {
|
|
95
|
+
const basePath = pathname.replace(/\/prototypes\.html$/, '/') || '/'
|
|
96
|
+
const result = computeEmbedNavigationSrc(pathname, hash, basePath)
|
|
97
|
+
expect(result.startsWith('/prototypes.html')).toBe(false)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
})
|
|
@@ -27,6 +27,21 @@ function formatName(name) {
|
|
|
27
27
|
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Self-heal canvases that persisted a dev-isolation src like
|
|
31
|
+
// `/prototypes.html#/MyProto?...` (caused by an older broadcastNavigation
|
|
32
|
+
// that reported pathname+hash, treating the loader path as the route).
|
|
33
|
+
// Without this normalization, the URL builder treats "prototypes.html" as
|
|
34
|
+
// the prototype name and produces a blank iframe — and cmd+D duplicates
|
|
35
|
+
// inherit the broken src. Pure, deterministic, safe to call on every render.
|
|
36
|
+
function normalizeLegacyEmbedSrc(src) {
|
|
37
|
+
if (typeof src !== 'string' || !src) return src
|
|
38
|
+
const m = src.match(/^\/?(?:[^#]*\/)?prototypes\.html#(.*)$/)
|
|
39
|
+
if (!m) return src
|
|
40
|
+
const inner = m[1] || ''
|
|
41
|
+
if (!inner) return '/'
|
|
42
|
+
return inner.startsWith('/') ? inner : `/${inner}`
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
function resolveCanvasThemeFromStorage() {
|
|
31
46
|
if (typeof localStorage === 'undefined') return 'light'
|
|
32
47
|
let sync = { prototype: true, toolbar: false, codeBoxes: true, canvas: false }
|
|
@@ -46,7 +61,7 @@ function resolveCanvasThemeFromStorage() {
|
|
|
46
61
|
const HEADER_HEIGHT = 37
|
|
47
62
|
|
|
48
63
|
export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdate, resizable }, ref) {
|
|
49
|
-
const src = readProp(props, 'src', prototypeEmbedSchema)
|
|
64
|
+
const src = normalizeLegacyEmbedSrc(readProp(props, 'src', prototypeEmbedSchema))
|
|
50
65
|
const width = readProp(props, 'width', prototypeEmbedSchema) || 800
|
|
51
66
|
const height = readProp(props, 'height', prototypeEmbedSchema) || 600
|
|
52
67
|
const zoom = readProp(props, 'zoom', prototypeEmbedSchema) || 100
|
|
@@ -309,7 +324,10 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
309
324
|
function handleMessage(e) {
|
|
310
325
|
if (e.source !== iframeRef.current?.contentWindow) return
|
|
311
326
|
if (e.data?.type !== 'storyboard:embed:navigate') return
|
|
312
|
-
|
|
327
|
+
// Defensive: older consumer embeds reported `/prototypes.html#/route`
|
|
328
|
+
// here (loader path + hash). Normalize to the inner route so we never
|
|
329
|
+
// persist the broken form.
|
|
330
|
+
const newSrc = normalizeLegacyEmbedSrc(e.data.src)
|
|
313
331
|
if (newSrc && newSrc !== src) {
|
|
314
332
|
const originalSrc = readProp(props, 'originalSrc', prototypeEmbedSchema)
|
|
315
333
|
onUpdate?.({ src: newSrc, originalSrc: originalSrc || src })
|