@dfosco/storyboard 0.6.9 → 0.6.11
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
|
@@ -54,22 +54,24 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
54
54
|
|
|
55
55
|
const basePath = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
|
|
56
56
|
const baseSegment = basePath.replace(/^\//, '')
|
|
57
|
-
//
|
|
58
|
-
// -
|
|
59
|
-
// broken prototype's transform/HMR errors
|
|
60
|
-
//
|
|
61
|
-
// prototypes.html
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// External http(s) URLs are left alone.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
// Two URLs are derived from `src`:
|
|
58
|
+
// - rawSrc — the iframe URL. In DEV this routes through the isolated
|
|
59
|
+
// prototypes.html entry so a broken prototype's transform/HMR errors
|
|
60
|
+
// stay inside the iframe (see .agents/plans/vite-isolation.md):
|
|
61
|
+
// /MyProto/SignupForm becomes prototypes.html?proto=MyProto#/MyProto/SignupForm.
|
|
62
|
+
// In PROD it loads the prototype path directly through the canvas SPA
|
|
63
|
+
// (prototypes.html is a build-time isolation artifact that must not
|
|
64
|
+
// leak into deployed URLs).
|
|
65
|
+
// - externalSrc — the URL used by "Open in new tab". Always direct
|
|
66
|
+
// (`${basePath}/<protoPath>`), never prototypes.html, even in dev —
|
|
67
|
+
// opening prototypes.html#/... in a fresh tab is a leaky surprise
|
|
68
|
+
// for users navigating from the canvas.
|
|
69
|
+
// External http(s) URLs are left alone in both cases. basePath already
|
|
70
|
+
// carries the /branch--xxx/ prefix on branch deploys, so both work for
|
|
71
|
+
// main and branch deploys alike.
|
|
72
|
+
const { rawSrc, externalSrc } = useMemo(() => {
|
|
73
|
+
if (!src) return { rawSrc: '', externalSrc: '' }
|
|
74
|
+
if (/^https?:\/\//.test(src)) return { rawSrc: src, externalSrc: src }
|
|
73
75
|
const cleaned = src.replace(/^\/branch--[^/]+/, '')
|
|
74
76
|
let normalized
|
|
75
77
|
if (baseSegment && cleaned.startsWith(basePath)) normalized = cleaned
|
|
@@ -77,7 +79,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
77
79
|
else normalized = `${basePath}${cleaned}`
|
|
78
80
|
// Strip basePath so we can split path/query/hash cleanly and
|
|
79
81
|
// re-anchor for whichever mode we're in. Any pre-existing #hash on
|
|
80
|
-
// the original src is preserved
|
|
82
|
+
// the original src is preserved.
|
|
81
83
|
const withoutBase = baseSegment && normalized.startsWith(basePath)
|
|
82
84
|
? normalized.slice(basePath.length) || '/'
|
|
83
85
|
: normalized
|
|
@@ -86,18 +88,20 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
86
88
|
const pathAndQuery = hashIdx >= 0 ? withoutBase.slice(0, hashIdx) : withoutBase
|
|
87
89
|
const routePath = pathAndQuery.startsWith('/') ? pathAndQuery : `/${pathAndQuery}`
|
|
88
90
|
const suffix = innerHash ? `#${innerHash}` : ''
|
|
91
|
+
// Direct path through the canvas SPA — used in prod for the iframe and
|
|
92
|
+
// always for "Open in new tab".
|
|
93
|
+
const directUrl = `${basePath}${routePath}${suffix}`
|
|
89
94
|
if (import.meta.env.PROD) {
|
|
90
|
-
|
|
91
|
-
// production URLs.
|
|
92
|
-
return `${basePath}${routePath}${suffix}`
|
|
95
|
+
return { rawSrc: directUrl, externalSrc: directUrl }
|
|
93
96
|
}
|
|
94
|
-
// Dev: prototypes.html with ?proto= narrowing. The consumer's
|
|
97
|
+
// Dev iframe: prototypes.html with ?proto= narrowing. The consumer's
|
|
95
98
|
// prototypes-entry.jsx reads ?proto= and calls getRoutesForProto();
|
|
96
99
|
// older scaffolds harmlessly ignore the param and load the full tree.
|
|
97
100
|
const pathOnly = pathAndQuery.split('?')[0]
|
|
98
101
|
const protoName = pathOnly.split('/').filter(Boolean)[0] || ''
|
|
99
102
|
const queryStr = protoName ? `?proto=${encodeURIComponent(protoName)}` : ''
|
|
100
|
-
|
|
103
|
+
const iframeUrl = `${basePath}/prototypes.html${queryStr}#${routePath}${suffix}`
|
|
104
|
+
return { rawSrc: iframeUrl, externalSrc: directUrl }
|
|
101
105
|
}, [src, basePath, baseSegment])
|
|
102
106
|
|
|
103
107
|
const scale = zoom / 100
|
|
@@ -332,7 +336,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
332
336
|
} else if (actionId === 'split-screen') {
|
|
333
337
|
setExpandMode('split')
|
|
334
338
|
} else if (actionId === 'open-external') {
|
|
335
|
-
if (
|
|
339
|
+
if (externalSrc) window.open(externalSrc, '_blank', 'noopener')
|
|
336
340
|
} else if (actionId === 'refresh-frame') {
|
|
337
341
|
const iframe = iframeRef.current
|
|
338
342
|
if (iframe) {
|
|
@@ -347,7 +351,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
347
351
|
onUpdate?.({ zoom: Math.max(25, zoom - step) })
|
|
348
352
|
}
|
|
349
353
|
},
|
|
350
|
-
}), [
|
|
354
|
+
}), [externalSrc, zoom, onUpdate])
|
|
351
355
|
|
|
352
356
|
function handlePickRoute(route) {
|
|
353
357
|
onUpdate?.({ src: route })
|
|
@@ -132,6 +132,14 @@ function matchStoryRoute(pathname) {
|
|
|
132
132
|
* Strip the app's sub-path prefix (e.g. /storyboard) from the pathname.
|
|
133
133
|
* React Router's basename strips the branch prefix but not the app name prefix
|
|
134
134
|
* when the app runs under a nested base path.
|
|
135
|
+
*
|
|
136
|
+
* Two prefixing scenarios are handled:
|
|
137
|
+
* 1. Branch deploys — BASE_URL is `/branch--xxx/{devDomain}/`, so we strip
|
|
138
|
+
* whatever app sub-path remains after the branch prefix.
|
|
139
|
+
* 2. Local dev via Caddy proxy — BASE_URL is `/` (Caddy strips `/{devDomain}/`
|
|
140
|
+
* before forwarding to the dev server), but window.location.pathname is
|
|
141
|
+
* still `/{devDomain}/...`. React Router's basename `/` doesn't strip it,
|
|
142
|
+
* so we rely on the injected `__SB_DEV_DOMAIN__` global instead.
|
|
135
143
|
*/
|
|
136
144
|
function stripBasePath(pathname) {
|
|
137
145
|
let p = pathname.replace(/\/+$/, '') || '/'
|
|
@@ -146,6 +154,18 @@ function stripBasePath(pathname) {
|
|
|
146
154
|
p = p.slice(subPath.length) || '/'
|
|
147
155
|
}
|
|
148
156
|
}
|
|
157
|
+
// Local dev via Caddy proxy: window.location.pathname includes the
|
|
158
|
+
// /{devDomain}/ prefix that Caddy strips before reaching the dev server.
|
|
159
|
+
// BASE_URL is `/` in this scenario, so the block above is a no-op.
|
|
160
|
+
if (typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ && window.__SB_DEV_DOMAIN__) {
|
|
161
|
+
const devDomain = String(window.__SB_DEV_DOMAIN__).replace(/^\/+|\/+$/g, '')
|
|
162
|
+
if (devDomain) {
|
|
163
|
+
const devPrefix = '/' + devDomain
|
|
164
|
+
if (p === devPrefix || p.startsWith(devPrefix + '/')) {
|
|
165
|
+
p = p.slice(devPrefix.length) || '/'
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
149
169
|
return p
|
|
150
170
|
}
|
|
151
171
|
|
|
@@ -293,4 +293,51 @@ describe('StoryboardProvider', () => {
|
|
|
293
293
|
expect(screen.getByText('Canvas not found')).toBeInTheDocument()
|
|
294
294
|
expect(screen.getByRole('link', { name: /go to index page/i })).toHaveAttribute('href', '/')
|
|
295
295
|
})
|
|
296
|
+
|
|
297
|
+
it('strips dev-domain prefix in local dev (Caddy proxy) so canvas routes resolve', () => {
|
|
298
|
+
// Local dev via Caddy: window.location.pathname includes the /{devDomain}/
|
|
299
|
+
// prefix even though BASE_URL is `/` (the proxy strips it before forwarding
|
|
300
|
+
// to the dev server). Without stripping, isCanvasPath returns false and the
|
|
301
|
+
// 404 never renders — the canvas page just stays blank.
|
|
302
|
+
const origLocalDev = window.__SB_LOCAL_DEV__
|
|
303
|
+
const origDomain = window.__SB_DEV_DOMAIN__
|
|
304
|
+
window.__SB_LOCAL_DEV__ = true
|
|
305
|
+
window.__SB_DEV_DOMAIN__ = 'storyboard'
|
|
306
|
+
try {
|
|
307
|
+
mockUseLocation.mockReturnValue({ pathname: '/storyboard/canvas/unknown-board', search: '', hash: '' })
|
|
308
|
+
|
|
309
|
+
render(
|
|
310
|
+
<StoryboardProvider>
|
|
311
|
+
<ContextReader />
|
|
312
|
+
</StoryboardProvider>,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
expect(screen.getByText('Canvas not found')).toBeInTheDocument()
|
|
316
|
+
} finally {
|
|
317
|
+
window.__SB_LOCAL_DEV__ = origLocalDev
|
|
318
|
+
window.__SB_DEV_DOMAIN__ = origDomain
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('does not strip dev-domain prefix when __SB_LOCAL_DEV__ is not set', () => {
|
|
323
|
+
// Same URL shape, but production-like: prefix should be left alone so it
|
|
324
|
+
// doesn't accidentally match canvas routes for unrelated path segments.
|
|
325
|
+
const origDomain = window.__SB_DEV_DOMAIN__
|
|
326
|
+
window.__SB_DEV_DOMAIN__ = 'storyboard'
|
|
327
|
+
try {
|
|
328
|
+
mockUseLocation.mockReturnValue({ pathname: '/storyboard/canvas/unknown-board', search: '', hash: '' })
|
|
329
|
+
|
|
330
|
+
render(
|
|
331
|
+
<StoryboardProvider>
|
|
332
|
+
<ContextReader />
|
|
333
|
+
</StoryboardProvider>,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
// isCanvasPath sees `/storyboard/canvas/...` → does NOT start with `/canvas/`
|
|
337
|
+
// → no 404, just renders flow context
|
|
338
|
+
expect(screen.queryByText('Canvas not found')).not.toBeInTheDocument()
|
|
339
|
+
} finally {
|
|
340
|
+
window.__SB_DEV_DOMAIN__ = origDomain
|
|
341
|
+
}
|
|
342
|
+
})
|
|
296
343
|
})
|