@dfosco/storyboard 0.6.14 → 0.6.15
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
|
@@ -461,12 +461,17 @@ export async function mountStoryboardCore(config = {}, options = {}) {
|
|
|
461
461
|
* Compute the route src to broadcast to the parent canvas from a prototype embed iframe.
|
|
462
462
|
*
|
|
463
463
|
* In **dev mode**, the embed iframe is loaded via the isolation entry
|
|
464
|
-
* (`/prototypes.html
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
464
|
+
* (`/prototypes.html/<route>`), so `pathname` includes the `/prototypes.html`
|
|
465
|
+
* loader prefix. We strip that prefix and report the remaining route + hash
|
|
466
|
+
* as the src. The hash is preserved verbatim because storyboard URL state
|
|
467
|
+
* (`#key=value` overrides) writes to the hash and the canvas needs the full
|
|
468
|
+
* navigated URL state to round-trip cleanly.
|
|
469
|
+
*
|
|
470
|
+
* Legacy dev shape (0.6.13 and older) put the route in the hash
|
|
471
|
+
* (`/prototypes.html#/<route>`). We still recognize that shape and unwrap
|
|
472
|
+
* the inner hash so canvases never persist `/prototypes.html#/...` as a src
|
|
473
|
+
* (which the canvas-side URL builder would mis-interpret, producing a blank
|
|
474
|
+
* iframe — and cmd+D would duplicate the broken src to every clone).
|
|
470
475
|
*
|
|
471
476
|
* In **prod mode**, the iframe loads the prototype route directly through
|
|
472
477
|
* the canvas SPA, so `pathname` already contains the route — we just strip
|
|
@@ -479,6 +484,13 @@ export function computeEmbedNavigationSrc(pathname, hash, basePath = '') {
|
|
|
479
484
|
const stripped = cleanBase && pathname.startsWith(cleanBase)
|
|
480
485
|
? pathname.slice(cleanBase.length) || '/'
|
|
481
486
|
: pathname.replace(/^\/branch--[^/]+/, '') || '/'
|
|
487
|
+
// New dev shape: `/prototypes.html/<route>` — strip loader prefix and
|
|
488
|
+
// append hash (storyboard URL state).
|
|
489
|
+
if (stripped.startsWith('/prototypes.html/')) {
|
|
490
|
+
const route = stripped.slice('/prototypes.html'.length) || '/'
|
|
491
|
+
return route + (hash || '')
|
|
492
|
+
}
|
|
493
|
+
// Legacy dev shape (0.6.13 and older): `/prototypes.html` + `#/<route>`.
|
|
482
494
|
if (stripped === '/prototypes.html' || stripped.endsWith('/prototypes.html')) {
|
|
483
495
|
const inner = (hash || '').startsWith('#') ? hash.slice(1) : (hash || '')
|
|
484
496
|
if (!inner) return '/'
|
|
@@ -29,7 +29,61 @@ describe('computeEmbedNavigationSrc', () => {
|
|
|
29
29
|
})
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
describe('dev mode (prototypes.html
|
|
32
|
+
describe('dev mode (new shape: /prototypes.html/<route>)', () => {
|
|
33
|
+
it('strips /prototypes.html prefix from pathname and keeps hash', () => {
|
|
34
|
+
expect(
|
|
35
|
+
computeEmbedNavigationSrc('/prototypes.html/cq-org-enablement', '#cqConfirmOpen=true', '/'),
|
|
36
|
+
).toBe('/cq-org-enablement#cqConfirmOpen=true')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('preserves multi-key storyboard hash state', () => {
|
|
40
|
+
expect(
|
|
41
|
+
computeEmbedNavigationSrc(
|
|
42
|
+
'/prototypes.html/cq-org-enablement',
|
|
43
|
+
'#cqEnabled=null&cqFlashDismissed=null&cqConfirmOpen=true&cqSettingUp=null',
|
|
44
|
+
'/',
|
|
45
|
+
),
|
|
46
|
+
).toBe('/cq-org-enablement#cqEnabled=null&cqFlashDismissed=null&cqConfirmOpen=true&cqSettingUp=null')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('preserves search params (e.g. ?flow=name)', () => {
|
|
50
|
+
expect(
|
|
51
|
+
computeEmbedNavigationSrc(
|
|
52
|
+
'/prototypes.html/cq-org-enablement',
|
|
53
|
+
'',
|
|
54
|
+
'/',
|
|
55
|
+
),
|
|
56
|
+
).toBe('/cq-org-enablement')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('handles nested route segments', () => {
|
|
60
|
+
expect(
|
|
61
|
+
computeEmbedNavigationSrc('/prototypes.html/MyProto/SignupForm', '', '/'),
|
|
62
|
+
).toBe('/MyProto/SignupForm')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('handles non-root basePath', () => {
|
|
66
|
+
expect(
|
|
67
|
+
computeEmbedNavigationSrc(
|
|
68
|
+
'/storyboard/prototypes.html/MyProto',
|
|
69
|
+
'#x=1',
|
|
70
|
+
'/storyboard/',
|
|
71
|
+
),
|
|
72
|
+
).toBe('/MyProto#x=1')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('handles branch deploy basePath', () => {
|
|
76
|
+
expect(
|
|
77
|
+
computeEmbedNavigationSrc(
|
|
78
|
+
'/branch--feature-x/prototypes.html/MyProto',
|
|
79
|
+
'#x=1',
|
|
80
|
+
'/branch--feature-x/',
|
|
81
|
+
),
|
|
82
|
+
).toBe('/MyProto#x=1')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('dev mode legacy shape (0.6.13 and older: /prototypes.html + #/<route>)', () => {
|
|
33
87
|
it('uses hash content as src when pathname is /prototypes.html', () => {
|
|
34
88
|
expect(
|
|
35
89
|
computeEmbedNavigationSrc('/prototypes.html', '#/cq-org-enablement', '/'),
|
|
@@ -77,22 +131,41 @@ describe('computeEmbedNavigationSrc', () => {
|
|
|
77
131
|
})
|
|
78
132
|
})
|
|
79
133
|
|
|
80
|
-
describe('
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
134
|
+
describe('regression — must never produce /prototypes.html prefix as src', () => {
|
|
135
|
+
// 0.6.13 bug: dev embed broadcast pathname+hash, so the parent canvas
|
|
136
|
+
// persisted `/prototypes.html#/route` as the widget src. The URL builder
|
|
137
|
+
// then rebuilt the iframe URL as
|
|
84
138
|
// `/prototypes.html?proto=prototypes.html#/prototypes.html#/route`,
|
|
85
139
|
// which loaded a blank page. Duplicating that widget (cmd+D) carried
|
|
86
140
|
// the broken src to every clone.
|
|
87
|
-
|
|
141
|
+
//
|
|
142
|
+
// 0.6.14 bug: hash-based routing in createHashRouter collided with
|
|
143
|
+
// storyboard URL state (which also writes to the hash). Navigating
|
|
144
|
+
// inside the iframe blanked the page because the route part of the
|
|
145
|
+
// hash got URL-encoded into a single nonsense fragment.
|
|
146
|
+
//
|
|
147
|
+
// 0.6.15 (this fix): pathname-based routing — both shapes must
|
|
148
|
+
// round-trip to a clean canvas-app route.
|
|
149
|
+
it('never returns a string starting with /prototypes.html', () => {
|
|
88
150
|
const cases = [
|
|
151
|
+
// legacy hash-form (0.6.13 bug)
|
|
89
152
|
['/prototypes.html', '#/foo'],
|
|
90
153
|
['/prototypes.html', '#/foo?bar=1'],
|
|
91
154
|
['/storyboard/prototypes.html', '#/bar'],
|
|
92
155
|
['/branch--x/prototypes.html', '#/baz'],
|
|
156
|
+
// new path-form (0.6.15)
|
|
157
|
+
['/prototypes.html/foo', ''],
|
|
158
|
+
['/prototypes.html/foo', '#key=value'],
|
|
159
|
+
['/prototypes.html/foo/bar', '#a=1&b=2'],
|
|
160
|
+
['/storyboard/prototypes.html/foo', '#x=y'],
|
|
161
|
+
['/branch--x/prototypes.html/foo', '#x=y'],
|
|
93
162
|
]
|
|
94
163
|
for (const [pathname, hash] of cases) {
|
|
95
|
-
const basePath = pathname.
|
|
164
|
+
const basePath = pathname.startsWith('/branch--x')
|
|
165
|
+
? '/branch--x/'
|
|
166
|
+
: pathname.startsWith('/storyboard')
|
|
167
|
+
? '/storyboard/'
|
|
168
|
+
: '/'
|
|
96
169
|
const result = computeEmbedNavigationSrc(pathname, hash, basePath)
|
|
97
170
|
expect(result.startsWith('/prototypes.html')).toBe(false)
|
|
98
171
|
}
|
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
* the consumer-side `src/prototypes-entry.jsx` so consumers don't need to
|
|
6
6
|
* maintain the iframe entry themselves.
|
|
7
7
|
*
|
|
8
|
-
* Uses `
|
|
9
|
-
* (`prototypes.html
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Uses `createBrowserRouter` with `/prototypes.html` as the basename so
|
|
9
|
+
* prototype routes live in the URL path (`prototypes.html/MyProto/SignupForm`),
|
|
10
|
+
* leaving the hash and search free for storyboard URL state (which writes
|
|
11
|
+
* `#key=value` for overrides and `?flow=name` for the active flow).
|
|
12
|
+
*
|
|
13
|
+
* The first path segment after `prototypes.html` is the prototype name —
|
|
14
|
+
* used by `getRoutesForProto()` to filter routes so a broken sibling
|
|
15
|
+
* prototype never appears in the matched route's lazy() chain
|
|
16
|
+
* (module-graph isolation — see .agents/plans/vite-isolation.md).
|
|
12
17
|
*
|
|
13
18
|
* CSS chain: relies on `mountStoryboardCore` to inject the compiled
|
|
14
19
|
* ui-runtime stylesheet (matches index.jsx's mount) — this restores stock
|
|
@@ -19,7 +24,7 @@
|
|
|
19
24
|
*/
|
|
20
25
|
import { StrictMode } from 'react'
|
|
21
26
|
import { createRoot } from 'react-dom/client'
|
|
22
|
-
import { RouterProvider,
|
|
27
|
+
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
|
23
28
|
import { ThemeProvider, BaseStyles } from '@primer/react'
|
|
24
29
|
import { routes, getRoutesForProto } from './prototypeRoutes.jsx'
|
|
25
30
|
import ThemeSync from '../../primer/ThemeSync.jsx'
|
|
@@ -46,10 +51,20 @@ const storyboardConfig = Object.values(configModules)[0]?.default || {}
|
|
|
46
51
|
// - sets up the iframe→parent postMessage navigation/wheel bridge
|
|
47
52
|
mountStoryboardCore(storyboardConfig, { basePath: import.meta.env.BASE_URL })
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
// Derive the prototype name for narrowing. Prefer the first pathname segment
|
|
55
|
+
// after `prototypes.html` (the new browser-router layout); fall back to the
|
|
56
|
+
// legacy `?proto=` query param so old canvases or older builds keep working.
|
|
57
|
+
const basePathNoTrail = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
|
|
58
|
+
const prototypesBasename = `${basePathNoTrail}/prototypes.html`
|
|
59
|
+
const afterBasename = window.location.pathname.startsWith(prototypesBasename)
|
|
60
|
+
? window.location.pathname.slice(prototypesBasename.length)
|
|
61
|
+
: ''
|
|
62
|
+
const pathProto = (afterBasename.match(/^\/([^/?#]+)/) || [])[1] || ''
|
|
63
|
+
const queryProto = new URLSearchParams(window.location.search).get('proto') || ''
|
|
64
|
+
const protoParam = pathProto || queryProto
|
|
50
65
|
const activeRoutes = protoParam ? getRoutesForProto(protoParam) : routes
|
|
51
66
|
|
|
52
|
-
const router =
|
|
67
|
+
const router = createBrowserRouter(activeRoutes, { basename: prototypesBasename })
|
|
53
68
|
|
|
54
69
|
const rootElement = document.getElementById('root')
|
|
55
70
|
const root = createRoot(rootElement)
|
|
@@ -27,19 +27,31 @@ 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
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
30
|
+
// Self-heal canvases that persisted a dev-isolation loader URL as the widget src.
|
|
31
|
+
// Two legacy shapes exist:
|
|
32
|
+
// - `/prototypes.html#/MyProto?...` — 0.6.13 and older, when the iframe
|
|
33
|
+
// broadcast pathname+hash and routing lived in the hash
|
|
34
|
+
// - `/prototypes.html/MyProto?...` — could appear if a future broadcaster
|
|
35
|
+
// ever forwards the loader path verbatim with the new browser-router layout
|
|
36
|
+
// Without normalization the URL builder treats "prototypes.html" as the
|
|
37
|
+
// prototype name and produces a blank iframe — and cmd+D duplicates inherit
|
|
38
|
+
// the broken src. Pure, deterministic, safe to call on every render.
|
|
36
39
|
function normalizeLegacyEmbedSrc(src) {
|
|
37
40
|
if (typeof src !== 'string' || !src) return src
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Hash-form: `/prototypes.html#/inner` or `/storyboard/prototypes.html#/inner`
|
|
42
|
+
const hashMatch = src.match(/^\/?(?:[^#]*\/)?prototypes\.html#(.*)$/)
|
|
43
|
+
if (hashMatch) {
|
|
44
|
+
const inner = hashMatch[1] || ''
|
|
45
|
+
if (!inner) return '/'
|
|
46
|
+
return inner.startsWith('/') ? inner : `/${inner}`
|
|
47
|
+
}
|
|
48
|
+
// Path-form: `/prototypes.html/inner...` or `/storyboard/prototypes.html/inner...`
|
|
49
|
+
const pathMatch = src.match(/^\/?(?:[^#?]*\/)?prototypes\.html(\/[^]*)$/)
|
|
50
|
+
if (pathMatch) {
|
|
51
|
+
const inner = pathMatch[1] || ''
|
|
52
|
+
return inner || '/'
|
|
53
|
+
}
|
|
54
|
+
return src
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
function resolveCanvasThemeFromStorage() {
|
|
@@ -73,13 +85,15 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
73
85
|
// - rawSrc — the iframe URL. In DEV this routes through the isolated
|
|
74
86
|
// prototypes.html entry so a broken prototype's transform/HMR errors
|
|
75
87
|
// stay inside the iframe (see .agents/plans/vite-isolation.md):
|
|
76
|
-
// /MyProto/SignupForm becomes prototypes.html
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
88
|
+
// /MyProto/SignupForm becomes prototypes.html/MyProto/SignupForm.
|
|
89
|
+
// Routing happens in the *pathname* (createBrowserRouter), not in the
|
|
90
|
+
// hash, so the hash and search stay free for storyboard URL state
|
|
91
|
+
// (`?flow=...` and `#key=value` overrides). In PROD it loads the
|
|
92
|
+
// prototype path directly through the canvas SPA (prototypes.html is
|
|
93
|
+
// a build-time isolation artifact that must not leak into deployed URLs).
|
|
80
94
|
// - externalSrc — the URL used by "Open in new tab". Always direct
|
|
81
95
|
// (`${basePath}/<protoPath>`), never prototypes.html, even in dev —
|
|
82
|
-
// opening prototypes.html
|
|
96
|
+
// opening prototypes.html/... in a fresh tab is a leaky surprise
|
|
83
97
|
// for users navigating from the canvas.
|
|
84
98
|
// External http(s) URLs are left alone in both cases. basePath already
|
|
85
99
|
// carries the /branch--xxx/ prefix on branch deploys, so both work for
|
|
@@ -109,13 +123,14 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
109
123
|
if (import.meta.env.PROD) {
|
|
110
124
|
return { rawSrc: directUrl, externalSrc: directUrl }
|
|
111
125
|
}
|
|
112
|
-
// Dev iframe: prototypes.html with
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
// Dev iframe: prototypes.html with the route as a sub-path. The library
|
|
127
|
+
// middleware (`data-plugin.js`) serves the same HTML shell for
|
|
128
|
+
// `/prototypes.html/*` so a hard reload / open-in-new-tab works. The
|
|
129
|
+
// prototype name (first path segment) narrows getRoutesForProto so a
|
|
130
|
+
// broken sibling prototype can't poison the lazy() chain. Hash and
|
|
131
|
+
// search stay attached so storyboard URL state (`#key=value` overrides,
|
|
132
|
+
// `?flow=name`) survives the redirect through the isolation entry.
|
|
133
|
+
const iframeUrl = `${basePath}/prototypes.html${routePath}${suffix}`
|
|
119
134
|
return { rawSrc: iframeUrl, externalSrc: directUrl }
|
|
120
135
|
}, [src, basePath, baseSegment])
|
|
121
136
|
|
|
@@ -1248,7 +1248,7 @@ export default function storyboardDataPlugin() {
|
|
|
1248
1248
|
url = url.slice(baseNoTrail.length) || '/'
|
|
1249
1249
|
}
|
|
1250
1250
|
const cleanUrl = url.split('?')[0].split('#')[0]
|
|
1251
|
-
if (cleanUrl !== '/prototypes.html') return next()
|
|
1251
|
+
if (cleanUrl !== '/prototypes.html' && !cleanUrl.startsWith('/prototypes.html/')) return next()
|
|
1252
1252
|
|
|
1253
1253
|
// Theme bootstrap mirrors index.html so the iframe paints with the
|
|
1254
1254
|
// correct color mode on first frame (before mountStoryboardCore
|