Skip to content

Commit 39772a5

Browse files
committed
feat: improve formatting of error messages
1 parent 98d4e63 commit 39772a5

File tree

7 files changed

+111
-17
lines changed

7 files changed

+111
-17
lines changed

src/formatLessError.js

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,36 @@
1+
const os = require('os');
2+
3+
/**
4+
* Tries to get an excerpt of the file where the error happened.
5+
* Uses err.line and err.column.
6+
*
7+
* Returns an empty string if the excerpt could not be retrieved.
8+
*
9+
* @param {LessError} err
10+
* @returns {Array<string>}
11+
*/
12+
function getFileExcerptIfPossible(lessErr) {
13+
try {
14+
const excerpt = lessErr.extract.slice(0, 2);
15+
const column = Math.max(lessErr.column - 1, 0);
16+
17+
if (typeof excerpt[0] === 'undefined') {
18+
excerpt.shift();
19+
}
20+
21+
excerpt.push(`${new Array(column).join(' ')}^`);
22+
23+
return excerpt;
24+
} catch (unexpectedErr) {
25+
// If anything goes wrong here, we don't want any errors to be reported to the user
26+
return [];
27+
}
28+
}
29+
130
/**
231
* Beautifies the error message from Less.
332
*
4-
* @param {Error} lessErr
33+
* @param {LessError} lessErr
534
* @param {string} lessErr.type - e.g. 'Name'
635
* @param {string} lessErr.message - e.g. '.undefined-mixin is undefined'
736
* @param {string} lessErr.filename - e.g. '/path/to/style.less'
@@ -11,18 +40,23 @@
1140
* @param {string} lessErr.callExtract - e.g. undefined
1241
* @param {number} lessErr.column - e.g. 6
1342
* @param {Array<string>} lessErr.extract - e.g. [' .my-style {', ' .undefined-mixin;', ' display: block;']
43+
* @returns {LessError}
1444
*/
15-
function formatLessError(lessErr) {
16-
const extract = lessErr.extract ? `\n near lines:\n ${lessErr.extract.join('\n ')}` : '';
17-
const err = new Error(
18-
`${lessErr.message}\n @ ${lessErr.filename
19-
} (line ${lessErr.line}, column ${lessErr.column})${
20-
extract}`,
21-
);
45+
function formatLessError(err) { /* eslint-disable no-param-reassign */
46+
const msg = err.message;
2247

48+
// Instruct webpack to hide the JS stack from the console
49+
// Usually you're only interested in the SASS stack in this case.
2350
err.hideStack = true;
2451

52+
err.message = [
53+
os.EOL,
54+
...getFileExcerptIfPossible(err),
55+
msg.charAt(0).toUpperCase() + msg.slice(1),
56+
` in ${err.filename} (line ${err.line}, column ${err.column})`,
57+
].join(os.EOL);
58+
2559
return err;
26-
}
60+
} /* eslint-enable no-param-reassign */
2761

2862
module.exports = formatLessError;

test/__snapshots__/index.test.js.snap

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`should fail if a file is tried to be loaded from include paths and with webpack's resolver simultaneously 1`] = `
4+
"Module build failed:
5+
6+
@import \\"some/module.less\\";
7+
@import \\"~some/module.less\\";
8+
^
9+
'~some/module.less' wasn't found. Tried - /test/fixtures/less/~some/module.less,/test/fixtures/node_modules/~some/module.less,~some/module.less
10+
in /test/fixtures/less/error-mixed-resolvers.less (line 3, column 0)"
11+
`;
12+
13+
exports[`should provide a useful error message if the import could not be found 1`] = `
14+
"Module build failed:
15+
16+
@import \\"not-existing\\";
17+
^
18+
Can't resolve './not-existing.less' in '/test/fixtures/less'
19+
in /test/fixtures/less/error-import-not-existing.less (line 1, column 0)"
20+
`;
21+
22+
exports[`should provide a useful error message if there was a syntax error 1`] = `
23+
"Module build failed:
24+
25+
but this is a syntax error
26+
27+
^
28+
Unrecognised input. Possibly missing something
29+
in /test/fixtures/less/error-syntax.less (line 6, column 0)"
30+
`;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
@import "not-existing";
1+
@import "not-existing";

test/fixtures/less/error-syntax.less

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.this-less-code-works {
2+
background: hotpink;
3+
}
4+
5+
but this is a syntax error

test/helpers/compareErrorMessage.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const path = require('path');
2+
3+
const projectPath = path.resolve(__dirname, '..', '..');
4+
const projectPathPattern = new RegExp(projectPath, 'g');
5+
6+
// We need to remove all environment dependent features
7+
function compareErrorMessage(msg) {
8+
const envIndependentMsg = msg
9+
.replace(projectPathPattern, '')
10+
.replace(/\\/g, '/');
11+
12+
expect(envIndependentMsg).toMatchSnapshot();
13+
}
14+
15+
module.exports = compareErrorMessage;

test/helpers/createSpec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const ignore = [
1212
'import-non-less',
1313
'error-import-not-existing',
1414
'error-mixed-resolvers',
15+
'error-syntax',
1516
];
1617
const lessReplacements = [
1718
[/~some\//g, '../node_modules/some/'],

test/index.test.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const path = require('path');
22
const compile = require('./helpers/compile');
33
const moduleRules = require('./helpers/moduleRules');
44
const { readCssFixture, readSourceMap } = require('./helpers/readFixture');
5+
const compareErrorMessage = require('./helpers/compareErrorMessage');
56

67
const nodeModulesPath = path.resolve(__dirname, 'fixtures', 'node_modules');
78

@@ -16,7 +17,7 @@ async function compileAndCompare(fixture, { lessLoaderOptions, lessLoaderContext
1617
]);
1718
const [actualCss] = inspect.arguments;
1819

19-
return expect(actualCss).toBe(expectedCss);
20+
expect(actualCss).toBe(expectedCss);
2021
}
2122

2223
test('should compile simple less without errors', async () => {
@@ -116,18 +117,26 @@ test('should not alter the original options object', async () => {
116117
expect(copiedOptions).toEqual(options);
117118
});
118119

119-
test('should report error correctly', async () => {
120-
const err = await compile('error-import-not-existing')
120+
test('should fail if a file is tried to be loaded from include paths and with webpack\'s resolver simultaneously', async () => {
121+
const err = await compile('error-mixed-resolvers', moduleRules.basic({ paths: [nodeModulesPath] }))
121122
.catch(e => e);
122123

123124
expect(err).toBeInstanceOf(Error);
124-
expect(err.message).toMatch(/not-existing/);
125+
compareErrorMessage(err.message);
125126
});
126127

127-
test('should fail if a file is tried to be loaded from include paths and with webpack\'s resolver simultaneously', async () => {
128-
const err = await compile('error-mixed-resolvers', moduleRules.basic({ paths: [nodeModulesPath] }))
128+
test('should provide a useful error message if the import could not be found', async () => {
129+
const err = await compile('error-import-not-existing', moduleRules.basic())
130+
.catch(e => e);
131+
132+
expect(err).toBeInstanceOf(Error);
133+
compareErrorMessage(err.message);
134+
});
135+
136+
test('should provide a useful error message if there was a syntax error', async () => {
137+
const err = await compile('error-syntax', moduleRules.basic())
129138
.catch(e => e);
130139

131140
expect(err).toBeInstanceOf(Error);
132-
expect(err.message).toMatch(/'~some\/module\.less' wasn't found/);
141+
compareErrorMessage(err.message);
133142
});

0 commit comments

Comments
 (0)