Skip to content

Commit 7b3490e

Browse files
authored
Merge pull request #75 from devallibus/fix/tsl-playground-e2e-v3
fix(playground): report structured TSL errors
2 parents a21915b + e467b2d commit 7b3490e

7 files changed

Lines changed: 145 additions & 26 deletions

File tree

apps/web/src/components/TslPreviewCanvas.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type {
33
TslPreviewModuleResult,
44
TslPreviewModuleRuntime,
55
} from '../../../../packages/schema/src/tsl-preview-module.ts'
6+
import { createPlainErrorReport, createTslErrorReport, TslPreviewError } from '../lib/tsl-error-reporting'
7+
import type { PlaygroundErrorReport } from '../lib/playground-types'
68

79
type THREE = typeof import('three/webgpu')
810
type TSL = typeof import('three/tsl')
@@ -12,7 +14,7 @@ type TslPreviewCanvasProps = {
1214
pipeline: string
1315
fallbackSvg?: string | null
1416
uniformOverrides?: Record<string, number | number[] | boolean>
15-
onError?: (errors: string[]) => void
17+
onError?: (report: PlaygroundErrorReport) => void
1618
onScreenshotReady?: (base64: string) => void
1719
}
1820

@@ -55,14 +57,14 @@ export default function TslPreviewCanvas(props: TslPreviewCanvasProps) {
5557
const [loading, setLoading] = createSignal(true)
5658
const [error, setError] = createSignal('')
5759

58-
function setPreviewError(message: string) {
59-
setError(message)
60-
props.onError?.([message])
60+
function setPreviewError(report: PlaygroundErrorReport) {
61+
setError(report.errors[0] ?? report.structuredErrors[0]?.message ?? 'Preview unavailable')
62+
props.onError?.(report)
6163
}
6264

6365
function clearPreviewError() {
6466
setError('')
65-
props.onError?.([])
67+
props.onError?.(createPlainErrorReport([]))
6668
}
6769

6870
function captureScreenshot() {
@@ -110,7 +112,10 @@ export default function TslPreviewCanvas(props: TslPreviewCanvasProps) {
110112

111113
const module = (await import(/* @vite-ignore */ currentModuleUrl)) as PreviewModuleNamespace
112114
if (typeof module.createPreview !== 'function') {
113-
throw new Error('TSL preview modules must export createPreview(runtime).')
115+
throw new TslPreviewError(
116+
'tsl-material-build',
117+
'TSL preview modules must export createPreview(runtime).',
118+
)
114119
}
115120

116121
const nextPreview = module.createPreview({
@@ -123,7 +128,10 @@ export default function TslPreviewCanvas(props: TslPreviewCanvasProps) {
123128
})
124129

125130
if (!nextPreview?.material || typeof nextPreview.material !== 'object') {
126-
throw new Error('createPreview(runtime) must return an object with a material.')
131+
throw new TslPreviewError(
132+
'tsl-material-build',
133+
'createPreview(runtime) must return an object with a material.',
134+
)
127135
}
128136

129137
previewInstance = nextPreview as PreviewInstance
@@ -144,9 +152,7 @@ export default function TslPreviewCanvas(props: TslPreviewCanvasProps) {
144152
} catch (previewError) {
145153
disposePreviewMesh()
146154
setPreviewError(
147-
previewError instanceof Error
148-
? previewError.message
149-
: 'Failed to build the TSL preview module.',
155+
createTslErrorReport(previewError, 'tsl-runtime', 'Failed to build the TSL preview module.'),
150156
)
151157
} finally {
152158
setLoading(false)
@@ -155,7 +161,13 @@ export default function TslPreviewCanvas(props: TslPreviewCanvasProps) {
155161

156162
onMount(async () => {
157163
if (!('gpu' in navigator)) {
158-
setPreviewError('WebGPU is not available in this browser.')
164+
setPreviewError(
165+
createTslErrorReport(
166+
new TslPreviewError('tsl-runtime', 'WebGPU is not available in this browser.'),
167+
'tsl-runtime',
168+
'WebGPU is not available in this browser.',
169+
),
170+
)
159171
setLoading(false)
160172
return
161173
}
@@ -236,9 +248,7 @@ export default function TslPreviewCanvas(props: TslPreviewCanvasProps) {
236248
await renderPreview(props.previewModule)
237249
} catch (previewError) {
238250
setPreviewError(
239-
previewError instanceof Error
240-
? previewError.message
241-
: 'Failed to initialize the TSL preview runtime.',
251+
createTslErrorReport(previewError, 'tsl-runtime', 'Failed to initialize the TSL preview runtime.'),
242252
)
243253
setLoading(false)
244254
}

apps/web/src/components/playground/PlaygroundCanvas.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
22
import { buildTslPreviewModule } from '../../../../../packages/schema/src/tsl-preview-module.ts'
33
import { collectShaderDiagnostics, diagnosticsToMessages } from '../../lib/webgl-shader-errors'
4+
import { createPlainErrorReport, createTslErrorReport } from '../../lib/tsl-error-reporting'
5+
import type { PlaygroundErrorReport } from '../../lib/playground-types'
46
import TslPreviewCanvas from '../TslPreviewCanvas'
57

68
type THREE = typeof import('three')
@@ -11,7 +13,7 @@ type PlaygroundCanvasProps = {
1113
tslSource?: string
1214
pipeline: string
1315
language: 'glsl' | 'tsl'
14-
onError: (errors: string[]) => void
16+
onError: (report: PlaygroundErrorReport) => void
1517
onScreenshotReady: (base64: string) => void
1618
}
1719

@@ -29,7 +31,7 @@ export default function PlaygroundCanvas(props: PlaygroundCanvasProps) {
2931
try {
3032
return buildTslPreviewModule(props.tslSource)
3133
} catch (error) {
32-
props.onError([error instanceof Error ? error.message : 'Failed to build TSL preview module'])
34+
props.onError(createTslErrorReport(error, 'tsl-parse', 'Failed to build TSL preview module.'))
3335
return ''
3436
}
3537
})
@@ -172,7 +174,7 @@ export default function PlaygroundCanvas(props: PlaygroundCanvasProps) {
172174
uniforms: shaderUniforms,
173175
})
174176
} catch (e) {
175-
props.onError([e instanceof Error ? e.message : 'Shader compilation failed'])
177+
props.onError(createPlainErrorReport([e instanceof Error ? e.message : 'Shader compilation failed']))
176178
return
177179
}
178180

@@ -210,21 +212,21 @@ export default function PlaygroundCanvas(props: PlaygroundCanvasProps) {
210212
? renderError.message
211213
: 'Shader compilation failed'
212214

213-
props.onError(
215+
props.onError(createPlainErrorReport(
214216
shaderDiagnostics.length > 0 ? diagnosticsToMessages(shaderDiagnostics) : [fallbackMessage],
215-
)
217+
))
216218
return
217219
} finally {
218220
renderer.debug.onShaderError = previousShaderError
219221
}
220222

221223
if (shaderDiagnostics.length > 0) {
222-
props.onError(diagnosticsToMessages(shaderDiagnostics))
224+
props.onError(createPlainErrorReport(diagnosticsToMessages(shaderDiagnostics)))
223225
return
224226
}
225227

226228
// No errors — clear any previous errors and capture screenshot
227-
props.onError([])
229+
props.onError(createPlainErrorReport([]))
228230
captureScreenshot()
229231
}
230232

apps/web/src/components/playground/PlaygroundLayout.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createSignal, onCleanup, onMount, Show, lazy } from 'solid-js'
2-
import type { PlaygroundSession } from '../../lib/playground-types'
2+
import type { PlaygroundErrorReport, PlaygroundSession } from '../../lib/playground-types'
33

44
const PlaygroundCanvas = lazy(() => import('./PlaygroundCanvas'))
55
const PlaygroundEditor = lazy(() => import('./PlaygroundEditor'))
@@ -93,13 +93,13 @@ export default function PlaygroundLayout(props: PlaygroundLayoutProps) {
9393
}
9494
}
9595

96-
function handleErrors(errs: string[]) {
97-
setErrors(errs)
96+
function handleErrors(report: PlaygroundErrorReport) {
97+
setErrors(report.errors)
9898
// Post errors to server so MCP can query them
9999
fetch(`/api/playground/${props.session.id}/errors`, {
100100
method: 'POST',
101101
headers: { 'Content-Type': 'application/json' },
102-
body: JSON.stringify({ errors: errs }),
102+
body: JSON.stringify(report),
103103
}).catch(() => {})
104104
}
105105

apps/web/src/lib/playground-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export type PlaygroundError =
2929
| { kind: 'tsl-runtime'; message: string }
3030
| { kind: 'tsl-material-build'; message: string }
3131

32+
export type PlaygroundErrorReport = {
33+
errors: string[]
34+
structuredErrors: PlaygroundError[]
35+
}
36+
3237
// ---------------------------------------------------------------------------
3338
// Session types — discriminated union on language
3439
// ---------------------------------------------------------------------------
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import assert from 'node:assert/strict'
2+
import {
3+
createPlainErrorReport,
4+
createTslErrorReport,
5+
TslPreviewError,
6+
} from './tsl-error-reporting.ts'
7+
8+
function runTest(name: string, callback: () => void) {
9+
callback()
10+
console.log(`ok ${name}`)
11+
}
12+
13+
runTest('createPlainErrorReport keeps structured errors empty', () => {
14+
assert.deepEqual(createPlainErrorReport(['plain error']), {
15+
errors: ['plain error'],
16+
structuredErrors: [],
17+
})
18+
})
19+
20+
runTest('createTslErrorReport preserves explicit preview error kinds', () => {
21+
const report = createTslErrorReport(
22+
new TslPreviewError('tsl-material-build', 'createPreview(runtime) must return a material.'),
23+
'tsl-runtime',
24+
'fallback',
25+
)
26+
27+
assert.deepEqual(report, {
28+
errors: ['createPreview(runtime) must return a material.'],
29+
structuredErrors: [{
30+
kind: 'tsl-material-build',
31+
message: 'createPreview(runtime) must return a material.',
32+
}],
33+
})
34+
})
35+
36+
runTest('createTslErrorReport maps SyntaxError to tsl-parse', () => {
37+
const report = createTslErrorReport(
38+
new SyntaxError('Unexpected token'),
39+
'tsl-runtime',
40+
'fallback',
41+
)
42+
43+
assert.deepEqual(report, {
44+
errors: ['Unexpected token'],
45+
structuredErrors: [{
46+
kind: 'tsl-parse',
47+
message: 'Unexpected token',
48+
}],
49+
})
50+
})
51+
52+
console.log('tsl-error-reporting tests passed')
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { PlaygroundErrorReport, PlaygroundError } from './playground-types'
2+
3+
type TslErrorKind = Extract<PlaygroundError['kind'], 'tsl-parse' | 'tsl-runtime' | 'tsl-material-build'>
4+
5+
export class TslPreviewError extends Error {
6+
kind: TslErrorKind
7+
8+
constructor(kind: TslErrorKind, message: string) {
9+
super(message)
10+
this.kind = kind
11+
this.name = 'TslPreviewError'
12+
}
13+
}
14+
15+
function getMessage(error: unknown, fallbackMessage: string): string {
16+
if (error instanceof Error && error.message.trim()) {
17+
return error.message.trim()
18+
}
19+
20+
if (typeof error === 'string' && error.trim()) {
21+
return error.trim()
22+
}
23+
24+
return fallbackMessage
25+
}
26+
27+
export function createPlainErrorReport(errors: string[]): PlaygroundErrorReport {
28+
return {
29+
errors,
30+
structuredErrors: [],
31+
}
32+
}
33+
34+
export function createTslErrorReport(
35+
error: unknown,
36+
fallbackKind: TslErrorKind,
37+
fallbackMessage: string,
38+
): PlaygroundErrorReport {
39+
const message = getMessage(error, fallbackMessage)
40+
const kind = error instanceof TslPreviewError
41+
? error.kind
42+
: error instanceof SyntaxError
43+
? 'tsl-parse'
44+
: fallbackKind
45+
46+
return {
47+
errors: [message],
48+
structuredErrors: [{ kind, message }],
49+
}
50+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"check": "bun run test && bun run typecheck && bun run validate:shaders && bun run build:web",
1414
"dev:web": "cd apps/web && bun run dev",
1515
"test": "node --experimental-strip-types packages/schema/src/index.test.ts && bun run test:cli && bun run test:mcp && bun run test:registry && bun run test:web",
16-
"test:web": "node --experimental-strip-types apps/web/src/lib/server/shaders.test.ts && node --experimental-strip-types apps/web/src/lib/server/reviews-db.test.node.ts && node --experimental-strip-types apps/web/src/lib/server/playground-db.test.node.ts && node --experimental-strip-types apps/web/src/lib/webgl-shader-errors.test.ts",
16+
"test:web": "node --experimental-strip-types apps/web/src/lib/server/shaders.test.ts && node --experimental-strip-types apps/web/src/lib/server/reviews-db.test.node.ts && node --experimental-strip-types apps/web/src/lib/server/playground-db.test.node.ts && node --experimental-strip-types apps/web/src/lib/webgl-shader-errors.test.ts && node --experimental-strip-types apps/web/src/lib/tsl-error-reporting.test.ts",
1717
"test:cli": "node --experimental-strip-types packages/cli/src/registry-types.test.ts && node --experimental-strip-types packages/cli/src/commands/search.test.ts && node --experimental-strip-types packages/cli/src/commands/add.test.ts && node --experimental-strip-types packages/cli/src/lib/resolve-source.test.ts && node --experimental-strip-types packages/cli/src/lib/build-manifest.test.ts && node --experimental-strip-types packages/cli/src/lib/github-pr.test.ts && node --experimental-strip-types packages/cli/src/commands/submit.test.ts",
1818
"test:mcp": "node --experimental-strip-types packages/mcp/src/handlers.test.ts && node --experimental-strip-types packages/mcp/src/index.test.ts",
1919
"test:registry": "node --experimental-strip-types scripts/build-registry.test.ts",

0 commit comments

Comments
 (0)