Skip to content

Commit 789f91b

Browse files
kirjsalxhub
authored andcommitted
feat(docs-infra): forward preview error locations
Intercept the WebContainer preview overlay open-in-editor requests and relay them to the editor via postMessage so errors open the matching file.
1 parent 21b995f commit 789f91b

File tree

4 files changed

+99
-0
lines changed

4 files changed

+99
-0
lines changed

adev/shared-docs/testing/testing-helper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ export class FakeWebContainer extends WebContainer {
111111
override teardown() {}
112112

113113
override fs: FakeFileSystemAPI = new FakeFileSystemAPI();
114+
115+
override async setPreviewScript(script: string): Promise<void> {}
114116
}
115117

116118
class FakeFileSystemAPI implements FileSystemAPI {

adev/src/app/editor/code-editor/code-editor.component.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,68 @@ export class CodeEditor {
120120

121121
this.listenToTabChange();
122122
this.setSelectedTabOnTutorialChange();
123+
this.listenToFileOpenRequests();
123124
});
124125

125126
cleanupFn(() => this.codeMirrorEditor.disable());
126127
});
127128
}
128129

130+
private listenToFileOpenRequests() {
131+
// Handler for opening files at specific locations
132+
const openFile = (file: string, line: number, character: number) => {
133+
// Normalize the file path - Vite uses /app/... but editor uses src/app/...
134+
let normalizedPath = file;
135+
if (file.startsWith('/')) {
136+
// Remove leading slash and prepend 'src'
137+
normalizedPath = 'src' + file;
138+
}
139+
140+
// Find the file in the files list
141+
const targetFile = this.files().find((f) => f.filename === normalizedPath);
142+
if (targetFile) {
143+
// Switch to the file's tab
144+
const fileIndex = this.files().indexOf(targetFile);
145+
this.matTabGroup().selectedIndex = fileIndex;
146+
147+
// Explicitly change the current file in the editor
148+
this.codeMirrorEditor.changeCurrentFile(targetFile.filename);
149+
150+
// Wait for the tab to switch and file to load, then scroll to the line
151+
setTimeout(() => {
152+
this.codeMirrorEditor.scrollToLine(line - 1, character); // Convert to 0-based
153+
}, 200);
154+
} else {
155+
// console.warn('File not found in editor:', normalizedPath);
156+
}
157+
};
158+
159+
// Listen for CustomEvent (backward compatibility)
160+
const handleCustomEvent = (event: Event) => {
161+
const customEvent = event as CustomEvent<{file: string; line: number; character: number}>;
162+
const {file, line, character} = customEvent.detail;
163+
openFile(file, line, character);
164+
};
165+
166+
// Listen for postMessage from preview iframe (Vite error overlay)
167+
const handlePostMessage = (event: MessageEvent) => {
168+
// Check if this is an openFileAtLocation message
169+
if (event.data?.type === 'openFileAtLocation') {
170+
const {file, line, character} = event.data;
171+
openFile(file, line, character);
172+
}
173+
};
174+
175+
window.addEventListener('openFileAtLocation', handleCustomEvent);
176+
window.addEventListener('message', handlePostMessage);
177+
178+
// Cleanup listeners on destroy
179+
this.destroyRef.onDestroy(() => {
180+
window.removeEventListener('openFileAtLocation', handleCustomEvent);
181+
window.removeEventListener('message', handlePostMessage);
182+
});
183+
}
184+
129185
protected openCurrentSolutionInFirebaseStudio(): void {
130186
this.firebaseStudioLauncher.openCurrentSolutionInFirebaseStudio();
131187
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {WebContainer} from '@webcontainer/api';
10+
11+
function errorFilenameHandler() {
12+
const originalFetch = window.fetch;
13+
window.fetch = async (input, init) => {
14+
const url = input.toString();
15+
if (url.includes('__open-in-editor')) {
16+
const params = new URLSearchParams(url.split('?')[1]);
17+
const file = params.get('file');
18+
if (file) {
19+
const [filepath, line, column] = file.split(':');
20+
window.parent.postMessage(
21+
{
22+
type: 'openFileAtLocation',
23+
file: filepath,
24+
line: parseInt(line, 10),
25+
character: parseInt(column, 10),
26+
},
27+
'*',
28+
);
29+
}
30+
return new Response(null, {status: 200});
31+
}
32+
return originalFetch(input, init);
33+
};
34+
}
35+
36+
export async function setupErrorFilenameHandler(webContainer: WebContainer): Promise<void> {
37+
await webContainer.setPreviewScript(`(${errorFilenameHandler.toString()})()`);
38+
}

adev/src/app/editor/node-runtime-sandbox.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {DestroyRef, effect, inject, Injectable, signal} from '@angular/core';
1010
import {FileSystemTree, WebContainer, WebContainerProcess} from '@webcontainer/api';
11+
import {setupErrorFilenameHandler} from './error-filename-handler';
1112
import {BehaviorSubject, filter, map, Subject} from 'rxjs';
1213

1314
import {type FileAndContent, TutorialType, checkFilesInDirectory} from '@angular/docs';
@@ -398,6 +399,8 @@ export class NodeRuntimeSandbox {
398399

399400
this.setErrorState(message, ErrorType.UNKNOWN);
400401
});
402+
403+
await setupErrorFilenameHandler(webContainer);
401404
}
402405

403406
private checkForOutOfMemoryError(message: string): boolean {

0 commit comments

Comments
 (0)