Skip to content

fix(playground): report structured TSL errors#75

Merged
devallibus merged 1 commit intodevelopmentfrom
fix/tsl-playground-e2e-v3
Mar 10, 2026
Merged

fix(playground): report structured TSL errors#75
devallibus merged 1 commit intodevelopmentfrom
fix/tsl-playground-e2e-v3

Conversation

@devallibus
Copy link
Owner

Summary

  • add a shared playground error-report contract so preview callbacks can send both plain errors and structured errors
  • classify TSL failures as tsl-parse, tsl-runtime, or tsl-material-build and persist them through the playground /errors endpoint
  • keep the working TSL happy path intact while making MCP-visible failure reports structured and machine-readable

Verification

  • bun run test:web
  • bun run build:web
  • local TSL happy path returned a screenshot with a browser-connected session
  • local broken TSL update (three/not-real import) returned:
    • compilationErrors: ["Unsupported TSL import source: three/not-real"]
    • structuredErrors: [{ kind: "tsl-parse", message: "Unsupported TSL import source: three/not-real" }]
    • response time ~59ms with a connected browser client

Addresses #70.

Copy link
Owner Author

@devallibus devallibus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The shared PlaygroundErrorReport contract is the right abstraction — it unifies the error channel so TSL failures propagate as machine-readable structured errors alongside plain strings, without breaking the existing GLSL flow.

The tsl-error-reporting module is clean: TslPreviewError for known failures, SyntaxErrortsl-parse heuristic for module parse errors, and a fallback for everything else. All call sites updated consistently.

Four inline comments left — one nit about a redundant constructor in the WebGPU check, one noting the PlaygroundErrorReport/ErrorReportPayload type duplication, one optional missing test, and one future enhancement idea for GLSL structured errors. None block merge.

Comment on lines 163 to +168
if (!('gpu' in navigator)) {
setPreviewError('WebGPU is not available in this browser.')
setPreviewError(
createTslErrorReport(
new TslPreviewError('tsl-runtime', 'WebGPU is not available in this browser.'),
'tsl-runtime',
'WebGPU is not available in this browser.',
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit roundabout — you construct a TslPreviewError just so createTslErrorReport can unwrap it back into the same kind and message. The message string appears three times.

Since the kind and message are fully known here, you could skip the round-trip:

setPreviewError({
  errors: ['WebGPU is not available in this browser.'],
  structuredErrors: [{ kind: 'tsl-runtime', message: 'WebGPU is not available in this browser.' }],
})

Or if you prefer keeping the helper for consistency, at least the TslPreviewError wrapper carries the info without needing the redundant string args:

const err = new TslPreviewError('tsl-runtime', 'WebGPU is not available in this browser.')
setPreviewError(createTslErrorReport(err, 'tsl-runtime', err.message))

nit — works correctly as-is

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 19c2091. I added a direct createKnownTslErrorReport() helper and switched the WebGPU gate to use it, so there is no redundant TslPreviewError round-trip and the message only lives in one place.

Comment on lines +34 to +49
export function createTslErrorReport(
error: unknown,
fallbackKind: TslErrorKind,
fallbackMessage: string,
): PlaygroundErrorReport {
const message = getMessage(error, fallbackMessage)
const kind = error instanceof TslPreviewError
? error.kind
: error instanceof SyntaxError
? 'tsl-parse'
: fallbackKind

return {
errors: [message],
structuredErrors: [{ kind, message }],
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean classification chain. The TslPreviewErrorSyntaxError → fallback priority is the right heuristic for catching TSL module errors at different stages (parse, runtime, material build).

One thing to be aware of: PlaygroundErrorReport here and ErrorReportPayload in playground-db.ts are structurally identical types with different names. They're compatible via structural typing so nothing breaks, but if you ever want to consolidate, PlaygroundErrorReport from playground-types.ts is the natural single source since it's already in the shared types file. The server could import it instead of defining ErrorReportPayload separately. Not blocking — just noting the duplication.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 19c2091. playground-db.ts now uses the shared PlaygroundErrorReport type directly instead of keeping a separate ErrorReportPayload alias.

message: 'Unexpected token',
}],
})
})
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good coverage of the TslPreviewError and SyntaxError branches. One optional gap: a plain Error (not TslPreviewError, not SyntaxError) should use fallbackKind. Easy to add:

runTest('createTslErrorReport uses fallbackKind for generic errors', () => {
  const report = createTslErrorReport(
    new Error('something broke'),
    'tsl-runtime',
    'fallback',
  )
  assert.deepEqual(report, {
    errors: ['something broke'],
    structuredErrors: [{ kind: 'tsl-runtime', message: 'something broke' }],
  })
})

nit / optional

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 19c2091. I added the plain Error coverage so the allbackKind path is now tested directly.

} finally {
renderer.debug.onShaderError = previousShaderError
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth noting for a future enhancement: GLSL errors go through createPlainErrorReport (no structured errors), even though webgl-shader-errors.ts already produces ShaderDiagnostic objects with kind: 'glsl-compile' | 'glsl-link' that map directly to PlaygroundError. If you wanted full structured coverage for both GLSL and TSL, you could map ShaderDiagnostic[]PlaygroundError[] here instead of flattening to strings. Not in scope for this PR though.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept out of scope for this PR, but I filed the follow-up here: #76. That issue tracks mapping ShaderDiagnostic[] through PlaygroundErrorReport so GLSL gets the same structured coverage as TSL.

@devallibus devallibus merged commit 7b3490e into development Mar 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant