Skip to content

Commit

Permalink
feat: add formatter.pathType option for absolute path (#792)
Browse files Browse the repository at this point in the history
Add `formatter.pathType` option with available values `relative`
(default) and `absolute`. If you set it to `absolute`, the plugin will
print absolute paths to error locations.

✅ Closes: #789
  • Loading branch information
piotr-oles committed Jan 10, 2023
1 parent 90ff881 commit 3ae3406
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 42 deletions.
16 changes: 8 additions & 8 deletions README.md
Expand Up @@ -93,14 +93,14 @@ you can place your configuration in the:

Options passed to the plugin constructor will overwrite options from the cosmiconfig (using [deepmerge](https://github.com/TehShrike/deepmerge)).

| Name | Type | Default value | Description |
|--------------|--------------------------------------|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `async` | `boolean` | `compiler.options.mode === 'development'` | If `true`, reports issues **after** webpack's compilation is done. Thanks to that it doesn't block the compilation. Used only in the `watch` mode. |
| `typescript` | `object` | `{}` | See [TypeScript options](#typescript-options). |
| `issue` | `object` | `{}` | See [Issues options](#issues-options). |
| `formatter` | `string` or `object` or `function` | `codeframe` | Available formatters are `basic`, `codeframe` and a custom `function`. To [configure](https://babeljs.io/docs/en/babel-code-frame#options) `codeframe` formatter, pass object: `{ type: 'codeframe', options: { <coderame options> } }`. |
| `logger` | `{ log: function, error: function }` or `webpack-infrastructure` | `console` | Console-like object to print issues in `async` mode. |
| `devServer` | `boolean` | `true` | If set to `false`, errors will not be reported to Webpack Dev Server. |
| Name | Type | Default value | Description |
|--------------|--------------------------------------|-------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `async` | `boolean` | `compiler.options.mode === 'development'` | If `true`, reports issues **after** webpack's compilation is done. Thanks to that it doesn't block the compilation. Used only in the `watch` mode. |
| `typescript` | `object` | `{}` | See [TypeScript options](#typescript-options). |
| `issue` | `object` | `{}` | See [Issues options](#issues-options). |
| `formatter` | `string` or `object` or `function` | `codeframe` | Available formatters are `basic`, `codeframe` and a custom `function`. To [configure](https://babeljs.io/docs/en/babel-code-frame#options) `codeframe` formatter, pass: `{ type: 'codeframe', options: { <coderame options> } }`. To use absolute file path, pass: `{ type: 'codeframe', pathType: 'absolute' }`. |
| `logger` | `{ log: function, error: function }` or `webpack-infrastructure` | `console` | Console-like object to print issues in `async` mode. |
| `devServer` | `boolean` | `true` | If set to `false`, errors will not be reported to Webpack Dev Server. |

### TypeScript options

Expand Down
25 changes: 20 additions & 5 deletions src/formatter/formatter-config.ts
@@ -1,31 +1,46 @@
import { createBasicFormatter } from './basic-formatter';
import { createCodeFrameFormatter } from './code-frame-formatter';
import type { Formatter } from './formatter';
import type { Formatter, FormatterPathType } from './formatter';
import type { CodeframeFormatterOptions, FormatterOptions } from './formatter-options';

type FormatterConfig = Formatter;
type FormatterConfig = {
format: Formatter;
pathType: FormatterPathType;
};

function createFormatterConfig(options: FormatterOptions | undefined): FormatterConfig {
if (typeof options === 'function') {
return options;
return {
format: options,
pathType: 'relative',
};
}

const type = options
? typeof options === 'object'
? options.type || 'codeframe'
: options
: 'codeframe';
const pathType =
options && typeof options === 'object' ? options.pathType || 'relative' : 'relative';

if (!type || type === 'basic') {
return createBasicFormatter();
return {
format: createBasicFormatter(),
pathType,
};
}

if (type === 'codeframe') {
const config =
options && typeof options === 'object'
? (options as CodeframeFormatterOptions).options || {}
: {};
return createCodeFrameFormatter(config);

return {
format: createCodeFrameFormatter(config),
pathType,
};
}

throw new Error(
Expand Down
4 changes: 3 additions & 1 deletion src/formatter/formatter-options.ts
@@ -1,13 +1,15 @@
import type { Formatter } from './formatter';
import type { Formatter, FormatterPathType } from './formatter';
import type { BabelCodeFrameOptions } from './types/babel__code-frame';

type FormatterType = 'basic' | 'codeframe';

type BasicFormatterOptions = {
type: 'basic';
pathType?: FormatterPathType;
};
type CodeframeFormatterOptions = {
type: 'codeframe';
pathType?: FormatterPathType;
options?: BabelCodeFrameOptions;
};
type FormatterOptions =
Expand Down
3 changes: 2 additions & 1 deletion src/formatter/formatter.ts
@@ -1,5 +1,6 @@
import type { Issue } from '../issue';

type Formatter = (issue: Issue) => string;
type FormatterPathType = 'relative' | 'absolute';

export { Formatter };
export { Formatter, FormatterPathType };
12 changes: 9 additions & 3 deletions src/formatter/webpack-formatter.ts
@@ -1,21 +1,27 @@
import os from 'os';
import path from 'path';

import chalk from 'chalk';

import { formatIssueLocation } from '../issue';
import { forwardSlash } from '../utils/path/forward-slash';
import { relativeToContext } from '../utils/path/relative-to-context';

import type { Formatter } from './formatter';
import type { Formatter, FormatterPathType } from './formatter';

function createWebpackFormatter(formatter: Formatter): Formatter {
function createWebpackFormatter(formatter: Formatter, pathType: FormatterPathType): Formatter {
// mimics webpack error formatter
return function webpackFormatter(issue) {
const color = issue.severity === 'warning' ? chalk.yellow.bold : chalk.red.bold;

const severity = issue.severity.toUpperCase();

if (issue.file) {
let location = chalk.bold(relativeToContext(issue.file, process.cwd()));
let location = chalk.bold(
pathType === 'absolute'
? forwardSlash(path.resolve(issue.file))
: relativeToContext(issue.file, process.cwd())
);
if (issue.location) {
location += `:${chalk.green.bold(formatIssueLocation(issue.location))}`;
}
Expand Down
6 changes: 5 additions & 1 deletion src/hooks/tap-after-compile-to-get-issues.ts
Expand Up @@ -44,7 +44,11 @@ function tapAfterCompileToGetIssues(
issues = hooks.issues.call(issues, compilation);

issues.forEach((issue) => {
const error = new IssueWebpackError(config.formatter(issue), issue);
const error = new IssueWebpackError(
config.formatter.format(issue),
config.formatter.pathType,
issue
);

if (issue.severity === 'warning') {
compilation.warnings.push(error);
Expand Down
8 changes: 6 additions & 2 deletions src/hooks/tap-done-to-async-get-issues.ts
Expand Up @@ -59,7 +59,7 @@ function tapDoneToAsyncGetIssues(
// modify list of issues in the plugin hooks
issues = hooks.issues.call(issues, stats.compilation);

const formatter = createWebpackFormatter(config.formatter);
const formatter = createWebpackFormatter(config.formatter.format, config.formatter.pathType);

if (issues.length) {
// follow webpack's approach - one process.write to stderr with all errors and warnings
Expand All @@ -75,7 +75,11 @@ function tapDoneToAsyncGetIssues(
// skip reporting if there are no issues, to avoid an extra hot reload
if (issues.length && state.webpackDevServerDoneTap) {
issues.forEach((issue) => {
const error = new IssueWebpackError(config.formatter(issue), issue);
const error = new IssueWebpackError(
config.formatter.format(issue),
config.formatter.pathType,
issue
);

if (issue.severity === 'warning') {
stats.compilation.warnings.push(error);
Expand Down
11 changes: 9 additions & 2 deletions src/issue/issue-webpack-error.ts
@@ -1,6 +1,10 @@
import path from 'path';

import chalk from 'chalk';
import webpack from 'webpack';

import type { FormatterPathType } from '../formatter';
import { forwardSlash } from '../utils/path/forward-slash';
import { relativeToContext } from '../utils/path/relative-to-context';

import type { Issue } from './issue';
Expand All @@ -9,14 +13,17 @@ import { formatIssueLocation } from './issue-location';
class IssueWebpackError extends webpack.WebpackError {
readonly hideStack = true;

constructor(message: string, readonly issue: Issue) {
constructor(message: string, pathType: FormatterPathType, readonly issue: Issue) {
super(message);

// to display issue location using `loc` property, webpack requires `error.module` which
// should be a NormalModule instance.
// to avoid such a dependency, we do a workaround - error.file will contain formatted location instead
if (issue.file) {
this.file = relativeToContext(issue.file, process.cwd());
this.file =
pathType === 'absolute'
? forwardSlash(path.resolve(issue.file))
: relativeToContext(issue.file, process.cwd());

if (issue.location) {
this.file += `:${chalk.green.bold(formatIssueLocation(issue.location))}`;
Expand Down
7 changes: 7 additions & 0 deletions src/plugin-options.json
Expand Up @@ -34,6 +34,9 @@
"type": {
"$ref": "#/definitions/FormatterType"
},
"pathType": {
"$ref": "#/definitions/FormatterPathType"
},
"options": {
"type": "object",
"additionalProperties": true
Expand All @@ -45,6 +48,10 @@
"type": "string",
"enum": ["basic", "codeframe"]
},
"FormatterPathType": {
"type": "string",
"enum": ["relative", "absolute"]
},
"IssueMatch": {
"type": "object",
"properties": {
Expand Down
26 changes: 17 additions & 9 deletions test/unit/formatter/formatter-config.spec.ts
Expand Up @@ -63,16 +63,24 @@ describe('formatter/formatter-config', () => {
].join(os.EOL);

it.each([
[undefined, CODEFRAME_FORMATTER_OUTPUT],
['basic', BASIC_FORMATTER_OUTPUT],
[customFormatter, CUSTOM_FORMATTER_OUTPUT],
['codeframe', CODEFRAME_FORMATTER_OUTPUT],
[{ type: 'basic' }, BASIC_FORMATTER_OUTPUT],
[{ type: 'codeframe' }, CODEFRAME_FORMATTER_OUTPUT],
[{ type: 'codeframe', options: { linesBelow: 1 } }, CUSTOM_CODEFRAME_FORMATTER_OUTPUT],
])('creates configuration from options', (options, expectedFormat) => {
[undefined, CODEFRAME_FORMATTER_OUTPUT, 'relative'],
['basic', BASIC_FORMATTER_OUTPUT, 'relative'],
[customFormatter, CUSTOM_FORMATTER_OUTPUT, 'relative'],
['codeframe', CODEFRAME_FORMATTER_OUTPUT, 'relative'],
[{ type: 'basic' }, BASIC_FORMATTER_OUTPUT, 'relative'],
[{ type: 'codeframe' }, CODEFRAME_FORMATTER_OUTPUT, 'relative'],
[
{ type: 'codeframe', options: { linesBelow: 1 } },
CUSTOM_CODEFRAME_FORMATTER_OUTPUT,
'relative',
],
[{ type: 'basic', pathType: 'relative' }, BASIC_FORMATTER_OUTPUT, 'relative'],
[{ type: 'basic', pathType: 'absolute' }, BASIC_FORMATTER_OUTPUT, 'absolute'],
[{ type: 'codeframe', pathType: 'absolute' }, CODEFRAME_FORMATTER_OUTPUT, 'absolute'],
])('creates configuration from options', (options, expectedFormat, expectedPathType) => {
const formatter = createFormatterConfig(options as FormatterOptions);

expect(formatter(issue)).toEqual(expectedFormat);
expect(formatter.format(issue)).toEqual(expectedFormat);
expect(formatter.pathType).toEqual(expectedPathType);
});
});
25 changes: 15 additions & 10 deletions test/unit/formatter/webpack-formatter.spec.ts
Expand Up @@ -5,6 +5,8 @@ import type { Formatter } from 'src/formatter';
import { createBasicFormatter, createWebpackFormatter } from 'src/formatter';
import type { Issue } from 'src/issue';

import { forwardSlash } from '../../../lib/utils/path/forward-slash';

describe('formatter/webpack-formatter', () => {
const issue: Issue = {
severity: 'error',
Expand All @@ -23,37 +25,40 @@ describe('formatter/webpack-formatter', () => {
},
};

let formatter: Formatter;
let relativeFormatter: Formatter;
let absoluteFormatter: Formatter;

beforeEach(() => {
formatter = createWebpackFormatter(createBasicFormatter());
relativeFormatter = createWebpackFormatter(createBasicFormatter(), 'relative');
absoluteFormatter = createWebpackFormatter(createBasicFormatter(), 'absolute');
});

it('decorates existing formatter', () => {
expect(formatter(issue)).toContain('TS123: Some issue content');
it('decorates existing relativeFormatter', () => {
expect(relativeFormatter(issue)).toContain('TS123: Some issue content');
});

it('formats issue severity', () => {
expect(formatter({ ...issue, severity: 'error' })).toContain('ERROR');
expect(formatter({ ...issue, severity: 'warning' })).toContain('WARNING');
expect(relativeFormatter({ ...issue, severity: 'error' })).toContain('ERROR');
expect(relativeFormatter({ ...issue, severity: 'warning' })).toContain('WARNING');
});

it('formats issue file', () => {
expect(formatter(issue)).toContain(`./some/file.ts`);
expect(relativeFormatter(issue)).toContain(`./some/file.ts`);
expect(absoluteFormatter(issue)).toContain(forwardSlash(`${process.cwd()}/some/file.ts`));
});

it('formats location', () => {
expect(formatter(issue)).toContain(':1:7');
expect(relativeFormatter(issue)).toContain(':1:7');
expect(
formatter({
relativeFormatter({
...issue,
location: { start: { line: 1, column: 7 }, end: { line: 10, column: 16 } },
})
).toContain(':1:7');
});

it('formats issue header like webpack', () => {
expect(formatter(issue)).toEqual(
expect(relativeFormatter(issue)).toEqual(
[`ERROR in ./some/file.ts:1:7`, 'TS123: Some issue content', ''].join(os.EOL)
);
});
Expand Down

0 comments on commit 3ae3406

Please sign in to comment.