Skip to content

Commit

Permalink
feat: display the source of client error
Browse files Browse the repository at this point in the history
  • Loading branch information
harrytothemoon authored Aug 19, 2022
1 parent 8dae00b commit c42ab54
Show file tree
Hide file tree
Showing 13 changed files with 347 additions and 292 deletions.
6 changes: 3 additions & 3 deletions packages/error-overlay/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import iframeScript from 'iframeScript';
import { parse } from 'stacktrace-parser';

import * as errorTypeHandler from './view/errorTypeHandler';
import {
Expand All @@ -10,6 +9,7 @@ import {
TYPE_REFRESH,
STACK_TRACE_LIMIT
} from './constants';
import { parseStack } from './view/helpers/parseStack';

let isRegistered = false;
let stackTraceLimit: number | undefined = undefined;
Expand Down Expand Up @@ -45,7 +45,7 @@ function onUnhandledError(ev: ErrorEvent) {
errorType = {
type: TYPE_UNHANDLED_ERROR,
reason: error,
frames: parse(error.stack)
frames: parseStack(error.stack)
};
update();
}
Expand All @@ -64,7 +64,7 @@ function onUnhandledRejection(ev: PromiseRejectionEvent) {
errorType = {
type: TYPE_UNHANDLED_REJECTION,
reason: reason,
frames: parse(reason.stack)
frames: parseStack(reason.stack)
};
update();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { codeFrameColumns } from '@babel/code-frame';
import { StackFrame } from 'stacktrace-parser';

import type webpack from '@shuvi/toolpack/lib/webpack';

import { getSourcePath } from './getSourcePath';
import { getModuleById } from './getModuleById';
import { findOriginalSourcePositionAndContent } from './findOriginalSourcePositionAndContent';

export type OriginalStackFrameResponse = {
originalStackFrame: StackFrame;
originalCodeFrame: string | null;
};

export async function createOriginalStackFrame({
line,
column,
source,
modulePath,
frame,
errorMessage,
compilation
}: {
line: number;
column: number | null;
source: any;
modulePath?: string;
frame: any;
errorMessage?: string;
compilation?: webpack.Compilation;
}): Promise<OriginalStackFrameResponse | null> {
const match = errorMessage?.match(/'([^']+)' module/);
const moduleNotFound = match && match[1];
const result =
moduleNotFound && compilation
? getModuleById(
modulePath,
compilation!
)?.buildInfo?.importLocByPath?.get(moduleNotFound) ?? null
: await findOriginalSourcePositionAndContent(source, {
line,
column
});

if (result === null) {
return null;
}

const { sourcePosition, sourceContent } = result;

if (!sourcePosition.source) {
return null;
}

const filePath = getSourcePath(sourcePosition.source) || modulePath || '';

const originalFrame: StackFrame = {
file: sourceContent ? filePath : sourcePosition.source,
lineNumber: sourcePosition.line,
column: sourcePosition.column,
methodName: frame.methodName,
arguments: []
};

const originalCodeFrame: string | null =
!(originalFrame.file?.includes('node_modules') ?? true) &&
sourceContent &&
sourcePosition.line
? (codeFrameColumns(
sourceContent,
{
start: {
line: sourcePosition.line,
column: sourcePosition.column ?? 0
}
},
{ forceColor: true }
) as string)
: null;

return {
originalStackFrame: originalFrame,
originalCodeFrame
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { StackFrame } from 'stacktrace-parser';
import type webpack from '@shuvi/toolpack/lib/webpack';
import type { Source } from './getSourceById';
import type { OriginalStackFrame } from '../../view/helpers/stack-frame';
import { createOriginalStackFrame } from './createOriginalStackFrame';

export async function getOriginalStackFrame(
frame: StackFrame,
cache: Map<string, Source>,
resolveBuildFile: (...paths: string[]) => string,
buildDefaultDir: string,
errorMessage?: string,
compilation?: webpack.Compilation
): Promise<OriginalStackFrame> {
if (
!(
frame.file?.startsWith('webpack-internal:') ||
frame.file?.startsWith('file:')
)
) {
return {
error: false,
reason: null,
external: true,
expanded: false,
sourceStackFrame: frame,
originalStackFrame: null,
originalCodeFrame: null
};
}

if (cache.get(frame.file) === null) {
return {
error: true,
reason: 'No Content',
external: false,
expanded: false,
sourceStackFrame: frame,
originalStackFrame: null,
originalCodeFrame: null
};
}

const frameLine = parseInt(frame.lineNumber?.toString() ?? '', 10);
let frameColumn: number | null = parseInt(frame.column?.toString() ?? '', 10);
if (!frameColumn) {
frameColumn = null;
}
const originalStackFrameResponse = await createOriginalStackFrame({
line: frameLine,
column: frameColumn,
source: cache.get(frame.file),
frame,
modulePath: resolveBuildFile(
buildDefaultDir,
frame.file.replace(/^(file:\/\/)/, '')
),
errorMessage,
compilation
});
if (originalStackFrameResponse === null) {
return {
error: true,
reason: 'No Content',
external: false,
expanded: false,
sourceStackFrame: frame,
originalStackFrame: null,
originalCodeFrame: null
};
}
return {
error: false,
reason: null,
external: false,
expanded: !Boolean(
/* collapsed */
(frame.file?.includes('node_modules') ||
originalStackFrameResponse.originalStackFrame?.file?.includes(
'node_modules'
)) ??
true
),
sourceStackFrame: frame,
originalStackFrame: originalStackFrameResponse.originalStackFrame,
originalCodeFrame: originalStackFrameResponse.originalCodeFrame || null
};
}
60 changes: 46 additions & 14 deletions packages/error-overlay/src/middleware/helper/getSourceById.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { promises as fs } from 'fs';
import { RawSourceMap } from 'source-map';
import dataUriToBuffer, { MimeBuffer } from 'data-uri-to-buffer';
import type webpack from '@shuvi/toolpack/lib/webpack';
Expand All @@ -8,12 +7,52 @@ import { getModuleById } from './getModuleById';

export type Source = { map: () => RawSourceMap } | null;

function getRawSourceMap(fileContents: string): RawSourceMap | null {
const sourceUrl = getSourceMapUrl(fileContents);
if (!sourceUrl?.startsWith('data:')) {
const readFileWrapper = (
url: string,
compiler: webpack.Compiler
): Promise<string | null> => {
return new Promise(resolve => {
compiler.outputFileSystem.readFile(url, (err: any, res: any) => {
if (err) {
resolve(null);
}
resolve(res.toString());
});
});
};

async function getRawSourceMap(
fileUrl: string,
compiler: webpack.Compiler
): Promise<RawSourceMap | null> {
//fetch sourcemap directly first
const url = fileUrl + '.map';
let sourceMapContent: string | null = null;

sourceMapContent = await readFileWrapper(url, compiler);

if (sourceMapContent !== null) {
return sourceMapContent;
}
//fetch sourcemap by fileContent
const fileContent = await readFileWrapper(fileUrl, compiler);

if (fileContent == null) {
return null;
}

const sourceUrl = getSourceMapUrl(fileContent);

if (!sourceUrl) {
return null;
}

if (!sourceUrl?.startsWith('data:')) {
const index = fileUrl.lastIndexOf('/');
const urlFromFile = fileUrl.substring(0, index + 1) + sourceUrl;
return await readFileWrapper(urlFromFile, compiler);
}

let buffer: MimeBuffer;
try {
buffer = dataUriToBuffer(sourceUrl);
Expand All @@ -38,22 +77,15 @@ function getRawSourceMap(fileContents: string): RawSourceMap | null {
export async function getSourceById(
isFile: boolean,
id: string,
compiler: webpack.Compiler,
compilation?: webpack.Compilation
): Promise<Source> {
if (isFile) {
const fileContent: string | null = await fs
.readFile(id, 'utf-8')
.catch(() => null);
const map = await getRawSourceMap(id, compiler);

if (fileContent == null) {
if (map === null) {
return null;
}

const map = getRawSourceMap(fileContent);
if (map == null) {
return null;
}

return {
map() {
return map;
Expand Down
10 changes: 4 additions & 6 deletions packages/error-overlay/src/middleware/helper/getSourcePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ export function getSourcePath(source: string) {
return source.substring(11);
}

// Make sure library name is filtered out as well
if (source.startsWith('webpack://_N_E/')) {
return source.substring(15);
}

if (source.startsWith('webpack://')) {
return source.substring(10);
return source.replace(
/^webpack:\/\/[^/]+/ /* webpack://namaspcae/resourcepath */,
''
);
}

if (source.startsWith('/')) {
Expand Down
5 changes: 0 additions & 5 deletions packages/error-overlay/src/middleware/helper/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import launchEditor from 'launch-editor';
import { IncomingMessage, ServerResponse } from 'http';
import url from 'url';
import path from 'path';
import { getSourcePath } from './helper';
import { getSourcePath } from './helper/getSourcePath';

export function launchEditorMiddleware(
launchEditorEndpoint: string,
Expand Down
Loading

0 comments on commit c42ab54

Please sign in to comment.