@dfosco/storyboard 0.12.0 → 0.12.2
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/dist/storyboard-ui.js.map +1 -1
- package/package.json +1 -1
- package/scaffold/eslint.config.js +45 -0
- package/scaffold/scaffold.config.json +25 -0
- package/scaffold/scripts/aggregate-artifacts.mjs +23 -1
- package/scaffold/src/library/README.md +48 -3
- package/scaffold/src/library/home.jsx +24 -0
- package/scaffold/src/library/index.jsx +37 -10
- package/scaffold/src/library/routes.jsx +22 -7
- package/scaffold/src/library/workspace.jsx +12 -2
- package/scaffold/tsconfig.json +25 -0
- package/scaffold/vite.config.js +153 -0
- package/scaffold/vitest.config.js +17 -0
- package/scripts/aggregate-artifacts.mjs +23 -1
- package/src/core/cli/updateVersion.js +18 -1
- package/src/core/data/artifactIndex.js +54 -8
- package/src/core/data/artifactIndex.test.js +52 -0
- package/src/core/data/viewfinder.js +3 -2
- package/src/core/data/viewfinder.test.js +39 -0
- package/src/core/scaffold/scaffold-integration.test.js +24 -0
- package/src/core/stores/configSchema.js +98 -0
- package/src/core/stores/configSchema.test.js +75 -1
- package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +116 -20
- package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +54 -0
- package/src/internals/Viewfinder.jsx +92 -22
- package/src/internals/Viewfinder.module.css +14 -1
- package/src/internals/canvas/isolation/prototypeRoutes.filter.test.js +47 -0
- package/src/internals/canvas/isolation/prototypeRoutes.jsx +16 -18
- package/src/internals/canvas/isolation/routeFilter.js +31 -0
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +1 -1
- package/src/internals/vite/data-plugin.js +4 -0
- package/src/internals/vite/data-plugin.test.js +31 -1
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import react from 'eslint-plugin-react'
|
|
4
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
5
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
6
|
+
|
|
7
|
+
export default [
|
|
8
|
+
{ ignores: ['dist', '.worktrees'] },
|
|
9
|
+
{
|
|
10
|
+
files: ['vite.config.js', 'eslint.config.js'],
|
|
11
|
+
languageOptions: {
|
|
12
|
+
globals: globals.node,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
files: ['**/*.{js,jsx}'],
|
|
17
|
+
languageOptions: {
|
|
18
|
+
ecmaVersion: 2020,
|
|
19
|
+
globals: globals.browser,
|
|
20
|
+
parserOptions: {
|
|
21
|
+
ecmaVersion: 'latest',
|
|
22
|
+
ecmaFeatures: { jsx: true },
|
|
23
|
+
sourceType: 'module',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
settings: { react: { version: 'detect' } },
|
|
27
|
+
plugins: {
|
|
28
|
+
react,
|
|
29
|
+
'react-hooks': reactHooks,
|
|
30
|
+
'react-refresh': reactRefresh,
|
|
31
|
+
},
|
|
32
|
+
rules: {
|
|
33
|
+
...js.configs.recommended.rules,
|
|
34
|
+
...react.configs.recommended.rules,
|
|
35
|
+
...react.configs['jsx-runtime'].rules,
|
|
36
|
+
...reactHooks.configs.recommended.rules,
|
|
37
|
+
'react/prop-types': 'off',
|
|
38
|
+
'react/jsx-no-target-blank': 'off',
|
|
39
|
+
'react-refresh/only-export-components': [
|
|
40
|
+
'warn',
|
|
41
|
+
{ allowConstantExport: true },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
]
|
|
@@ -15,6 +15,26 @@
|
|
|
15
15
|
"target": "storyboard.config.json",
|
|
16
16
|
"mode": "scaffold"
|
|
17
17
|
},
|
|
18
|
+
{
|
|
19
|
+
"source": "scaffold/vite.config.js",
|
|
20
|
+
"target": "vite.config.js",
|
|
21
|
+
"mode": "updateable"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"source": "scaffold/vitest.config.js",
|
|
25
|
+
"target": "vitest.config.js",
|
|
26
|
+
"mode": "updateable"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"source": "scaffold/eslint.config.js",
|
|
30
|
+
"target": "eslint.config.js",
|
|
31
|
+
"mode": "updateable"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"source": "scaffold/tsconfig.json",
|
|
35
|
+
"target": "tsconfig.json",
|
|
36
|
+
"mode": "updateable"
|
|
37
|
+
},
|
|
18
38
|
{
|
|
19
39
|
"source": "scaffold/scripts/link.sh",
|
|
20
40
|
"target": "scripts/link.sh",
|
|
@@ -139,6 +159,11 @@
|
|
|
139
159
|
"target": "src/library/index.jsx",
|
|
140
160
|
"mode": "updateable"
|
|
141
161
|
},
|
|
162
|
+
{
|
|
163
|
+
"source": "scaffold/src/library/home.jsx",
|
|
164
|
+
"target": "src/library/home.jsx",
|
|
165
|
+
"mode": "updateable"
|
|
166
|
+
},
|
|
142
167
|
{
|
|
143
168
|
"source": "scaffold/src/library/README.md",
|
|
144
169
|
"target": "src/library/README.md",
|
|
@@ -19,6 +19,28 @@ function withTrailingSlash(folder) {
|
|
|
19
19
|
return folder.endsWith('/') ? folder : `${folder}/`
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Folder-independent identity for a canvas, mirroring prototype `dirName`
|
|
24
|
+
* matching. Keeps cross-branch dedup stable when a canvas moves between
|
|
25
|
+
* folders (or root ↔ folder). Proto-scoped canvases retain their prototype
|
|
26
|
+
* segment so same-named canvases in different prototypes never collide.
|
|
27
|
+
*/
|
|
28
|
+
function canvasMatchKey(id) {
|
|
29
|
+
let raw = String(id || '')
|
|
30
|
+
let isProto = false
|
|
31
|
+
if (raw.startsWith('proto:')) {
|
|
32
|
+
isProto = true
|
|
33
|
+
raw = raw.slice('proto:'.length)
|
|
34
|
+
}
|
|
35
|
+
const segments = raw.split('/').filter(Boolean)
|
|
36
|
+
const name = segments[segments.length - 1] || raw
|
|
37
|
+
if (isProto) {
|
|
38
|
+
const scope = segments.length >= 2 ? segments[segments.length - 2] : ''
|
|
39
|
+
return scope ? `proto:${scope}/${name}` : `proto:${name}`
|
|
40
|
+
}
|
|
41
|
+
return name
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
function generatedAtMs(manifest) {
|
|
23
45
|
const value = Date.parse(manifest?.generatedAt || '')
|
|
24
46
|
return Number.isNaN(value) ? 0 : value
|
|
@@ -157,7 +179,7 @@ function addCanvas(index, manifestInfo, item) {
|
|
|
157
179
|
|
|
158
180
|
const { manifest, folder } = manifestInfo
|
|
159
181
|
const branch = deriveBranch(manifest, folder)
|
|
160
|
-
const key =
|
|
182
|
+
const key = canvasMatchKey(id)
|
|
161
183
|
|
|
162
184
|
if (!index.canvases[key]) {
|
|
163
185
|
index.canvases[key] = { id: key, branches: [] }
|
|
@@ -13,10 +13,11 @@ The contents:
|
|
|
13
13
|
| `prototypes-entry.jsx` | Vite iframe entry — `prototypes.html` loads this |
|
|
14
14
|
| `routes.jsx` | Generouted glue: scans `src/library/*.jsx` (workspace pages) and `src/prototypes/**` (your prototypes) |
|
|
15
15
|
| `_app.jsx`, `_app.module.css` | Workspace SPA shell — `StoryboardProvider`, suspense, error boundary, feature-flag banner |
|
|
16
|
-
| `workspace.jsx` | `/workspace` route — calls `<Workspace
|
|
16
|
+
| `workspace.jsx` | `/workspace` route — calls `<Workspace>`; props come from `pages.workspace` in `storyboard.config.json` |
|
|
17
17
|
| `viewfinder.jsx` | `/viewfinder` redirect (kept for back-compat) |
|
|
18
18
|
| `create.jsx` | `/create` standalone artifact creator |
|
|
19
|
-
| `
|
|
19
|
+
| `home.jsx` | `/home` route — the landing surface (`<SimpleWorkspace>`); props come from `pages.home` in `storyboard.config.json` |
|
|
20
|
+
| `index.jsx` | `/` index — renders whichever surface `routes["/"]` selects (env-aware) |
|
|
20
21
|
|
|
21
22
|
## How to customize storyboard
|
|
22
23
|
|
|
@@ -25,9 +26,53 @@ Almost every customization knob lives elsewhere:
|
|
|
25
26
|
| What you want to change | Edit |
|
|
26
27
|
|---|---|
|
|
27
28
|
| App title, theming, feature flags, surfaces | `storyboard.config.json` |
|
|
29
|
+
| Which page renders at `/` (and per environment) | `storyboard.config.json` → `routes` |
|
|
30
|
+
| Landing / workspace titles, subtitles, logo, tabs | `storyboard.config.json` → `pages` |
|
|
28
31
|
| Provider chain for every **prototype** (design system, fonts, Tailwind) | `src/_prototype.jsx` (optional — create it if you want it) |
|
|
29
32
|
| Provider chain for every **story** | `src/_story.jsx` (optional) |
|
|
30
|
-
| Add new routes the user can navigate to inside the SPA | Add files under `src/prototypes
|
|
33
|
+
| Add new routes the user can navigate to inside the SPA | Add files under `src/prototypes/`, or a flat `src/<Name>.jsx` → `/<Name>` |
|
|
34
|
+
|
|
35
|
+
### `routes` — what renders at `/`
|
|
36
|
+
|
|
37
|
+
`routes` maps a path to a target page name. The index `/` defaults to `home`:
|
|
38
|
+
|
|
39
|
+
```jsonc
|
|
40
|
+
{
|
|
41
|
+
"routes": {
|
|
42
|
+
"/": "home" // same page in every env
|
|
43
|
+
// "/": { "dev": "workspace", "prod": "home" } // per-environment target
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A target is a page name with no leading slash — a library surface (`"home"`,
|
|
49
|
+
`"workspace"`) or a flat component (`"Dashboard"` for `src/Dashboard.jsx`). The
|
|
50
|
+
object form picks `dev` under `storyboard dev`, `prod` in production builds, and
|
|
51
|
+
`default` as a fallback. If `/` resolves to a flat component, the index
|
|
52
|
+
redirects there.
|
|
53
|
+
|
|
54
|
+
### `pages` — surface presentation props
|
|
55
|
+
|
|
56
|
+
`pages` holds the title/subtitle/logo/tab props the library pages used to
|
|
57
|
+
hardcode, so you configure surfaces without touching the JSX:
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
{
|
|
61
|
+
"pages": {
|
|
62
|
+
"home": { "title": "My App", "subtitle": "Welcome" },
|
|
63
|
+
"workspace": { "title": "My App", "showComponents": false }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Both are leaf-merged over behavior-preserving defaults — set only the keys you
|
|
69
|
+
want to change.
|
|
70
|
+
|
|
71
|
+
### Flat-file routes
|
|
72
|
+
|
|
73
|
+
Any top-level `src/<Name>.jsx` automatically becomes a `/<Name>` route, rendered
|
|
74
|
+
**bare** (only the `_app` SPA shell — no prototype wrapper). Files prefixed with
|
|
75
|
+
`_` (e.g. `src/_prototype.jsx`) are excluded.
|
|
31
76
|
|
|
32
77
|
If you have a real reason to edit a file in this directory, just edit it —
|
|
33
78
|
storyboard will warn (but not block) on the next sync. You opt out of sync
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DO NOT EDIT — storyboard-owned library file.
|
|
3
|
+
*
|
|
4
|
+
* Synced from @dfosco/storyboard. The `home` surface (landing workspace).
|
|
5
|
+
* All presentation props (title, subtitle) come from `pages.home` in
|
|
6
|
+
* storyboard.config.json — configure there, never edit this file.
|
|
7
|
+
* See src/library/README.md for the full customization surface.
|
|
8
|
+
*/
|
|
9
|
+
import { SimpleWorkspace } from '@dfosco/storyboard'
|
|
10
|
+
import { getConfig } from '@dfosco/storyboard/config'
|
|
11
|
+
import storyboardConfig from '../../storyboard.config.json'
|
|
12
|
+
|
|
13
|
+
const pageModules = import.meta.glob('/src/prototypes/*/*.jsx')
|
|
14
|
+
const { pages } = getConfig(storyboardConfig)
|
|
15
|
+
|
|
16
|
+
export default function HomePage() {
|
|
17
|
+
return (
|
|
18
|
+
<SimpleWorkspace
|
|
19
|
+
{...pages.home}
|
|
20
|
+
pageModules={pageModules}
|
|
21
|
+
basePath={import.meta.env.BASE_URL}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -1,14 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* DO NOT EDIT — storyboard-owned library file.
|
|
3
|
+
*
|
|
4
|
+
* Synced from @dfosco/storyboard. The site index (`/`).
|
|
5
|
+
*
|
|
6
|
+
* Which surface renders here is configured via `routes["/"]` in
|
|
7
|
+
* storyboard.config.json — a page name (e.g. "home", "workspace") or a
|
|
8
|
+
* per-environment object `{ dev, prod, default }`. Defaults to "home".
|
|
9
|
+
* Configure the route in storyboard.config.json, never edit this file.
|
|
10
|
+
* See src/library/README.md for the full customization surface.
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect } from 'react'
|
|
13
|
+
import { getConfig, resolveRouteTarget } from '@dfosco/storyboard/config'
|
|
14
|
+
import storyboardConfig from '../../storyboard.config.json'
|
|
15
|
+
import HomePage from './home'
|
|
16
|
+
import WorkspacePage from './workspace'
|
|
2
17
|
|
|
3
|
-
const
|
|
18
|
+
const { routes } = getConfig(storyboardConfig)
|
|
19
|
+
const target = resolveRouteTarget(routes, '/', { dev: import.meta.env.DEV }) || 'home'
|
|
20
|
+
|
|
21
|
+
// Built-in surfaces that render inline at the index without a redirect.
|
|
22
|
+
const SURFACES = {
|
|
23
|
+
home: HomePage,
|
|
24
|
+
workspace: WorkspacePage,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function RedirectTo({ to }) {
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const base = (import.meta.env.BASE_URL || '/').replace(/\/+$/, '')
|
|
30
|
+
window.location.replace(`${base}/${to}${window.location.hash}`)
|
|
31
|
+
}, [to])
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
4
34
|
|
|
5
35
|
export default function IndexPage() {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
basePath={import.meta.env.BASE_URL}
|
|
12
|
-
/>
|
|
13
|
-
)
|
|
36
|
+
const Surface = SURFACES[target]
|
|
37
|
+
if (Surface) return <Surface />
|
|
38
|
+
// Target is a flat component (src/<Name>.jsx) or another library page —
|
|
39
|
+
// redirect `/` to that route so any configured target works.
|
|
40
|
+
return <RedirectTo to={target} />
|
|
14
41
|
}
|
|
@@ -39,7 +39,12 @@ import {
|
|
|
39
39
|
// and `drafts/` segments. A `drafts/` dir is a gitignored scratch root —
|
|
40
40
|
// prototypes inside route at the same URL as a non-prefixed sibling (so
|
|
41
41
|
// `prototypes/drafts/Foo/` and `prototypes/Foo/` both resolve at `/Foo`).
|
|
42
|
-
|
|
42
|
+
//
|
|
43
|
+
// The second alternation strips a leading `/src/` for BARE top-level files
|
|
44
|
+
// (`/src/Dashboard.jsx` → `Dashboard`). The lookahead `(?=[^/]+\.(jsx|tsx|mdx)$)`
|
|
45
|
+
// ensures it only fires for files directly under /src (no further slash), so
|
|
46
|
+
// it never touches library/ or prototypes/ paths.
|
|
47
|
+
patterns.route = [/^.*\/src\/(library|prototypes)\/|^.*\/src\/(?=[^/]+\.(jsx|tsx|mdx)$)|^\/(library|prototypes)\/|[^/]*\.folder\/|(?<=^|\/)drafts\/|\.(jsx|tsx|mdx)$/g, '']
|
|
43
48
|
|
|
44
49
|
// `_app.jsx` lives in src/library/ — it's storyboard's SPA shell
|
|
45
50
|
// (StoryboardProvider + suspense + error boundary). Consumers can copy
|
|
@@ -63,6 +68,13 @@ const PROTOTYPE_ROUTES = import.meta.glob(
|
|
|
63
68
|
['/src/prototypes/**/[\\w[-]*.{jsx,tsx,mdx}', '!/src/prototypes/**/(_!(layout)*(/*)?|_app|404)*'],
|
|
64
69
|
)
|
|
65
70
|
|
|
71
|
+
// Flat-file routes — any top-level `src/<Name>.jsx` becomes a `/<Name>` route,
|
|
72
|
+
// rendered BARE (only the _app SPA shell, NOT wrapped in _prototype). Files
|
|
73
|
+
// prefixed with `_` (e.g. _prototype.jsx, _story.jsx) are excluded.
|
|
74
|
+
const ROOT_ROUTES = import.meta.glob(
|
|
75
|
+
['/src/*.{jsx,tsx}', '!/src/_*.{jsx,tsx}'],
|
|
76
|
+
)
|
|
77
|
+
|
|
66
78
|
const preservedRoutes = generatePreservedRoutes(PRESERVED)
|
|
67
79
|
const modalRoutes = generateModalRoutes(MODALS)
|
|
68
80
|
|
|
@@ -102,6 +114,7 @@ function makeLazyRoute(importFn, key) {
|
|
|
102
114
|
|
|
103
115
|
const libraryRoutes = generateRegularRoutes(LIBRARY_ROUTES, makeLazyRoute)
|
|
104
116
|
const prototypeRoutes = generateRegularRoutes(PROTOTYPE_ROUTES, makeLazyRoute)
|
|
117
|
+
const rootRoutes = generateRegularRoutes(ROOT_ROUTES, makeLazyRoute)
|
|
105
118
|
|
|
106
119
|
const _app = preservedRoutes?.['_app']
|
|
107
120
|
const _404 = preservedRoutes?.['404']
|
|
@@ -162,7 +175,7 @@ const prototypeLayoutRoute = {
|
|
|
162
175
|
|
|
163
176
|
export const routes = [{
|
|
164
177
|
...app,
|
|
165
|
-
children: [...libraryRoutes, prototypeLayoutRoute, fallback],
|
|
178
|
+
children: [...libraryRoutes, ...rootRoutes, prototypeLayoutRoute, fallback],
|
|
166
179
|
}]
|
|
167
180
|
|
|
168
181
|
// Return a routes tree filtered to a single prototype subtree. Used by the
|
|
@@ -185,13 +198,15 @@ export function getRoutesForProto(proto) {
|
|
|
185
198
|
.map((r) => {
|
|
186
199
|
if (r.path === '*') return r
|
|
187
200
|
if (!r.path) {
|
|
188
|
-
if (r.children) return { ...r, children: filter(r.children) }
|
|
189
|
-
return r
|
|
190
|
-
}
|
|
191
|
-
if (matchesProto(r)) {
|
|
192
201
|
return r.children ? { ...r, children: filter(r.children) } : r
|
|
193
202
|
}
|
|
194
|
-
|
|
203
|
+
// A route WITH a path is kept iff its first segment matches the
|
|
204
|
+
// requested proto. When it matches, keep its ENTIRE subtree — do
|
|
205
|
+
// NOT re-filter children, since generouted nests sub-routes under
|
|
206
|
+
// the prototype (e.g. `canvas-threats/threats` becomes a `threats`
|
|
207
|
+
// child of `canvas-threats`); re-filtering would drop them because
|
|
208
|
+
// their own segment isn't the proto name.
|
|
209
|
+
return matchesProto(r) ? r : null
|
|
195
210
|
})
|
|
196
211
|
.filter(Boolean)
|
|
197
212
|
return [{
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DO NOT EDIT — storyboard-owned library file.
|
|
3
|
+
*
|
|
4
|
+
* Synced from @dfosco/storyboard. The `workspace` surface (Viewfinder).
|
|
5
|
+
* All presentation props (title, subtitle, logo, tab toggles) come from
|
|
6
|
+
* `pages.workspace` in storyboard.config.json — configure there, never edit
|
|
7
|
+
* this file. See src/library/README.md for the full customization surface.
|
|
8
|
+
*/
|
|
1
9
|
import { flows } from 'virtual:storyboard-data-index'
|
|
2
10
|
import { Workspace } from '@dfosco/storyboard'
|
|
11
|
+
import { getConfig } from '@dfosco/storyboard/config'
|
|
12
|
+
import storyboardConfig from '../../storyboard.config.json'
|
|
3
13
|
|
|
4
14
|
const pageModules = import.meta.glob('/src/prototypes/*.jsx')
|
|
15
|
+
const { pages } = getConfig(storyboardConfig)
|
|
5
16
|
|
|
6
17
|
export default function WorkspacePage() {
|
|
7
18
|
return (
|
|
8
19
|
<Workspace
|
|
9
|
-
|
|
10
|
-
subtitle="Where design work goes"
|
|
20
|
+
{...pages.workspace}
|
|
11
21
|
flows={flows}
|
|
12
22
|
hideDefaultFlow={true}
|
|
13
23
|
pageModules={pageModules}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": false,
|
|
15
|
+
"noUnusedLocals": false,
|
|
16
|
+
"noUnusedParameters": false,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"allowJs": true,
|
|
19
|
+
"baseUrl": ".",
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./src/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["src"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import react from '@vitejs/plugin-react'
|
|
4
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
5
|
+
import generouted from '@generouted/react-router/plugin'
|
|
6
|
+
import storyboardData from '@dfosco/storyboard/vite'
|
|
7
|
+
import storyboardServer from '@dfosco/storyboard/vite/server'
|
|
8
|
+
import postcssGlobalData from '@csstools/postcss-global-data'
|
|
9
|
+
import postcssPresetEnv from 'postcss-preset-env'
|
|
10
|
+
import browsers from '@github/browserslist-config'
|
|
11
|
+
import { globSync } from 'glob'
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
|
14
|
+
|
|
15
|
+
export default defineConfig(() => {
|
|
16
|
+
const base = process.env.VITE_BASE_PATH || '/'
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
base,
|
|
20
|
+
resolve: {
|
|
21
|
+
alias: {
|
|
22
|
+
'@': path.resolve(__dirname, './src'),
|
|
23
|
+
},
|
|
24
|
+
dedupe: ['react', 'react-dom'],
|
|
25
|
+
},
|
|
26
|
+
plugins: [
|
|
27
|
+
tailwindcss(),
|
|
28
|
+
storyboardData(),
|
|
29
|
+
storyboardServer(),
|
|
30
|
+
react(),
|
|
31
|
+
generouted({
|
|
32
|
+
source: {
|
|
33
|
+
routes: './src/prototypes/**/[\\w[-]*.{jsx,tsx,mdx}',
|
|
34
|
+
modals: './src/prototypes/**/[+]*.{jsx,tsx,mdx}',
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
// generouted's built-in watcher only listens for /src/pages/ changes.
|
|
38
|
+
// This plugin triggers a full reload when prototypes are added/removed.
|
|
39
|
+
{
|
|
40
|
+
name: 'prototypes-watcher',
|
|
41
|
+
configureServer(server) {
|
|
42
|
+
const listener = (file = '') => {
|
|
43
|
+
if (file.includes(path.normalize('/src/prototypes/'))) {
|
|
44
|
+
server.ws.send({ type: 'full-reload' })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
server.watcher.on('add', listener)
|
|
48
|
+
server.watcher.on('unlink', listener)
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'base-redirect',
|
|
53
|
+
configureServer(server) {
|
|
54
|
+
const baseNoTrail = base.replace(/\/$/, '')
|
|
55
|
+
server.middlewares.use((req, res, next) => {
|
|
56
|
+
if (req.url === baseNoTrail) {
|
|
57
|
+
res.writeHead(302, { Location: base })
|
|
58
|
+
res.end()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
if (req.url && req.url !== baseNoTrail && !req.url.startsWith(base) && !req.url.startsWith('/@') && !req.url.startsWith('/node_modules/')) {
|
|
62
|
+
const newUrl = baseNoTrail + req.url
|
|
63
|
+
res.writeHead(302, { Location: newUrl })
|
|
64
|
+
res.end()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
next()
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
server: {
|
|
73
|
+
port: 1234,
|
|
74
|
+
fs: { allow: ['..'] },
|
|
75
|
+
warmup: {
|
|
76
|
+
clientFiles: [
|
|
77
|
+
'src/library/mount.jsx',
|
|
78
|
+
'src/library/prototypes-entry.jsx',
|
|
79
|
+
'src/prototypes/**/*.jsx',
|
|
80
|
+
'src/components/**/*.jsx',
|
|
81
|
+
'src/templates/**/*.jsx',
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
optimizeDeps: {
|
|
86
|
+
include: [
|
|
87
|
+
'reshaped',
|
|
88
|
+
'@primer/react',
|
|
89
|
+
'@primer/octicons-react',
|
|
90
|
+
'prop-types',
|
|
91
|
+
'@base-ui/react/accordion',
|
|
92
|
+
'@base-ui/react/checkbox',
|
|
93
|
+
'@base-ui/react/dialog',
|
|
94
|
+
'@base-ui/react/number-field',
|
|
95
|
+
'@base-ui/react/progress',
|
|
96
|
+
'@base-ui/react/radio',
|
|
97
|
+
'@base-ui/react/radio-group',
|
|
98
|
+
'@base-ui/react/select',
|
|
99
|
+
'@base-ui/react/slider',
|
|
100
|
+
'@base-ui/react/switch',
|
|
101
|
+
'@base-ui/react/tabs',
|
|
102
|
+
'@base-ui/react/tooltip',
|
|
103
|
+
// CommonJS leaf pulled in transitively via @dfosco/storyboard ->
|
|
104
|
+
// unified (markdown). unified does `import extend from 'extend'`,
|
|
105
|
+
// but `extend` is CJS; without pre-bundling Vite serves it raw and
|
|
106
|
+
// the default-import interop fails ("does not provide an export
|
|
107
|
+
// named 'default'"). Forcing it into optimizeDeps gives it a
|
|
108
|
+
// synthesized default export.
|
|
109
|
+
'extend',
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
esbuild: {
|
|
113
|
+
// Preserve function names so the storyboard inspector shows
|
|
114
|
+
// real component names instead of minified identifiers
|
|
115
|
+
keepNames: true,
|
|
116
|
+
},
|
|
117
|
+
build: {
|
|
118
|
+
chunkSizeWarningLimit: 700,
|
|
119
|
+
rollupOptions: {
|
|
120
|
+
output: {
|
|
121
|
+
manualChunks: {
|
|
122
|
+
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
|
|
123
|
+
'vendor-primer': ['@primer/react'],
|
|
124
|
+
'vendor-octicons': ['@primer/octicons-react'],
|
|
125
|
+
'vendor-reshaped': ['reshaped'],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
css: {
|
|
131
|
+
postcss: {
|
|
132
|
+
plugins: [
|
|
133
|
+
postcssGlobalData({
|
|
134
|
+
files: globSync(
|
|
135
|
+
'node_modules/@primer/primitives/dist/css/**/*.css',
|
|
136
|
+
{ ignore: ['**/themes/**'] }
|
|
137
|
+
),
|
|
138
|
+
}),
|
|
139
|
+
postcssPresetEnv({
|
|
140
|
+
stage: 2,
|
|
141
|
+
browsers,
|
|
142
|
+
features: {
|
|
143
|
+
'nesting-rules': {
|
|
144
|
+
noIsPseudoSelector: true,
|
|
145
|
+
},
|
|
146
|
+
'focus-visible-pseudo-class': false,
|
|
147
|
+
'logical-properties-and-values': false,
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
test: {
|
|
7
|
+
environment: 'jsdom',
|
|
8
|
+
globals: true,
|
|
9
|
+
css: true,
|
|
10
|
+
setupFiles: ['./src/test-setup.ts'],
|
|
11
|
+
server: {
|
|
12
|
+
deps: {
|
|
13
|
+
inline: ['@primer/react'],
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
})
|
|
@@ -19,6 +19,28 @@ function withTrailingSlash(folder) {
|
|
|
19
19
|
return folder.endsWith('/') ? folder : `${folder}/`
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Folder-independent identity for a canvas, mirroring prototype `dirName`
|
|
24
|
+
* matching. Keeps cross-branch dedup stable when a canvas moves between
|
|
25
|
+
* folders (or root ↔ folder). Proto-scoped canvases retain their prototype
|
|
26
|
+
* segment so same-named canvases in different prototypes never collide.
|
|
27
|
+
*/
|
|
28
|
+
function canvasMatchKey(id) {
|
|
29
|
+
let raw = String(id || '')
|
|
30
|
+
let isProto = false
|
|
31
|
+
if (raw.startsWith('proto:')) {
|
|
32
|
+
isProto = true
|
|
33
|
+
raw = raw.slice('proto:'.length)
|
|
34
|
+
}
|
|
35
|
+
const segments = raw.split('/').filter(Boolean)
|
|
36
|
+
const name = segments[segments.length - 1] || raw
|
|
37
|
+
if (isProto) {
|
|
38
|
+
const scope = segments.length >= 2 ? segments[segments.length - 2] : ''
|
|
39
|
+
return scope ? `proto:${scope}/${name}` : `proto:${name}`
|
|
40
|
+
}
|
|
41
|
+
return name
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
function generatedAtMs(manifest) {
|
|
23
45
|
const value = Date.parse(manifest?.generatedAt || '')
|
|
24
46
|
return Number.isNaN(value) ? 0 : value
|
|
@@ -157,7 +179,7 @@ function addCanvas(index, manifestInfo, item) {
|
|
|
157
179
|
|
|
158
180
|
const { manifest, folder } = manifestInfo
|
|
159
181
|
const branch = deriveBranch(manifest, folder)
|
|
160
|
-
const key =
|
|
182
|
+
const key = canvasMatchKey(id)
|
|
161
183
|
|
|
162
184
|
if (!index.canvases[key]) {
|
|
163
185
|
index.canvases[key] = { id: key, branches: [] }
|
|
@@ -144,7 +144,12 @@ try {
|
|
|
144
144
|
installedVersion = installedVersion || suffix.slice(1)
|
|
145
145
|
const commitMsg = `[storyboard-update] Update storyboard to ${installedVersion}`
|
|
146
146
|
|
|
147
|
-
//
|
|
147
|
+
// Stage update-related files (package.json, lock files, scaffold outputs).
|
|
148
|
+
// The Tier-1 package-owned infra configs + library are force-synced by the
|
|
149
|
+
// scaffold pass and must be committed so `storyboard update` is fully
|
|
150
|
+
// hands-off — customers never hand-patch these. User-facing *.config.json
|
|
151
|
+
// (storyboard.config.json, terminal.config.json, …) are intentionally NOT
|
|
152
|
+
// staged — those are customer-owned.
|
|
148
153
|
const filesToStage = [
|
|
149
154
|
'package.json',
|
|
150
155
|
'package-lock.json',
|
|
@@ -153,11 +158,23 @@ try {
|
|
|
153
158
|
'.github/skills',
|
|
154
159
|
'.github/workflows/scripts/aggregate-artifacts.mjs',
|
|
155
160
|
'scripts',
|
|
161
|
+
// Tier-1 package-owned infrastructure (force-synced):
|
|
162
|
+
'vite.config.js',
|
|
163
|
+
'vitest.config.js',
|
|
164
|
+
'eslint.config.js',
|
|
165
|
+
'tsconfig.json',
|
|
166
|
+
'src/library',
|
|
156
167
|
]
|
|
157
168
|
for (const f of filesToStage) {
|
|
158
169
|
try { execSync(`git add ${f}`, { cwd: process.cwd(), stdio: 'pipe' }) } catch { /* empty */ }
|
|
159
170
|
}
|
|
160
171
|
|
|
172
|
+
// Svelte was removed from the storyboard base — drop the stale empty stub
|
|
173
|
+
// from existing customer repos so the update sheds it automatically.
|
|
174
|
+
try {
|
|
175
|
+
execSync('git rm --quiet --ignore-unmatch svelte.config.js', { cwd: process.cwd(), stdio: 'pipe' })
|
|
176
|
+
} catch { /* empty */ }
|
|
177
|
+
|
|
161
178
|
// Only commit if there are staged changes
|
|
162
179
|
try {
|
|
163
180
|
execSync('git diff --cached --quiet', { cwd: process.cwd(), stdio: 'pipe' })
|