Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions src/utils/charts-catalog-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from './example-workspace'

const catalogSourceRoot = 'benchmarks/conformance/'
const generatedEntryPath = '/__catalog.ts'
const generatedEntryPath = '/__catalog.tsx'
const generatedDocumentPath = '/index.html'
const revisionPattern = /^[a-f0-9]{40}$/
const exactVersionPattern =
Expand Down Expand Up @@ -48,7 +48,10 @@ export function createChartsCatalogExampleDefinition({
}

const initialFile = normalizeCatalogSourcePath(entryPath)
const expectedInitialFile = `/cases/${caseId}/tanstack.ts`
const isPublicExample = initialFile.endsWith('/example.tsx')
const expectedInitialFile = isPublicExample
? `/cases/${caseId}/example.tsx`
: `/cases/${caseId}/tanstack.ts`

if (initialFile !== expectedInitialFile) {
throw new Error(
Expand All @@ -74,6 +77,7 @@ export function createChartsCatalogExampleDefinition({
initialFile,
chartHeight,
renderRevision,
isPublicExample,
)
workspaceFiles[generatedDocumentPath] = createCatalogDocument(chartHeight)

Expand Down Expand Up @@ -171,6 +175,50 @@ function createCatalogEntry(
initialFile: string,
chartHeight: number,
renderRevision: number,
isPublicExample: boolean,
) {
if (!isPublicExample) {
return createLegacyCatalogEntry(initialFile, chartHeight, renderRevision)
}

return `import { createRoot } from 'react-dom/client'
import type { ComponentType } from 'react'
import Example from ${JSON.stringify(initialFile)}

const root = document.querySelector<HTMLElement>('#root')
if (!root) throw new Error('Charts catalog root not found')

const height = ${chartHeight}
let width = Math.max(1, Math.floor(root.getBoundingClientRect().width))
const CatalogExample = Example as ComponentType<{
width?: number
height?: number
revision?: number
}>
const reactRoot = createRoot(root)
const render = () => reactRoot.render(
<CatalogExample width={width} height={height} revision={${renderRevision}} />
)
render()
const observer = new ResizeObserver(() => {
const nextWidth = Math.max(1, Math.floor(root.getBoundingClientRect().width))
if (nextWidth === width) return
width = nextWidth
render()
})

observer.observe(root)
window.addEventListener('pagehide', () => {
observer.disconnect()
reactRoot.unmount()
}, { once: true })
`
}

function createLegacyCatalogEntry(
initialFile: string,
chartHeight: number,
renderRevision: number,
) {
return `import { mount } from ${JSON.stringify(initialFile)}

Expand Down
79 changes: 66 additions & 13 deletions src/utils/charts-catalog-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,29 @@ const httpsUrlSchema = v.pipe(
v.url(),
v.check((value) => new URL(value).protocol === 'https:', 'Expected HTTPS'),
)
const referenceRendererSchema = v.picklist([
const legacyReferenceRendererSchema = v.picklist([
'observable-plot',
'recharts',
'echarts',
])
const caseEntryPathSchema = v.pipe(
const legacyCaseEntryPathSchema = v.pipe(
v.string(),
v.regex(
/^benchmarks\/conformance\/cases\/[a-z0-9]+(?:-[a-z0-9]+)*\/(?:tanstack|plot|recharts|echarts)\.ts$/,
'Invalid legacy catalog entry path',
),
)
const caseEntryPathSchema = v.pipe(
v.string(),
v.regex(
/^benchmarks\/conformance\/cases\/[a-z0-9]+(?:-[a-z0-9]+)*\/example\.tsx$/,
'Invalid catalog entry path',
),
)

// Deliberately use `object` here. The Charts benchmark owns geometry and
// interaction metadata; the site validates and retains only its UI contract.
const catalogCaseSchema = v.pipe(
const legacyCatalogCaseSchema = v.pipe(
v.object({
schemaVersion: v.literal(1),
order: nonNegativeIntegerSchema,
Expand All @@ -67,10 +74,10 @@ const catalogCaseSchema = v.pipe(
maintain: nonEmptyStringSchema,
}),
entries: v.strictObject({
tanstack: caseEntryPathSchema,
tanstack: legacyCaseEntryPathSchema,
reference: v.strictObject({
renderer: referenceRendererSchema,
path: caseEntryPathSchema,
renderer: legacyReferenceRendererSchema,
path: legacyCaseEntryPathSchema,
}),
}),
}),
Expand All @@ -83,7 +90,7 @@ const catalogCaseSchema = v.pipe(
(catalogCase) =>
catalogCase.entries.tanstack ===
`benchmarks/conformance/cases/${catalogCase.id}/tanstack.ts`,
'Catalog case TanStack entry must match its ID',
'Legacy catalog case TanStack entry must match its ID',
),
v.check((catalogCase) => {
const filename =
Expand All @@ -94,18 +101,64 @@ const catalogCaseSchema = v.pipe(
catalogCase.entries.reference.path ===
`benchmarks/conformance/cases/${catalogCase.id}/${filename}.ts`
)
}, 'Catalog case reference entry must match its ID and renderer'),
}, 'Legacy catalog case reference entry must match its ID and renderer'),
)

const catalogIndexSchema = v.pipe(
v.strictObject({
const catalogCaseSchema = v.pipe(
v.object({
schemaVersion: v.literal(1),
order: nonNegativeIntegerSchema,
id: caseIdSchema,
collection: v.optional(caseIdSchema),
title: nonEmptyStringSchema,
family: nonEmptyStringSchema,
intent: nonEmptyStringSchema,
support: v.picklist(['native', 'composed', 'gap', 'deferred']),
features: v.array(nonEmptyStringSchema),
source: v.strictObject({
repo: v.literal(chartsCatalogIndexRepo),
pathRoot: v.literal('benchmarks/conformance/'),
title: nonEmptyStringSchema,
url: httpsUrlSchema,
}),
ai: v.strictObject({
create: nonEmptyStringSchema,
maintain: nonEmptyStringSchema,
}),
entries: v.strictObject({
example: caseEntryPathSchema,
}),
cases: v.pipe(v.array(catalogCaseSchema), v.minLength(1)),
}),
v.check(
(catalogCase) =>
new Set(catalogCase.features).size === catalogCase.features.length,
'Catalog case features must be unique',
),
v.check(
(catalogCase) =>
catalogCase.entries.example ===
`benchmarks/conformance/cases/${catalogCase.id}/example.tsx`,
'Catalog case example entry must match its ID',
),
)

const catalogIndexSchema = v.pipe(
v.union([
v.strictObject({
schemaVersion: v.literal(1),
source: v.strictObject({
repo: v.literal(chartsCatalogIndexRepo),
pathRoot: v.literal('benchmarks/conformance/'),
}),
cases: v.pipe(v.array(legacyCatalogCaseSchema), v.minLength(1)),
}),
v.strictObject({
schemaVersion: v.literal(2),
source: v.strictObject({
repo: v.literal(chartsCatalogIndexRepo),
pathRoot: v.literal('benchmarks/conformance/'),
}),
cases: v.pipe(v.array(catalogCaseSchema), v.minLength(1)),
}),
]),
v.check((index) => {
const ids = index.cases.map((catalogCase) => catalogCase.id)
return new Set(ids).size === ids.length
Expand Down
30 changes: 23 additions & 7 deletions src/utils/charts-catalog.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
chartsCatalogRepo,
type ChartsCatalogAuthoredSource,
} from './charts-catalog'
import type { ChartsCatalogIndexPublication } from './charts-catalog-index'
import type {
ChartsCatalogIndexCase,
ChartsCatalogIndexPublication,
} from './charts-catalog-index'
import {
createChartsCatalogExampleDefinition,
type ChartsCatalogExampleVersions,
Expand Down Expand Up @@ -83,11 +86,12 @@ export async function getChartsCatalogExample(
`Charts catalog case not found: ${caseId}`,
)
}
const entryPath = getChartsCatalogEntryPath(catalogCase)

const [files, versions] = await Promise.all([
getChartsCatalogExampleFiles(
publication.revision,
catalogCase.entries.tanstack,
entryPath,
publication.sourceKind,
),
getChartsCatalogExampleVersions(
Expand All @@ -103,18 +107,21 @@ export async function getChartsCatalogExample(
title: catalogCase.title,
description: catalogCase.intent,
revision: publication.revision,
entryPath: catalogCase.entries.tanstack,
entryPath,
files,
renderRevision: options?.renderRevision,
versions,
}),
authoredSource: createChartsCatalogAuthoredSource(
files,
catalogCase.entries.tanstack,
),
authoredSource: createChartsCatalogAuthoredSource(files, entryPath),
}
}

function getChartsCatalogEntryPath(catalogCase: ChartsCatalogIndexCase) {
return 'example' in catalogCase.entries
? catalogCase.entries.example
: catalogCase.entries.tanstack
}

export async function getChartsCatalogExampleDefinition(
publication: ChartsCatalogIndexPublication,
caseId: string,
Expand Down Expand Up @@ -181,6 +188,8 @@ async function getChartsCatalogExampleFiles(
entryPath: string,
sourceKind: ChartsCatalogIndexPublication['sourceKind'],
) {
const caseDirectory = entryPath.slice(0, entryPath.lastIndexOf('/') + 1)
const isSelfContainedExample = entryPath.endsWith('/example.tsx')
const sourcePaths =
sourceKind === 'local'
? undefined
Expand Down Expand Up @@ -211,6 +220,13 @@ async function getChartsCatalogExampleFiles(
resolveCatalogExampleModule(path, specifier, sourcePaths, revision),
),
)
for (const dependency of dependencies) {
if (isSelfContainedExample && !dependency.startsWith(caseDirectory)) {
throw new ChartsCatalogIntegrityError(
`Charts catalog example import leaves its case directory: ${dependency}`,
)
}
}
await Promise.all(dependencies.map(load))
}

Expand Down
Loading
Loading