Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: display the source of client error #405

Merged
merged 12 commits into from
Aug 19, 2022
Merged
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