Skip to content

Commit a563fc3

Browse files
committed
feat: improve view exception rendering
1 parent 55bf936 commit a563fc3

File tree

11 files changed

+166
-103
lines changed

11 files changed

+166
-103
lines changed

packages/debug/src/Stacktrace/Stacktrace.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,43 @@ function: $firstTraceFrame['function'] ?? null,
8282
relativeFile: $rootPath ? to_relative_path($rootPath, $exceptionFile) : $exceptionFile,
8383
);
8484
}
85+
86+
public function prependFrame(Frame $frame): self
87+
{
88+
return new self(
89+
message: $this->message,
90+
exceptionClass: $this->exceptionClass,
91+
frames: [
92+
// we add our frame
93+
new Frame(
94+
line: $frame->line,
95+
class: $frame->class,
96+
function: $frame->function,
97+
type: $frame->type,
98+
isVendor: $frame->isVendor,
99+
snippet: $frame->snippet,
100+
absoluteFile: $frame->absoluteFile,
101+
relativeFile: $frame->relativeFile,
102+
arguments: $frame->arguments,
103+
index: 1,
104+
),
105+
// and shift the frame index by one for each frame
106+
...array_map(fn (Frame $frame) => new Frame(
107+
line: $frame->line,
108+
class: $frame->class,
109+
function: $frame->function,
110+
type: $frame->type,
111+
isVendor: $frame->isVendor,
112+
snippet: $frame->snippet,
113+
absoluteFile: $frame->absoluteFile,
114+
relativeFile: $frame->relativeFile,
115+
arguments: $frame->arguments,
116+
index: $frame->index + 1,
117+
), $this->frames),
118+
],
119+
line: $this->line,
120+
absoluteFile: $this->absoluteFile,
121+
relativeFile: $this->relativeFile,
122+
);
123+
}
85124
}

packages/router/src/Exceptions/DevelopmentException.php

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44

55
use Tempest\Core\Kernel;
66
use Tempest\Core\ProvidesContext;
7+
use Tempest\Debug\Stacktrace\CodeSnippet;
8+
use Tempest\Debug\Stacktrace\Frame;
79
use Tempest\Debug\Stacktrace\Stacktrace;
810
use Tempest\Http\IsResponse;
911
use Tempest\Http\Request;
1012
use Tempest\Http\Response;
1113
use Tempest\Http\Status;
1214
use Tempest\Support\Filesystem;
15+
use Tempest\View\Exceptions\ViewCompilationFailed;
1316
use Tempest\View\GenericView;
17+
use Tempest\View\Renderers\TempestViewRenderer;
1418
use Throwable;
1519

1620
use function Tempest\Mapper\map;
1721
use function Tempest\root_path;
22+
use function Tempest\Support\Path\to_relative_path;
1823

1924
final class DevelopmentException implements Response
2025
{
@@ -26,36 +31,79 @@ public function __construct(Throwable $throwable, Response $response, Request $r
2631

2732
if (! Filesystem\exists(__DIR__ . '/local/dist/main.js')) {
2833
$this->body = 'The development exception interface is not built.';
29-
} else {
30-
$this->body = new GenericView(
31-
path: __DIR__ . '/local/exception.view.php',
32-
data: [
33-
'script' => Filesystem\read_file(__DIR__ . '/local/dist/main.js'),
34-
'css' => Filesystem\read_file(__DIR__ . '/local/dist/style.css'),
35-
'hydration' => map([
36-
'stacktrace' => Stacktrace::fromThrowable($throwable, rootPath: root_path()),
37-
'context' => $throwable instanceof ProvidesContext ? $throwable->context() : [],
38-
'rootPath' => root_path(),
39-
'request' => [
40-
'uri' => $request->uri,
41-
'method' => $request->method,
42-
'headers' => $request->headers->toArray(),
43-
'body' => $request->raw,
44-
],
45-
'response' => [
46-
'status' => $response->status->value,
47-
],
48-
'resources' => [
49-
'memoryPeakUsage' => memory_get_peak_usage(real_usage: true),
50-
'executionTimeMs' => (hrtime(as_number: true) - TEMPEST_START) / 1_000_000,
51-
],
52-
'versions' => [
53-
'php' => PHP_VERSION,
54-
'tempest' => Kernel::VERSION,
55-
],
56-
])->toJson(),
57-
],
58-
);
34+
return;
5935
}
36+
37+
$stacktrace = Stacktrace::fromThrowable($throwable, rootPath: root_path());
38+
39+
if ($throwable instanceof ViewCompilationFailed) {
40+
$stacktrace = $this->enhanceStacktraceForViewCompilation($throwable, $stacktrace);
41+
}
42+
43+
$this->body = new GenericView(
44+
path: __DIR__ . '/local/exception.view.php',
45+
data: [
46+
'script' => Filesystem\read_file(__DIR__ . '/local/dist/main.js'),
47+
'css' => Filesystem\read_file(__DIR__ . '/local/dist/style.css'),
48+
'hydration' => map([
49+
'stacktrace' => $stacktrace,
50+
'context' => $throwable instanceof ProvidesContext ? $throwable->context() : [],
51+
'rootPath' => root_path(),
52+
'request' => [
53+
'uri' => $request->uri,
54+
'method' => $request->method,
55+
'headers' => $request->headers->toArray(),
56+
'body' => $request->raw,
57+
],
58+
'response' => [
59+
'status' => $response->status->value,
60+
],
61+
'resources' => [
62+
'memoryPeakUsage' => memory_get_peak_usage(real_usage: true),
63+
'executionTimeMs' => (hrtime(as_number: true) - TEMPEST_START) / 1_000_000,
64+
],
65+
'versions' => [
66+
'php' => PHP_VERSION,
67+
'tempest' => Kernel::VERSION,
68+
],
69+
])->toJson(),
70+
],
71+
);
72+
}
73+
74+
private function enhanceStacktraceForViewCompilation(ViewCompilationFailed $exception, Stacktrace $stacktrace): Stacktrace
75+
{
76+
$previous = $exception->getPrevious();
77+
78+
if (! $previous) {
79+
return $stacktrace;
80+
}
81+
82+
$lines = explode("\n", $exception->content);
83+
$errorLine = $previous->getLine();
84+
$contextLines = 5;
85+
$startLine = max(1, $errorLine - $contextLines);
86+
$endLine = min(count($lines), $errorLine + $contextLines);
87+
$snippetLines = [];
88+
89+
for ($i = $startLine; $i <= $endLine; $i++) {
90+
$snippetLines[$i] = $lines[$i - 1];
91+
}
92+
93+
return $stacktrace->prependFrame(new Frame(
94+
line: $errorLine,
95+
class: TempestViewRenderer::class,
96+
function: 'renderCompiled',
97+
type: '->',
98+
isVendor: false,
99+
snippet: new CodeSnippet(
100+
lines: $snippetLines,
101+
highlightedLine: $errorLine,
102+
),
103+
absoluteFile: $exception->path,
104+
relativeFile: to_relative_path(root_path(), $exception->path),
105+
arguments: [],
106+
index: 1,
107+
));
60108
}
61109
}

packages/router/src/Exceptions/local/dist/main.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/router/src/Exceptions/local/dist/style.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/router/src/Exceptions/local/src/components/stacktrace/application-frame.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ const $props = defineProps<{
2121
:class="open ? 'bg-(--ui-text-muted)/80' : 'bg-(--ui-text-dimmed)/60'"
2222
/>
2323
</div>
24-
<div class="flex justify-between items-center grow">
24+
<div class="flex justify-between items-center gap-x-4 grow">
2525
<!-- Symbol -->
26-
<symbol-call :frame />
26+
<symbol-call :frame class="grow" />
2727
<!-- File -->
2828
<file-label
29+
class="min-w-[20%] text-right shrink-0"
2930
:relative-file="frame.relativeFile"
3031
:absolute-file="frame.absoluteFile"
3132
:line="frame.line"

packages/router/src/Exceptions/local/src/components/stacktrace/code-snippet.vue

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,28 @@ const lines = computed(() => {
2020
</script>
2121

2222
<template>
23-
<div class="overflow-hidden font-mono">
24-
<div class="overflow-x-auto">
25-
<ul class="flex flex-col">
26-
<li
27-
v-for="line in lines"
28-
:key="line.number"
29-
:class="
30-
[
31-
'flex transition-colors items-center text-sm py-1 group cursor-pointer hover:bg-accented!',
32-
line.isHighlighted && 'bg-error-400/15',
33-
!line.isHighlighted && 'even:bg-elevated dark:even:bg-accented/20',
34-
]
35-
"
36-
@click="openFileInEditor(file, line.number)"
37-
>
38-
<div class="px-3 w-12 text-dimmed text-right select-none shrink-0" v-text="line.number" />
39-
<div class="flex-1 pr-3 min-w-0">
40-
<div v-html="line.highlighted" />
41-
</div>
42-
<div class="opacity-0 group-hover:opacity-100 transition-all group-hover:-translate-x-3">
43-
<u-icon name="tabler:code" class="text-dimmed" />
44-
</div>
45-
</li>
46-
</ul>
47-
</div>
23+
<div class="flex flex-col min-w-0 overflow-x-auto font-mono">
24+
<ul class="flex flex-col w-max min-w-full">
25+
<li
26+
v-for="line in lines"
27+
:key="line.number"
28+
:class="
29+
[
30+
'flex transition-colors items-center text-sm py-1 group cursor-pointer hover:bg-accented! grow w-full',
31+
line.isHighlighted && 'bg-error-400/15',
32+
!line.isHighlighted && 'even:bg-elevated dark:even:bg-accented/20',
33+
]
34+
"
35+
@click="openFileInEditor(file, line.number)"
36+
>
37+
<div class="px-3 w-12 text-dimmed text-right select-none shrink-0" v-text="line.number" />
38+
<div class="flex-1 pr-3 min-w-0">
39+
<div v-html="line.highlighted" />
40+
</div>
41+
<div class="opacity-0 group-hover:opacity-100 transition-all group-hover:-translate-x-3">
42+
<u-icon name="tabler:code" class="text-dimmed" />
43+
</div>
44+
</li>
45+
</ul>
4846
</div>
4947
</template>

packages/router/src/Exceptions/local/src/components/stacktrace/symbol-call.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const parts = computed<HighlightedPart[]>(() => {
4747
// Highlight each argument individually using the grammar state
4848
$props.frame.arguments.forEach((argument, index) => {
4949
if (index > 0) {
50-
result.push({ html: '<span style="color: var(--code-foreground)">,</span>' })
50+
result.push({ html: '<span style="color: var(--code-foreground)">, </span>' })
5151
if ($props.formatted) {
5252
result.push({ html: '<br /> ' })
5353
}

packages/router/src/Exceptions/local/src/sections/context.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const json = computed(() => JSON.stringify($props.context ?? '', null, 2))
1313
<card title="Exception context" icon="tabler:info-circle">
1414
<div
1515
v-if="Object.values(context ?? {}).length > 0"
16-
class="p-2"
16+
class="p-2 overflow-auto"
1717
v-html="highlight(json, 'json')"
1818
/>
1919
<div v-else class="flex justify-center items-center p-8 pt-4 font-mono uppercase">

packages/router/src/Exceptions/local/src/sections/summary.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ function toFileSize(
123123
<!-- Error -->
124124
<span
125125
v-if="exception.stacktrace.message"
126-
class="font-light text-xl"
126+
class="font-light text-xl whitespace-pre-line"
127127
v-text="exception.stacktrace.message"
128128
/>
129129
</div>

packages/router/src/Exceptions/local/src/settings/settings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const overlay = useOverlay()
77
export const settingsDialog = overlay.create(Dialog)
88

99
export const schema = type({
10-
editor: type('"vscode" | "phpstorm" | "zed" | "custom"').optional(),
11-
openEditorTemplate: type('string').optional(),
10+
editor: type('"vscode" | "phpstorm" | "zed" | "custom" | undefined').optional(),
11+
openEditorTemplate: type('string | undefined').optional(),
1212
})
1313

1414
export type Editor = typeof schema.infer['editor']

0 commit comments

Comments
 (0)