Skip to content

Commit f2ce5b1

Browse files
feat: improve errors
1 parent 9923244 commit f2ce5b1

File tree

9 files changed

+339
-268
lines changed

9 files changed

+339
-268
lines changed

src/HtmlSourceError.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
function getIndices(value) {
2+
const result = [];
3+
let index = value.indexOf('\n');
4+
5+
while (index !== -1) {
6+
result.push(index + 1);
7+
index = value.indexOf('\n', index + 1);
8+
}
9+
10+
result.push(value.length + 1);
11+
12+
return result;
13+
}
14+
15+
function offsetToPosition(source, offset) {
16+
let index = -1;
17+
const indices = getIndices(source);
18+
const { length } = indices;
19+
20+
if (offset < 0) {
21+
return {};
22+
}
23+
24+
// eslint-disable-next-line no-plusplus
25+
while (++index < length) {
26+
if (indices[index] > offset) {
27+
return {
28+
line: index + 1,
29+
column: offset - (indices[index - 1] || 0) + 1,
30+
offset,
31+
};
32+
}
33+
}
34+
35+
return {};
36+
}
37+
38+
export default class HtmlSourceError extends Error {
39+
constructor(error, startIndex, endIndex, source) {
40+
super(error);
41+
42+
this.name = 'HtmlSourceError';
43+
this.message = `${this.name}: ${this.message}`;
44+
this.startIndex = startIndex;
45+
this.endIndex = endIndex;
46+
this.source = source;
47+
48+
const startPosition = offsetToPosition(source, this.startIndex);
49+
const endPosition = offsetToPosition(source, this.endIndex);
50+
51+
this.message += ` (From line ${startPosition.line}, column ${startPosition.column}; to line ${endPosition.line}, column ${endPosition.column})`;
52+
53+
// We don't need stack
54+
this.stack = false;
55+
}
56+
}

src/Warning.js

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/index.js

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { getOptions } from 'loader-utils';
22
import validateOptions from 'schema-utils';
33

44
import { sourcePlugin, minimizerPlugin } from './plugins';
5-
import Warning from './Warning';
65

76
import {
87
pluginRunner,
@@ -40,37 +39,32 @@ export default function htmlLoader(content) {
4039
plugins.push(minimizerPlugin({ minimize }));
4140
}
4241

43-
const { html, messages, warnings, errors } = pluginRunner(plugins).process(
44-
content
45-
);
46-
47-
for (const warning of warnings) {
48-
this.emitWarning(new Warning(warning));
49-
}
50-
51-
for (const error of errors) {
52-
this.emitError(new Error(error));
53-
}
42+
const { html, messages } = pluginRunner(plugins).process(content);
5443

44+
const errors = [];
5545
const importedMessages = [];
5646
const replaceableMessages = [];
5747
const exportedMessages = [];
5848

5949
for (const message of messages) {
6050
// eslint-disable-next-line default-case
6151
switch (message.type) {
52+
case 'error':
53+
errors.push(message.value);
54+
break;
6255
case 'import':
6356
importedMessages.push(message.value);
6457
break;
6558
case 'replacer':
6659
replaceableMessages.push(message.value);
6760
break;
68-
case 'exports':
69-
exportedMessages.push(message.value);
70-
break;
7161
}
7262
}
7363

64+
for (const error of errors) {
65+
this.emitError(error instanceof Error ? error : new Error(error));
66+
}
67+
7468
const codeOptions = { ...options, loaderContext: this };
7569
const importCode = getImportCode(html, importedMessages, codeOptions);
7670
const moduleCode = getModuleCode(html, replaceableMessages, codeOptions);

src/plugins/minimizer-plugin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default (options) =>
2323
// eslint-disable-next-line no-param-reassign
2424
html = minify(html, minimizeOptions);
2525
} catch (error) {
26-
result.errors.push(error);
26+
result.messages.push({ type: 'error', value: error });
2727
}
2828

2929
return html;

src/plugins/source-plugin.js

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { parse } from 'url';
33
import { Parser } from 'htmlparser2';
44
import { isUrlRequest, urlToRequest } from 'loader-utils';
55

6+
import HtmlSourceError from '../HtmlSourceError';
67
import { getFilter } from '../utils';
78

89
function isASCIIWhitespace(character) {
@@ -448,8 +449,6 @@ export default (options) =>
448449
unquoted,
449450
} = this.attributesMeta[attribute];
450451

451-
// TODO use code frame for errors
452-
453452
if (
454453
!onOpenTagFilter.test(`:${attribute}`) &&
455454
!onOpenTagFilter.test(`${tag}:${attribute}`)
@@ -478,11 +477,15 @@ export default (options) =>
478477
try {
479478
sourceSet = parseSrcset(value);
480479
} catch (error) {
481-
result.errors.push(
482-
new Error(
483-
`Bad value for attribute "${attribute}" on element "${tag}": ${error.message}`
484-
)
485-
);
480+
result.messages.push({
481+
type: 'error',
482+
value: new HtmlSourceError(
483+
`Bad value for attribute "${attribute}" on element "${tag}": ${error.message}`,
484+
parser.startIndex,
485+
parser.endIndex,
486+
html
487+
),
488+
});
486489

487490
return;
488491
}
@@ -507,11 +510,15 @@ export default (options) =>
507510
try {
508511
source = parseSrc(value);
509512
} catch (error) {
510-
result.errors.push(
511-
new Error(
512-
`Bad value for attribute "${attribute}" on element "${tag}": ${error.message}`
513-
)
514-
);
513+
result.messages.push({
514+
type: 'error',
515+
value: new HtmlSourceError(
516+
`Bad value for attribute "${attribute}" on element "${tag}": ${error.message}`,
517+
parser.startIndex,
518+
parser.endIndex,
519+
html
520+
),
521+
});
515522

516523
return;
517524
}
@@ -527,9 +534,11 @@ export default (options) =>
527534

528535
this.attributesMeta = {};
529536
},
530-
/* istanbul ignore next */
531537
onerror(error) {
532-
result.errors.push(error);
538+
result.messages.push({
539+
type: 'error',
540+
value: error,
541+
});
533542
},
534543
},
535544
{

src/utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const GET_SOURCE_FROM_IMPORT_NAME = '___HTML_LOADER_GET_SOURCE_FROM_IMPORT___';
55
export function pluginRunner(plugins) {
66
return {
77
process: (content) => {
8-
const result = { messages: [], warnings: [], errors: [] };
8+
const result = { messages: [] };
99

1010
for (const plugin of plugins) {
1111
// eslint-disable-next-line no-param-reassign

0 commit comments

Comments
 (0)