UNPKG

4.61 kBJavaScriptView Raw
1'use strict';
2const path = require('path');
3const cleanYamlObject = require('clean-yaml-object');
4const concordance = require('concordance');
5const isError = require('is-error');
6const slash = require('slash');
7const StackUtils = require('stack-utils');
8const assert = require('./assert');
9const concordanceOptions = require('./concordance-options').default;
10
11function isAvaAssertionError(source) {
12 return source instanceof assert.AssertionError;
13}
14
15function filter(propertyName, isRoot) {
16 return !isRoot || (propertyName !== 'message' && propertyName !== 'name' && propertyName !== 'stack');
17}
18
19const stackUtils = new StackUtils();
20function extractSource(stack, testFile) {
21 if (!stack || !testFile) {
22 return null;
23 }
24
25 // Normalize the test file so it matches `callSite.file`.
26 const relFile = path.relative(process.cwd(), testFile);
27 const normalizedFile = process.platform === 'win32' ? slash(relFile) : relFile;
28 for (const line of stack.split('\n')) {
29 try {
30 const callSite = stackUtils.parseLine(line);
31 if (callSite.file === normalizedFile) {
32 return {
33 isDependency: false,
34 isWithinProject: true,
35 file: path.resolve(process.cwd(), callSite.file),
36 line: callSite.line
37 };
38 }
39 } catch {}
40 }
41
42 return null;
43}
44
45function buildSource(source) {
46 if (!source) {
47 return null;
48 }
49
50 // Assume the CWD is the project directory. This holds since this function
51 // is only called in test workers, which are created with their working
52 // directory set to the project directory.
53 const projectDir = process.cwd();
54
55 const file = path.resolve(projectDir, source.file.trim());
56 const rel = path.relative(projectDir, file);
57
58 const [segment] = rel.split(path.sep);
59 const isWithinProject = segment !== '..' && (process.platform !== 'win32' || !segment.includes(':'));
60 const isDependency = isWithinProject && path.dirname(rel).split(path.sep).includes('node_modules');
61
62 return {
63 isDependency,
64 isWithinProject,
65 file,
66 line: source.line
67 };
68}
69
70function trySerializeError(err, shouldBeautifyStack, testFile) {
71 const stack = err.savedError ? err.savedError.stack : err.stack;
72
73 const retval = {
74 avaAssertionError: isAvaAssertionError(err),
75 nonErrorObject: false,
76 source: extractSource(stack, testFile),
77 stack,
78 shouldBeautifyStack
79 };
80
81 if (err.actualStack) {
82 retval.stack = err.actualStack;
83 }
84
85 if (retval.avaAssertionError) {
86 retval.improperUsage = err.improperUsage;
87 retval.message = err.message;
88 retval.name = err.name;
89 retval.statements = err.statements;
90 retval.values = err.values;
91
92 if (err.fixedSource) {
93 const source = buildSource(err.fixedSource);
94 if (source) {
95 retval.source = source;
96 }
97 }
98
99 if (err.assertion) {
100 retval.assertion = err.assertion;
101 }
102
103 if (err.operator) {
104 retval.operator = err.operator;
105 }
106 } else {
107 retval.object = cleanYamlObject(err, filter); // Cleanly copy non-standard properties
108 if (typeof err.message === 'string') {
109 retval.message = err.message;
110 }
111
112 if (typeof err.name === 'string') {
113 retval.name = err.name;
114 }
115 }
116
117 if (typeof err.stack === 'string') {
118 const lines = err.stack.split('\n');
119 if (err.name === 'SyntaxError' && !lines[0].startsWith('SyntaxError')) {
120 retval.summary = '';
121 for (const line of lines) {
122 retval.summary += line + '\n';
123 if (line.startsWith('SyntaxError')) {
124 break;
125 }
126 }
127
128 retval.summary = retval.summary.trim();
129 } else {
130 // Skip the source line header inserted by `esm`:
131 // <https://github.com/standard-things/esm/wiki/improved-errors>
132 const start = lines.findIndex(line => !/:\d+$/.test(line));
133 retval.summary = '';
134 for (let index = start; index < lines.length; index++) {
135 if (lines[index].startsWith(' at')) {
136 break;
137 }
138
139 const next = index + 1;
140 const end = next === lines.length || lines[next].startsWith(' at');
141 retval.summary += end ? lines[index] : lines[index] + '\n';
142 }
143 }
144 }
145
146 return retval;
147}
148
149function serializeError(origin, shouldBeautifyStack, err, testFile) {
150 if (!isError(err)) {
151 return {
152 avaAssertionError: false,
153 nonErrorObject: true,
154 formatted: concordance.formatDescriptor(concordance.describe(err, concordanceOptions), concordanceOptions)
155 };
156 }
157
158 try {
159 return trySerializeError(err, shouldBeautifyStack, testFile);
160 } catch {
161 const replacement = new Error(`${origin}: Could not serialize error`);
162 return {
163 avaAssertionError: false,
164 nonErrorObject: false,
165 name: replacement.name,
166 message: replacement.message,
167 stack: replacement.stack,
168 summary: replacement.message
169 };
170 }
171}
172
173module.exports = serializeError;