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
4 changes: 2 additions & 2 deletions src/utils/builder-project-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
import { decodeExampleBinaryFile } from './example-workspace'

const maxCanonicalBytes = 1024 * 1024
const maxFileBytes = 512 * 1024
const maxFileBytes = maxCanonicalBytes
const maxFiles = 128
const maxPathBytes = 512
const maxTitleCharacters = 160
Expand Down Expand Up @@ -41,7 +41,7 @@ export function validateBuilderProjectSnapshot(project: SharedExampleProject) {
throw new Error(`Builder path exceeds 512 bytes: ${path}`)
}
if (byteLength > maxFileBytes) {
throw new Error(`Builder file exceeds 512 KiB: ${path}`)
throw new Error(`Builder file exceeds 1 MiB: ${path}`)
}
}

Expand Down
12 changes: 4 additions & 8 deletions src/utils/charts-catalog-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function createChartsCatalogExampleDefinition({
workspace: createExampleWorkspace({
entry: generatedEntryPath,
files: workspaceFiles,
imports: createCatalogImports(revision, versions),
imports: createCatalogImports(versions),
}),
}
}
Expand All @@ -113,10 +113,7 @@ function normalizeCatalogSourcePath(path: string) {
return workspacePath
}

function createCatalogImports(
revision: string,
versions: ChartsCatalogExampleVersions,
) {
function createCatalogImports(versions: ChartsCatalogExampleVersions) {
const imports: Record<string, string> = {}

for (const [specifier, version] of Object.entries(versions.dependencies).sort(
Expand All @@ -142,9 +139,8 @@ function createCatalogImports(
imports['react-dom/client'] =
`${packageUrl('react-dom', versions.reactDom)}/client`

const dataUrl = `https://esm.sh/gh/TanStack/charts@${revision}/packages/charts-demo-data/src/`
imports['@charts-poc/demo-data/'] = dataUrl
imports['@tanstack/charts-data/'] = dataUrl
imports['@charts-poc/demo-data/'] = '/packages/charts-demo-data/src/'
imports['@tanstack/charts-data/'] = '/packages/charts-demo-data/src/'

return imports
}
Expand Down
54 changes: 41 additions & 13 deletions src/utils/charts-catalog.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
} from './charts-catalog-example'

const catalogSourceRoot = 'benchmarks/conformance/'
const catalogDataRoot = 'packages/charts-demo-data/src/'
const catalogDataPrefixes = ['@tanstack/charts-data/', '@charts-poc/demo-data/']
const catalogExamplePackagePaths = {
charts: 'packages/charts-core/package.json',
root: 'package.json',
Expand Down Expand Up @@ -215,18 +217,22 @@ async function getChartsCatalogExampleFiles(
const source = await getChartsCatalogSource(revision, path, sourceKind)
files.set(path, source)

const sourceRoot = path.startsWith(catalogDataRoot)
? catalogDataRoot
: isSelfContainedExample
? caseDirectory
: undefined
const dependencies = await Promise.all(
extractStaticRelativeModuleSpecifiers(source).map((specifier) =>
resolveCatalogExampleModule(path, specifier, sourcePaths, revision),
extractStaticCatalogModuleSpecifiers(source).map((specifier) =>
resolveCatalogExampleModule(
path,
specifier,
sourcePaths,
revision,
sourceRoot,
),
),
)
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 Expand Up @@ -254,11 +260,26 @@ async function resolveCatalogExampleModule(
specifier: string,
sourcePaths: Set<string> | undefined,
revision: string,
sourceRoot: string | undefined,
) {
const dataPrefix = catalogDataPrefixes.find((prefix) =>
specifier.startsWith(prefix),
)
const importerDirectory = importer.slice(0, importer.lastIndexOf('/'))
const requestedPath = normalizeRepoModulePath(
`${importerDirectory}/${specifier}`,
dataPrefix
? `${catalogDataRoot}${specifier.slice(dataPrefix.length)}`
: `${importerDirectory}/${specifier}`,
)
const allowedRoot = dataPrefix ? catalogDataRoot : sourceRoot
if (
(allowedRoot && !requestedPath.startsWith(allowedRoot)) ||
(dataPrefix && specifier.slice(dataPrefix.length).split('/').includes('..'))
) {
throw new ChartsCatalogIntegrityError(
`Charts catalog source import leaves its allowed directory: ${specifier} from ${importer}`,
)
}
const hasKnownExtension = catalogExampleModuleExtensions.some((extension) =>
requestedPath.endsWith(extension),
)
Expand Down Expand Up @@ -314,7 +335,7 @@ function normalizeRepoModulePath(path: string) {
return segments.join('/')
}

function extractStaticRelativeModuleSpecifiers(source: string) {
function extractStaticCatalogModuleSpecifiers(source: string) {
const tokens = tokenizeModuleSource(source)
const specifiers = new Set<string>()

Expand All @@ -328,7 +349,7 @@ function extractStaticRelativeModuleSpecifiers(source: string) {
if (token.value === 'import' && next?.value === '.') continue

if (next?.kind === 'string') {
if (next.value.startsWith('.')) specifiers.add(next.value)
if (isCatalogModuleSpecifier(next.value)) specifiers.add(next.value)
continue
}

Expand All @@ -339,7 +360,7 @@ function extractStaticRelativeModuleSpecifiers(source: string) {
continue
}
const value = tokens[cursor + 1]
if (value?.kind === 'string' && value.value.startsWith('.')) {
if (value?.kind === 'string' && isCatalogModuleSpecifier(value.value)) {
specifiers.add(value.value)
}
break
Expand All @@ -349,6 +370,13 @@ function extractStaticRelativeModuleSpecifiers(source: string) {
return [...specifiers]
}

function isCatalogModuleSpecifier(specifier: string) {
return (
specifier.startsWith('.') ||
catalogDataPrefixes.some((prefix) => specifier.startsWith(prefix))
)
}

type ModuleSourceToken = {
kind: 'identifier' | 'punctuation' | 'string'
value: string
Expand Down
16 changes: 11 additions & 5 deletions src/utils/example-esbuild.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import {
decodeExampleBinaryFile,
normalizeExamplePath,
resolveExampleWorkspaceImport,
type ExampleWorkspace,
} from './example-workspace'

Expand Down Expand Up @@ -70,7 +71,7 @@ export async function compileExampleWorkspace(
metafile: true,
outdir: '/out',
platform: 'browser',
plugins: [createWorkspacePlugin(files)],
plugins: [createWorkspacePlugin(files, workspace.imports)],
sourcemap: 'inline',
target: 'es2022',
write: false,
Expand Down Expand Up @@ -122,15 +123,19 @@ function getExternalSpecifiers(metafile: esbuild.Metafile) {
)
}

function createWorkspacePlugin(files: WorkspaceBuildFiles): esbuild.Plugin {
function createWorkspacePlugin(
files: WorkspaceBuildFiles,
imports: ExampleWorkspace['imports'],
): esbuild.Plugin {
return {
name: workspaceNamespace,
setup(build) {
build.onResolve({ filter: /.*/ }, (args) => {
if (args.path.startsWith('https://')) {
return { external: true, path: args.path }
}
if (isBareSpecifier(args.path)) {
const mappedPath = resolveExampleWorkspaceImport(args.path, imports)
if (!mappedPath && isBareSpecifier(args.path)) {
return {
external: true,
path: args.path.endsWith('.json')
Expand All @@ -140,9 +145,10 @@ function createWorkspacePlugin(files: WorkspaceBuildFiles): esbuild.Plugin {
}

const unresolvedPath =
args.kind === 'entry-point'
mappedPath ??
(args.kind === 'entry-point'
? normalizeExamplePath(args.path)
: resolveRelativePath(args.importer, args.path)
: resolveRelativePath(args.importer, args.path))
const path = resolveWorkspacePath(unresolvedPath, files)

if (!path) {
Expand Down
15 changes: 15 additions & 0 deletions src/utils/example-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ export function normalizeExamplePath(path: string) {
return `/${segments.join('/')}`
}

export function resolveExampleWorkspaceImport(
specifier: string,
imports: Record<string, string> = {},
) {
const key = Object.hasOwn(imports, specifier)
? specifier
: Object.keys(imports)
.filter((key) => key.endsWith('/') && specifier.startsWith(key))
.sort((left, right) => right.length - left.length)[0]
if (key === undefined) return undefined
const target = imports[key]
if (!target?.startsWith('/') || target.startsWith('//')) return undefined
return normalizeExamplePath(target + specifier.slice(key.length))
}

export function serializeExampleWorkspace(workspace: ExampleWorkspace) {
const files = Object.fromEntries(
Object.entries(workspace.files).sort(([left], [right]) =>
Expand Down
43 changes: 39 additions & 4 deletions tests/builder-project-snapshot-storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,14 +389,49 @@ test('fails closed when the legacy snapshot reference lookup fails', async () =>
)
})

test('accepts a large text dataset within the total snapshot budget', () => {
const dataPath = '/packages/charts-demo-data/src/rows.js'
const dataSource = 'export const rows = []\n'.padEnd(854_345, ' ')
const project = createSharedExampleProject({
title: 'Dataset chart',
workspace: createExampleWorkspace({
entry: '/src/index.tsx',
files: {
'/src/index.tsx': "import { rows } from '@tanstack/charts-data/rows'",
[dataPath]: dataSource,
},
imports: {
'@tanstack/charts-data/': '/packages/charts-demo-data/src/',
},
}),
})

const parsed = parseStoredBuilderProjectSnapshot(project)

assert.equal(parsed.workspace.files[dataPath], dataSource)
assert.deepEqual(parsed.workspace.imports, project.workspace.imports)
})

test('rejects projects over the total snapshot budget', () => {
const project = createProject({
'/src/index.tsx': 'a'.repeat(512 * 1024),
'/src/data.js': 'a'.repeat(512 * 1024),
})

assert.throws(
() => parseStoredBuilderProjectSnapshot(project),
/Project snapshot exceeds 1 MiB/,
)
})

test('rejects projects over the per-file byte limit', () => {
const project = createProject({
'/src/index.tsx': 'a'.repeat(512 * 1024 + 1),
'/src/index.tsx': 'a'.repeat(1024 * 1024 + 1),
})

assert.throws(
() => parseStoredBuilderProjectSnapshot(project),
/Builder file exceeds 512 KiB/,
/Builder file exceeds 1 MiB/,
)
})

Expand All @@ -406,7 +441,7 @@ test('applies the per-file byte limit to decoded binary files', () => {
workspace: createExampleWorkspace({
binaryFiles: {
'/public/favicon.ico': encodeExampleBinaryFile(
new Uint8Array(512 * 1024 + 1),
new Uint8Array(1024 * 1024 + 1),
),
},
entry: '/src/index.tsx',
Expand All @@ -416,7 +451,7 @@ test('applies the per-file byte limit to decoded binary files', () => {

assert.throws(
() => parseStoredBuilderProjectSnapshot(project),
/Builder file exceeds 512 KiB: \/public\/favicon\.ico/,
/Builder file exceeds 1 MiB: \/public\/favicon\.ico/,
)
})

Expand Down
2 changes: 1 addition & 1 deletion tests/builder-project-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ test('rejects unsendable project revisions before they enter the outbox', () =>
initialFile: '/index.tsx',
workspace: createExampleWorkspace({
entry: '/index.tsx',
files: { '/index.tsx': 'a'.repeat(512 * 1024 + 1) },
files: { '/index.tsx': 'a'.repeat(1024 * 1024 + 1) },
}),
})

Expand Down
4 changes: 2 additions & 2 deletions tests/charts-catalog-example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('Charts catalog example workspaces', () => {
const imports = definition.workspace.imports

assert.deepEqual(imports, {
'@charts-poc/demo-data/': `https://esm.sh/gh/TanStack/charts@${revision}/packages/charts-demo-data/src/`,
'@charts-poc/demo-data/': '/packages/charts-demo-data/src/',
'@tanstack/charts': 'https://esm.sh/@tanstack/charts@0.10.0',
'@tanstack/charts/': 'https://esm.sh/@tanstack/charts@0.10.0/',
'@tanstack/charts/react':
Expand All @@ -107,7 +107,7 @@ describe('Charts catalog example workspaces', () => {
'https://esm.sh/@tanstack/charts@0.10.0/react/core?external=react',
'@tanstack/charts/react/tooltip':
'https://esm.sh/@tanstack/charts@0.10.0/react/tooltip?external=react,react-dom',
'@tanstack/charts-data/': `https://esm.sh/gh/TanStack/charts@${revision}/packages/charts-demo-data/src/`,
'@tanstack/charts-data/': '/packages/charts-demo-data/src/',
'd3-scale': 'https://esm.sh/d3-scale@4.0.2',
'd3-scale/': 'https://esm.sh/d3-scale@4.0.2/',
react: 'https://esm.sh/react@19.2.3',
Expand Down
Loading
Loading