diff --git a/README.md b/README.md index fbdbd5ef..a471cfe4 100644 --- a/README.md +++ b/README.md @@ -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: { } }`. | -| `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: { } }`. 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 diff --git a/src/formatter/formatter-config.ts b/src/formatter/formatter-config.ts index 9fb99ce4..785f6d75 100644 --- a/src/formatter/formatter-config.ts +++ b/src/formatter/formatter-config.ts @@ -1,13 +1,19 @@ 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 @@ -15,9 +21,14 @@ function createFormatterConfig(options: FormatterOptions | undefined): Formatter ? 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') { @@ -25,7 +36,11 @@ function createFormatterConfig(options: FormatterOptions | undefined): Formatter options && typeof options === 'object' ? (options as CodeframeFormatterOptions).options || {} : {}; - return createCodeFrameFormatter(config); + + return { + format: createCodeFrameFormatter(config), + pathType, + }; } throw new Error( diff --git a/src/formatter/formatter-options.ts b/src/formatter/formatter-options.ts index deb6e72c..baeee13f 100644 --- a/src/formatter/formatter-options.ts +++ b/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 = diff --git a/src/formatter/formatter.ts b/src/formatter/formatter.ts index 4511a7f5..a3908552 100644 --- a/src/formatter/formatter.ts +++ b/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 }; diff --git a/src/formatter/webpack-formatter.ts b/src/formatter/webpack-formatter.ts index 7adacfe4..a4144e86 100644 --- a/src/formatter/webpack-formatter.ts +++ b/src/formatter/webpack-formatter.ts @@ -1,13 +1,15 @@ 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; @@ -15,7 +17,11 @@ function createWebpackFormatter(formatter: Formatter): Formatter { 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))}`; } diff --git a/src/hooks/tap-after-compile-to-get-issues.ts b/src/hooks/tap-after-compile-to-get-issues.ts index 6f8a3187..38c0cb97 100644 --- a/src/hooks/tap-after-compile-to-get-issues.ts +++ b/src/hooks/tap-after-compile-to-get-issues.ts @@ -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); diff --git a/src/hooks/tap-done-to-async-get-issues.ts b/src/hooks/tap-done-to-async-get-issues.ts index 82a4577c..0dc4c894 100644 --- a/src/hooks/tap-done-to-async-get-issues.ts +++ b/src/hooks/tap-done-to-async-get-issues.ts @@ -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 @@ -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); diff --git a/src/issue/issue-webpack-error.ts b/src/issue/issue-webpack-error.ts index 8d76c17c..612d69ed 100644 --- a/src/issue/issue-webpack-error.ts +++ b/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'; @@ -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))}`; diff --git a/src/plugin-options.json b/src/plugin-options.json index 810edfa3..9132ebfe 100644 --- a/src/plugin-options.json +++ b/src/plugin-options.json @@ -34,6 +34,9 @@ "type": { "$ref": "#/definitions/FormatterType" }, + "pathType": { + "$ref": "#/definitions/FormatterPathType" + }, "options": { "type": "object", "additionalProperties": true @@ -45,6 +48,10 @@ "type": "string", "enum": ["basic", "codeframe"] }, + "FormatterPathType": { + "type": "string", + "enum": ["relative", "absolute"] + }, "IssueMatch": { "type": "object", "properties": { diff --git a/test/unit/formatter/formatter-config.spec.ts b/test/unit/formatter/formatter-config.spec.ts index 83dec218..0f228c9f 100644 --- a/test/unit/formatter/formatter-config.spec.ts +++ b/test/unit/formatter/formatter-config.spec.ts @@ -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); }); }); diff --git a/test/unit/formatter/webpack-formatter.spec.ts b/test/unit/formatter/webpack-formatter.spec.ts index 178ab151..4279be43 100644 --- a/test/unit/formatter/webpack-formatter.spec.ts +++ b/test/unit/formatter/webpack-formatter.spec.ts @@ -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', @@ -23,29 +25,32 @@ 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 } }, }) @@ -53,7 +58,7 @@ describe('formatter/webpack-formatter', () => { }); 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) ); });