Skip to content

Commit 22f95f9

Browse files
Run Charts catalog sandboxes from public examples (#1159)
Run catalog sandboxes from public examples
1 parent ec06325 commit 22f95f9

8 files changed

Lines changed: 302 additions & 92 deletions

src/utils/charts-catalog-example.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from './example-workspace'
66

77
const catalogSourceRoot = 'benchmarks/conformance/'
8-
const generatedEntryPath = '/__catalog.ts'
8+
const generatedEntryPath = '/__catalog.tsx'
99
const generatedDocumentPath = '/index.html'
1010
const revisionPattern = /^[a-f0-9]{40}$/
1111
const exactVersionPattern =
@@ -48,7 +48,10 @@ export function createChartsCatalogExampleDefinition({
4848
}
4949

5050
const initialFile = normalizeCatalogSourcePath(entryPath)
51-
const expectedInitialFile = `/cases/${caseId}/tanstack.ts`
51+
const isPublicExample = initialFile.endsWith('/example.tsx')
52+
const expectedInitialFile = isPublicExample
53+
? `/cases/${caseId}/example.tsx`
54+
: `/cases/${caseId}/tanstack.ts`
5255

5356
if (initialFile !== expectedInitialFile) {
5457
throw new Error(
@@ -74,6 +77,7 @@ export function createChartsCatalogExampleDefinition({
7477
initialFile,
7578
chartHeight,
7679
renderRevision,
80+
isPublicExample,
7781
)
7882
workspaceFiles[generatedDocumentPath] = createCatalogDocument(chartHeight)
7983

@@ -171,6 +175,50 @@ function createCatalogEntry(
171175
initialFile: string,
172176
chartHeight: number,
173177
renderRevision: number,
178+
isPublicExample: boolean,
179+
) {
180+
if (!isPublicExample) {
181+
return createLegacyCatalogEntry(initialFile, chartHeight, renderRevision)
182+
}
183+
184+
return `import { createRoot } from 'react-dom/client'
185+
import type { ComponentType } from 'react'
186+
import Example from ${JSON.stringify(initialFile)}
187+
188+
const root = document.querySelector<HTMLElement>('#root')
189+
if (!root) throw new Error('Charts catalog root not found')
190+
191+
const height = ${chartHeight}
192+
let width = Math.max(1, Math.floor(root.getBoundingClientRect().width))
193+
const CatalogExample = Example as ComponentType<{
194+
width?: number
195+
height?: number
196+
revision?: number
197+
}>
198+
const reactRoot = createRoot(root)
199+
const render = () => reactRoot.render(
200+
<CatalogExample width={width} height={height} revision={${renderRevision}} />
201+
)
202+
render()
203+
const observer = new ResizeObserver(() => {
204+
const nextWidth = Math.max(1, Math.floor(root.getBoundingClientRect().width))
205+
if (nextWidth === width) return
206+
width = nextWidth
207+
render()
208+
})
209+
210+
observer.observe(root)
211+
window.addEventListener('pagehide', () => {
212+
observer.disconnect()
213+
reactRoot.unmount()
214+
}, { once: true })
215+
`
216+
}
217+
218+
function createLegacyCatalogEntry(
219+
initialFile: string,
220+
chartHeight: number,
221+
renderRevision: number,
174222
) {
175223
return `import { mount } from ${JSON.stringify(initialFile)}
176224

src/utils/charts-catalog-index.ts

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,29 @@ const httpsUrlSchema = v.pipe(
3232
v.url(),
3333
v.check((value) => new URL(value).protocol === 'https:', 'Expected HTTPS'),
3434
)
35-
const referenceRendererSchema = v.picklist([
35+
const legacyReferenceRendererSchema = v.picklist([
3636
'observable-plot',
3737
'recharts',
3838
'echarts',
3939
])
40-
const caseEntryPathSchema = v.pipe(
40+
const legacyCaseEntryPathSchema = v.pipe(
4141
v.string(),
4242
v.regex(
4343
/^benchmarks\/conformance\/cases\/[a-z0-9]+(?:-[a-z0-9]+)*\/(?:tanstack|plot|recharts|echarts)\.ts$/,
44+
'Invalid legacy catalog entry path',
45+
),
46+
)
47+
const caseEntryPathSchema = v.pipe(
48+
v.string(),
49+
v.regex(
50+
/^benchmarks\/conformance\/cases\/[a-z0-9]+(?:-[a-z0-9]+)*\/example\.tsx$/,
4451
'Invalid catalog entry path',
4552
),
4653
)
4754

4855
// Deliberately use `object` here. The Charts benchmark owns geometry and
4956
// interaction metadata; the site validates and retains only its UI contract.
50-
const catalogCaseSchema = v.pipe(
57+
const legacyCatalogCaseSchema = v.pipe(
5158
v.object({
5259
schemaVersion: v.literal(1),
5360
order: nonNegativeIntegerSchema,
@@ -67,10 +74,10 @@ const catalogCaseSchema = v.pipe(
6774
maintain: nonEmptyStringSchema,
6875
}),
6976
entries: v.strictObject({
70-
tanstack: caseEntryPathSchema,
77+
tanstack: legacyCaseEntryPathSchema,
7178
reference: v.strictObject({
72-
renderer: referenceRendererSchema,
73-
path: caseEntryPathSchema,
79+
renderer: legacyReferenceRendererSchema,
80+
path: legacyCaseEntryPathSchema,
7481
}),
7582
}),
7683
}),
@@ -83,7 +90,7 @@ const catalogCaseSchema = v.pipe(
8390
(catalogCase) =>
8491
catalogCase.entries.tanstack ===
8592
`benchmarks/conformance/cases/${catalogCase.id}/tanstack.ts`,
86-
'Catalog case TanStack entry must match its ID',
93+
'Legacy catalog case TanStack entry must match its ID',
8794
),
8895
v.check((catalogCase) => {
8996
const filename =
@@ -94,18 +101,64 @@ const catalogCaseSchema = v.pipe(
94101
catalogCase.entries.reference.path ===
95102
`benchmarks/conformance/cases/${catalogCase.id}/${filename}.ts`
96103
)
97-
}, 'Catalog case reference entry must match its ID and renderer'),
104+
}, 'Legacy catalog case reference entry must match its ID and renderer'),
98105
)
99106

100-
const catalogIndexSchema = v.pipe(
101-
v.strictObject({
107+
const catalogCaseSchema = v.pipe(
108+
v.object({
102109
schemaVersion: v.literal(1),
110+
order: nonNegativeIntegerSchema,
111+
id: caseIdSchema,
112+
collection: v.optional(caseIdSchema),
113+
title: nonEmptyStringSchema,
114+
family: nonEmptyStringSchema,
115+
intent: nonEmptyStringSchema,
116+
support: v.picklist(['native', 'composed', 'gap', 'deferred']),
117+
features: v.array(nonEmptyStringSchema),
103118
source: v.strictObject({
104-
repo: v.literal(chartsCatalogIndexRepo),
105-
pathRoot: v.literal('benchmarks/conformance/'),
119+
title: nonEmptyStringSchema,
120+
url: httpsUrlSchema,
121+
}),
122+
ai: v.strictObject({
123+
create: nonEmptyStringSchema,
124+
maintain: nonEmptyStringSchema,
125+
}),
126+
entries: v.strictObject({
127+
example: caseEntryPathSchema,
106128
}),
107-
cases: v.pipe(v.array(catalogCaseSchema), v.minLength(1)),
108129
}),
130+
v.check(
131+
(catalogCase) =>
132+
new Set(catalogCase.features).size === catalogCase.features.length,
133+
'Catalog case features must be unique',
134+
),
135+
v.check(
136+
(catalogCase) =>
137+
catalogCase.entries.example ===
138+
`benchmarks/conformance/cases/${catalogCase.id}/example.tsx`,
139+
'Catalog case example entry must match its ID',
140+
),
141+
)
142+
143+
const catalogIndexSchema = v.pipe(
144+
v.union([
145+
v.strictObject({
146+
schemaVersion: v.literal(1),
147+
source: v.strictObject({
148+
repo: v.literal(chartsCatalogIndexRepo),
149+
pathRoot: v.literal('benchmarks/conformance/'),
150+
}),
151+
cases: v.pipe(v.array(legacyCatalogCaseSchema), v.minLength(1)),
152+
}),
153+
v.strictObject({
154+
schemaVersion: v.literal(2),
155+
source: v.strictObject({
156+
repo: v.literal(chartsCatalogIndexRepo),
157+
pathRoot: v.literal('benchmarks/conformance/'),
158+
}),
159+
cases: v.pipe(v.array(catalogCaseSchema), v.minLength(1)),
160+
}),
161+
]),
109162
v.check((index) => {
110163
const ids = index.cases.map((catalogCase) => catalogCase.id)
111164
return new Set(ids).size === ids.length

src/utils/charts-catalog.server.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {
77
chartsCatalogRepo,
88
type ChartsCatalogAuthoredSource,
99
} from './charts-catalog'
10-
import type { ChartsCatalogIndexPublication } from './charts-catalog-index'
10+
import type {
11+
ChartsCatalogIndexCase,
12+
ChartsCatalogIndexPublication,
13+
} from './charts-catalog-index'
1114
import {
1215
createChartsCatalogExampleDefinition,
1316
type ChartsCatalogExampleVersions,
@@ -83,11 +86,12 @@ export async function getChartsCatalogExample(
8386
`Charts catalog case not found: ${caseId}`,
8487
)
8588
}
89+
const entryPath = getChartsCatalogEntryPath(catalogCase)
8690

8791
const [files, versions] = await Promise.all([
8892
getChartsCatalogExampleFiles(
8993
publication.revision,
90-
catalogCase.entries.tanstack,
94+
entryPath,
9195
publication.sourceKind,
9296
),
9397
getChartsCatalogExampleVersions(
@@ -103,18 +107,21 @@ export async function getChartsCatalogExample(
103107
title: catalogCase.title,
104108
description: catalogCase.intent,
105109
revision: publication.revision,
106-
entryPath: catalogCase.entries.tanstack,
110+
entryPath,
107111
files,
108112
renderRevision: options?.renderRevision,
109113
versions,
110114
}),
111-
authoredSource: createChartsCatalogAuthoredSource(
112-
files,
113-
catalogCase.entries.tanstack,
114-
),
115+
authoredSource: createChartsCatalogAuthoredSource(files, entryPath),
115116
}
116117
}
117118

119+
function getChartsCatalogEntryPath(catalogCase: ChartsCatalogIndexCase) {
120+
return 'example' in catalogCase.entries
121+
? catalogCase.entries.example
122+
: catalogCase.entries.tanstack
123+
}
124+
118125
export async function getChartsCatalogExampleDefinition(
119126
publication: ChartsCatalogIndexPublication,
120127
caseId: string,
@@ -181,6 +188,8 @@ async function getChartsCatalogExampleFiles(
181188
entryPath: string,
182189
sourceKind: ChartsCatalogIndexPublication['sourceKind'],
183190
) {
191+
const caseDirectory = entryPath.slice(0, entryPath.lastIndexOf('/') + 1)
192+
const isSelfContainedExample = entryPath.endsWith('/example.tsx')
184193
const sourcePaths =
185194
sourceKind === 'local'
186195
? undefined
@@ -211,6 +220,13 @@ async function getChartsCatalogExampleFiles(
211220
resolveCatalogExampleModule(path, specifier, sourcePaths, revision),
212221
),
213222
)
223+
for (const dependency of dependencies) {
224+
if (isSelfContainedExample && !dependency.startsWith(caseDirectory)) {
225+
throw new ChartsCatalogIntegrityError(
226+
`Charts catalog example import leaves its case directory: ${dependency}`,
227+
)
228+
}
229+
}
214230
await Promise.all(dependencies.map(load))
215231
}
216232

0 commit comments

Comments
 (0)