From 79ef87f5913bf8cb62205662a7bb5da40fdef4fd Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 11 Oct 2026 21:15:18 -0400 Subject: [PATCH] Fix simplified chart SSR stack overflows - Avoid snippet/prop name collisions for marks and Layer children - Add SSR coverage for default and custom chart marks --- .changeset/simplified-charts-ssr-recursion.md | 5 ++ .../charts/ArcChart/ArcChart.base.svelte | 6 +-- .../charts/AreaChart/AreaChart.base.svelte | 6 +-- .../charts/BarChart/BarChart.base.svelte | 6 +-- .../charts/LineChart/LineChart.base.svelte | 6 +-- .../charts/PieChart/PieChart.base.svelte | 6 +-- .../ScatterChart/ScatterChart.base.svelte | 6 +-- .../__fixtures__/CustomMarksChart.svelte | 17 +++++++ .../charts/simplifiedCharts.ssr.test.ts | 49 +++++++++++++++++++ .../src/lib/components/layers/Layer.svelte | 8 +-- 10 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 .changeset/simplified-charts-ssr-recursion.md create mode 100644 packages/layerchart/src/lib/components/charts/__fixtures__/CustomMarksChart.svelte create mode 100644 packages/layerchart/src/lib/components/charts/simplifiedCharts.ssr.test.ts diff --git a/.changeset/simplified-charts-ssr-recursion.md b/.changeset/simplified-charts-ssr-recursion.md new file mode 100644 index 000000000..0c6e5d4e1 --- /dev/null +++ b/.changeset/simplified-charts-ssr-recursion.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(AreaChart|BarChart|LineChart|ScatterChart|PieChart|ArcChart|Layer): Render server-side with `ssr` instead of overflowing the stack (#940) diff --git a/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.base.svelte b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.base.svelte index de44f102c..98487b57c 100644 --- a/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.base.svelte +++ b/packages/layerchart/src/lib/components/charts/ArcChart/ArcChart.base.svelte @@ -54,7 +54,7 @@ props = {}, profile = false, tooltipContext = true, - marks, + marks: marksProp, tooltip: tooltipProp, arc, labels = false, @@ -242,8 +242,8 @@ }} > {#snippet marks(snippetProps: any)} - {#if typeof marks === 'function'} - {@render marks(snippetProps)} + {#if typeof marksProp === 'function'} + {@render marksProp(snippetProps)} {:else} {#each series as s, i (s.key)} diff --git a/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.base.svelte b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.base.svelte index 314b4a1c6..7df5eb004 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.base.svelte +++ b/packages/layerchart/src/lib/components/charts/AreaChart/AreaChart.base.svelte @@ -34,7 +34,7 @@ onPointClick, props = {}, profile = false, - marks, + marks: marksProp, tooltip: tooltipProp, ref = $bindable(), context = $bindable(), @@ -104,8 +104,8 @@ }} > {#snippet marks({ context, facet }: { context: any; facet: any })} - {#if typeof marks === 'function'} - {@render marks({ context, facet })} + {#if typeof marksProp === 'function'} + {@render marksProp({ context, facet })} {:else} {#each context.series.visibleSeries as s (s.key)} {#snippet marks({ context, facet }: { context: any; facet: any })} - {#if typeof marks === 'function'} - {@render marks({ context, facet })} + {#if typeof marksProp === 'function'} + {@render marksProp({ context, facet })} {:else} {#each context.series.visibleSeries as s, i (s.key)} {#snippet marks({ context, facet }: { context: any; facet: any })} - {#if typeof marks === 'function'} - {@render marks({ context, facet })} + {#if typeof marksProp === 'function'} + {@render marksProp({ context, facet })} {:else} {#each context.series.visibleSeries as s, i (s.key)} diff --git a/packages/layerchart/src/lib/components/charts/PieChart/PieChart.base.svelte b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.base.svelte index 84c1521e3..bf1e73a09 100644 --- a/packages/layerchart/src/lib/components/charts/PieChart/PieChart.base.svelte +++ b/packages/layerchart/src/lib/components/charts/PieChart/PieChart.base.svelte @@ -56,7 +56,7 @@ props = {}, profile = false, tooltipContext = true, - marks, + marks: marksProp, tooltip: tooltipProp, pie, arc, @@ -226,8 +226,8 @@ }} > {#snippet marks(snippetProps: any)} - {#if typeof marks === 'function'} - {@render marks(snippetProps)} + {#if typeof marksProp === 'function'} + {@render marksProp(snippetProps)} {:else} diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.base.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.base.svelte index d3a27a3e0..f63db8fbb 100644 --- a/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.base.svelte +++ b/packages/layerchart/src/lib/components/charts/ScatterChart/ScatterChart.base.svelte @@ -34,7 +34,7 @@ props = {}, profile = false, tooltipContext = true, - marks, + marks: marksProp, tooltip: tooltipProp, ref = $bindable(), context = $bindable(), @@ -88,8 +88,8 @@ {props} > {#snippet marks({ context, facet }: { context: any; facet: any })} - {#if typeof marks === 'function'} - {@render marks({ context, facet })} + {#if typeof marksProp === 'function'} + {@render marksProp({ context, facet })} {:else} {#each context.series.visibleSeries as s, i (s.key)} diff --git a/packages/layerchart/src/lib/components/charts/__fixtures__/CustomMarksChart.svelte b/packages/layerchart/src/lib/components/charts/__fixtures__/CustomMarksChart.svelte new file mode 100644 index 000000000..a5ba0274a --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/__fixtures__/CustomMarksChart.svelte @@ -0,0 +1,17 @@ + + + + {#snippet marks()} + + {/snippet} + diff --git a/packages/layerchart/src/lib/components/charts/simplifiedCharts.ssr.test.ts b/packages/layerchart/src/lib/components/charts/simplifiedCharts.ssr.test.ts new file mode 100644 index 000000000..abb5a70cf --- /dev/null +++ b/packages/layerchart/src/lib/components/charts/simplifiedCharts.ssr.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { render } from 'svelte/server'; + +import AreaChart from './AreaChart/AreaChart.svelte'; +import BarChart from './BarChart/BarChart.svelte'; +import LineChart from './LineChart/LineChart.svelte'; +import ScatterChart from './ScatterChart/ScatterChart.svelte'; +import PieChart from './PieChart/PieChart.svelte'; +import ArcChart from './ArcChart/ArcChart.svelte'; +import CustomMarksChart from './__fixtures__/CustomMarksChart.svelte'; + +const data = [ + { month: 'Jan', value: 1 }, + { month: 'Feb', value: 3 }, + { month: 'Mar', value: 2 }, +]; + +const cartesian = { x: 'month', y: 'value' }; +const radial = { key: 'month', value: 'value' }; + +const cases = [ + ['AreaChart', AreaChart, cartesian], + ['BarChart', BarChart, cartesian], + ['LineChart', LineChart, cartesian], + ['ScatterChart', ScatterChart, cartesian], + ['PieChart', PieChart, radial], + ['ArcChart', ArcChart, radial], +] as const; + +// The `marks` prop shares its name with the `{#snippet marks}` each chart passes to `Chart`. +// The server compiler resolves the name inside the snippet to the snippet itself, so it recursed +// until the stack overflowed (#940) +describe('simplified charts with `ssr`', () => { + for (const [name, component, props] of cases) { + it(`${name} renders default marks`, () => { + const { body } = render(component as any, { + props: { ssr: true, data, width: 400, height: 200, ...props }, + }); + expect(body).toContain(' { + const { body } = render(CustomMarksChart, { + props: { component: component as any, ssr: true, data, width: 400, height: 200, ...props }, + }); + expect(body).toContain('class="custom-marks"'); + }); + } +}); diff --git a/packages/layerchart/src/lib/components/layers/Layer.svelte b/packages/layerchart/src/lib/components/layers/Layer.svelte index d05e23f3f..59e316617 100644 --- a/packages/layerchart/src/lib/components/layers/Layer.svelte +++ b/packages/layerchart/src/lib/components/layers/Layer.svelte @@ -35,7 +35,7 @@ import Frame from '../Frame/Frame.svelte'; import { getSettings } from '$lib/contexts/settings.js'; - let { type, children, ...restProps }: LayerProps = $props(); + let { type, children: childrenProp, ...restProps }: LayerProps = $props(); let settings = getSettings(); let layer = $derived(type ?? settings.layer); @@ -49,7 +49,7 @@ {/if} - {@render children?.(props)} + {@render (childrenProp as CanvasProps['children'])?.(props)} {/snippet} {:else if layer === 'svg'} @@ -60,7 +60,7 @@ {/if} - {@render children?.(props)} + {@render (childrenProp as SVGProps['children'])?.(props)} {/snippet} {:else if layer === 'html'} @@ -71,7 +71,7 @@ {/if} - {@render children?.(props)} + {@render (childrenProp as HTMLProps['children'])?.(props)} {/snippet} {/if}