Skip to content

Commit 7832231

Browse files
feat: idb infrastructure. (#63)
1 parent 1bc9ef1 commit 7832231

10 files changed

Lines changed: 623 additions & 1 deletion

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
pull_request:
88
branches:
99
- main
10+
- next
1011

1112
jobs:
1213
ci:

.github/workflows/playwright.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
pull_request:
55
branches:
66
- main
7+
- next
78
types:
89
- opened
910
- synchronize

docs/build-and-deploy.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ This command forces `KNIGHTED_PRIMARY_CDN=esm` and runs `npm run build` first, t
114114
Related docs:
115115

116116
- `docs/code-mirror.md` for CodeMirror CDN integration rules, fallback behavior, and validation checklist.
117+
- `docs/dual-build-gh-pages-strategy.md` for the clean two-URL stable and next deployment model during UI migration.
117118

118119
- `src/modules/cdn.js` is the source of truth for CDN-managed runtime libraries (including fallback candidates). Add/update CDN specs there instead of hardcoding module URLs inside feature modules.
119120

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Dual Build GitHub Pages Strategy
2+
3+
## Purpose
4+
5+
Document a clean migration strategy for delivering both stable and overhaul UI versions from one repository without adding runtime feature flags to application code.
6+
7+
## Core Idea
8+
9+
Build two versions of the site during deployment and publish both under one GitHub Pages branch.
10+
11+
- Stable site at root path: /index.html
12+
- Overhaul site at next path: /next/index.html
13+
14+
The URL path acts as the switch.
15+
16+
- Stable: /develop/
17+
- Overhaul: /develop/next/
18+
19+
## Why This Approach
20+
21+
1. Keeps runtime code clean.
22+
2. Avoids pervasive if version checks in app modules.
23+
3. Allows side-by-side validation of stable and next UX.
24+
4. Reduces long-term cleanup work versus in-app toggles.
25+
26+
## Deployment Layout
27+
28+
Publish a combined artifact to the GitHub Pages branch with this shape:
29+
30+
- /index.html and root assets from stable branch build
31+
- /next/index.html and next assets from overhaul branch build
32+
33+
## CI Workflow Design
34+
35+
A deployment workflow builds both branches in one run and publishes one artifact.
36+
37+
1. Checkout stable branch into an isolated worktree directory.
38+
2. Install dependencies and build stable output.
39+
3. Copy stable output into publish root.
40+
4. Checkout overhaul branch into a second isolated worktree directory.
41+
5. Install dependencies and build overhaul output.
42+
6. Copy overhaul output into publish root under /next.
43+
7. Deploy combined publish folder to GitHub Pages.
44+
45+
## Operational Guidance
46+
47+
1. Run both builds in isolated directories to prevent cross-branch contamination.
48+
2. Keep Node and npm versions pinned consistently in CI.
49+
3. Use workflow concurrency to cancel outdated deploy jobs.
50+
4. Use relative asset URLs so content works under both root and /next paths.
51+
5. Fail the deploy if either build fails.
52+
53+
## Source Control Model
54+
55+
- main branch represents stable production UX.
56+
- overhaul branch represents next-generation UX.
57+
- Deploy workflow may trigger on pushes to either branch, but each run should still build both branches for a consistent dual-output artifact.
58+
59+
## Relationship To App Architecture Work
60+
61+
This strategy complements the multi-tab and local workspace migration by separating rollout concerns from runtime logic.
62+
63+
- Runtime implementation remains modular and focused on architecture.
64+
- Deployment controls the exposure of stable versus next.
65+
66+
## Tradeoffs
67+
68+
Pros:
69+
70+
1. Cleaner codebase during migration.
71+
2. Lower risk of runtime toggle regressions.
72+
3. Clear QA and stakeholder review URLs.
73+
74+
Cons:
75+
76+
1. Longer deploy times due to dual builds.
77+
2. More CI configuration complexity.
78+
3. Temporary branch coordination requirements.
79+
80+
## Exit Plan
81+
82+
After next UI is production-ready:
83+
84+
1. Promote next code into main.
85+
2. Remove dual-build deployment logic.
86+
3. Publish only root output again.
87+
4. Remove migration-only docs and branch conventions.
88+
89+
## Suggested Follow-up
90+
91+
1. Add a deploy workflow implementation doc with exact GitHub Actions YAML and permissions.
92+
2. Add a release checklist for validating both URLs before each deploy.
93+
3. Add ownership notes for stable and next branch review responsibilities.

src/bootstrap.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const preloadImportKeys = [
1313
'jsxReact',
1414
'react',
1515
'reactDomClient',
16+
'idb',
1617
]
1718

1819
const isImportMapPrimary =

src/modules/cdn.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ export const cdnImportSpecs = {
7474
esm: 'react-dom@19.2.4/client',
7575
jspmGa: 'npm:react-dom@19.2.4/client.js',
7676
},
77+
idb: {
78+
importMap: 'idb',
79+
esm: 'idb@8.0.3',
80+
unpkg: 'idb@8.0.3/build/index.js?module',
81+
jspmGa: 'npm:idb@8.0.3/build/index.js',
82+
},
7783
sass: {
7884
importMap: 'sass',
7985
esm: [
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const previewEntryNamePattern = /(?:^|\/)(?:app|main)\.[jt]sx?$/i
2+
3+
const normalizeTabIdentity = tab => {
4+
if (!tab || typeof tab !== 'object') {
5+
return ''
6+
}
7+
8+
if (typeof tab.path === 'string' && tab.path.trim().length > 0) {
9+
return tab.path.trim()
10+
}
11+
12+
if (typeof tab.name === 'string' && tab.name.trim().length > 0) {
13+
return tab.name.trim()
14+
}
15+
16+
return ''
17+
}
18+
19+
export const isPreviewEntryTab = tab =>
20+
previewEntryNamePattern.test(normalizeTabIdentity(tab))
21+
22+
export const resolvePreviewEntryTab = tabs => {
23+
if (!Array.isArray(tabs) || tabs.length === 0) {
24+
return null
25+
}
26+
27+
return tabs.find(isPreviewEntryTab) ?? null
28+
}
29+
30+
export const canRenderPreview = ({ tabs, fallbackSource = '' } = {}) => {
31+
if (Array.isArray(tabs) && tabs.length > 0) {
32+
return Boolean(resolvePreviewEntryTab(tabs))
33+
}
34+
35+
return typeof fallbackSource === 'string' && fallbackSource.trim().length > 0
36+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
const normalizeImportSpecifier = value =>
2+
typeof value === 'string' && value.trim().length > 0 ? value.trim() : null
3+
4+
const normalizeGraphEntry = entry => {
5+
if (!entry || typeof entry !== 'object') {
6+
return null
7+
}
8+
9+
if (typeof entry.tabId !== 'string' || entry.tabId.length === 0) {
10+
return null
11+
}
12+
13+
const imports = Array.isArray(entry.imports)
14+
? entry.imports.map(normalizeImportSpecifier).filter(Boolean)
15+
: []
16+
17+
return {
18+
tabId: entry.tabId,
19+
contentHash: typeof entry.contentHash === 'string' ? entry.contentHash : '',
20+
imports,
21+
lastUpdated:
22+
typeof entry.lastUpdated === 'number' && Number.isFinite(entry.lastUpdated)
23+
? entry.lastUpdated
24+
: Date.now(),
25+
}
26+
}
27+
28+
export const createPreviewWorkspaceGraphCache = () => {
29+
const byTabId = new Map()
30+
31+
const upsert = entry => {
32+
const normalized = normalizeGraphEntry(entry)
33+
34+
if (!normalized) {
35+
throw new TypeError('Graph entry is invalid.')
36+
}
37+
38+
byTabId.set(normalized.tabId, normalized)
39+
return normalized
40+
}
41+
42+
const get = tabId => {
43+
if (typeof tabId !== 'string' || tabId.length === 0) {
44+
return null
45+
}
46+
47+
return byTabId.get(tabId) ?? null
48+
}
49+
50+
const getDependents = targetImportSpecifier => {
51+
const normalizedSpecifier = normalizeImportSpecifier(targetImportSpecifier)
52+
53+
if (!normalizedSpecifier) {
54+
return []
55+
}
56+
57+
const dependents = []
58+
59+
for (const entry of byTabId.values()) {
60+
if (entry.imports.includes(normalizedSpecifier)) {
61+
dependents.push(entry)
62+
}
63+
}
64+
65+
return dependents
66+
}
67+
68+
const remove = tabId => {
69+
if (typeof tabId !== 'string' || tabId.length === 0) {
70+
return false
71+
}
72+
73+
return byTabId.delete(tabId)
74+
}
75+
76+
const clear = () => {
77+
byTabId.clear()
78+
}
79+
80+
const list = () => [...byTabId.values()]
81+
82+
return {
83+
upsert,
84+
get,
85+
getDependents,
86+
remove,
87+
clear,
88+
list,
89+
}
90+
}

src/modules/render-runtime.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
getFunctionLikeDeclarationNames,
44
hasFunctionLikeDeclarationNamed,
55
} from './jsx-top-level-declarations.js'
6+
import { canRenderPreview } from './preview-entry-resolver.js'
67
import { ensureJsxTransformSource } from './jsx-transform-runtime.js'
78

89
export const createRenderRuntimeController = ({
@@ -14,6 +15,7 @@ export const createRenderRuntimeController = ({
1415
isAutoRenderEnabled = () => false,
1516
getCssSource,
1617
getJsxSource,
18+
getWorkspaceTabs,
1719
getPreviewHost,
1820
setPreviewHost,
1921
applyPreviewBackgroundColor,
@@ -866,7 +868,14 @@ export const createRenderRuntimeController = ({
866868
}
867869
}
868870

869-
const hasComponentSource = () => getJsxSource().trim().length > 0
871+
const hasComponentSource = () => {
872+
const tabs = typeof getWorkspaceTabs === 'function' ? getWorkspaceTabs() : undefined
873+
874+
return canRenderPreview({
875+
tabs,
876+
fallbackSource: getJsxSource(),
877+
})
878+
}
870879

871880
const clearPreview = () => {
872881
const target = getRenderTarget()

0 commit comments

Comments
 (0)