Skip to content

feat: add formatter.pathType option for absolute path #792

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

Merged
merged 1 commit into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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