diff --git a/.changeset/lazy-rats-add.md b/.changeset/lazy-rats-add.md
new file mode 100644
index 00000000..5ef7255b
--- /dev/null
+++ b/.changeset/lazy-rats-add.md
@@ -0,0 +1,5 @@
+---
+'prettier-eslint': major
+---
+
+Use Rollup for bundling CJS and ESM artifacts. Use Vitest for tests.
diff --git a/.eslintrc.js b/.eslintrc.cjs
similarity index 72%
rename from .eslintrc.js
rename to .eslintrc.cjs
index f7fac129..a3ee2463 100644
--- a/.eslintrc.js
+++ b/.eslintrc.cjs
@@ -2,10 +2,10 @@ const config = {
extends: [
'kentcdodds',
'kentcdodds/jest',
- 'plugin:node-dependencies/recommended'
+ 'plugin:node-dependencies/recommended',
],
parserOptions: {
- ecmaVersion: 2021
+ ecmaVersion: 2021,
},
rules: {
'valid-jsdoc': 'off',
@@ -15,16 +15,16 @@ const config = {
{
anonymous: 'never',
named: 'never',
- asyncArrow: 'always'
- }
+ asyncArrow: 'always',
+ },
],
'import/no-import-module-exports': 'off',
'arrow-parens': ['error', 'as-needed'],
quotes: ['error', 'single', { avoidEscape: true }],
},
settings: {
- 'import/ignore': ['node_modules', 'src'] // Using CommonJS in src
- }
+ 'import/ignore': ['node_modules', 'src'], // Using CommonJS in src
+ },
};
module.exports = config;
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index c25860b1..1f18b104 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -59,20 +59,41 @@ updates:
- 17.3.9
- 17.4.0
- 17.4.1
+ - dependency-name: '@rollup/plugin-commonjs'
+ versions:
+ - 28.0.1
+ - dependency-name: '@rollup/plugin-json'
+ versions:
+ - 6.1.0
+ - dependency-name: '@rollup/plugin-node-resolve'
+ versions:
+ - 15.3.0
+ - dependency-name: rollup
+ versions:
+ - 4.28.0
+ - dependency-name: rollup-plugin-auto-external
+ versions:
+ - 2.0.0
+ - dependency-name: memfs
+ versions:
+ - 4.14.1
- dependency-name: vue-eslint-parser
versions:
- 7.4.1
- 7.5.0
- 7.6.0
- - dependency-name: jest-cli
+ - dependency-name: vite
+ versions:
+ - 6.0.3
+ - dependency-name: vitest
versions:
- - 26.6.3
- - dependency-name: babel-jest
+ - 2.1.8
+ - dependency-name: '@vitest/coverage-v8'
versions:
- - 26.6.3
- - dependency-name: jest
+ - 2.1.8
+ - dependency-name: '@vitest/ui'
versions:
- - 26.6.3
+ - 2.1.8
- dependency-name: pretty-format
versions:
- 26.6.2
diff --git a/.prettierrc.js b/.prettierrc.js
deleted file mode 100644
index 48b1b40e..00000000
--- a/.prettierrc.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- arrowParens: 'avoid',
- singleQuote: true
-};
diff --git a/babel.config.js b/babel.config.js
deleted file mode 100644
index c2857c1f..00000000
--- a/babel.config.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const config = {
- presets: [
- [
- '@babel/env',
- {
- targets: {
- node: '10'
- }
- }
- ]
- ]
-};
-
-module.exports = config;
diff --git a/jest.config.js b/jest.config.js
deleted file mode 100644
index 7a638dae..00000000
--- a/jest.config.js
+++ /dev/null
@@ -1,14 +0,0 @@
-module.exports = {
- testEnvironment: 'node',
- collectCoverageFrom: ['src/**/*.js'],
- testPathIgnorePatterns: ['/node_modules/', '/fixtures/', '/__mocks__/'],
- coveragePathIgnorePatterns: ['/node_modules/', '/fixtures/', '/__mocks__/'],
- coverageThreshold: {
- global: {
- branches: 96,
- functions: 100,
- lines: 100,
- statements: 100
- }
- }
-};
diff --git a/package-scripts.js b/package-scripts.cjs
similarity index 75%
rename from package-scripts.js
rename to package-scripts.cjs
index b993df91..0f385227 100644
--- a/package-scripts.js
+++ b/package-scripts.cjs
@@ -22,23 +22,15 @@ module.exports = {
// with ESM. ESM support is needed due to prettier v3’s use of a dynamic
// `import()` in its `.cjs` file. The flag can be removed when node
// supports modules in the VM API or the import is removed from prettier.
- default: crossEnv(
- 'NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --coverage'
- ),
- update: crossEnv(
- 'NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --coverage --updateSnapshot'
- ),
- watch: crossEnv(
- 'NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --watch'
- ),
+ default: crossEnv('vitest --coverage run'),
+ update: crossEnv('vitest --coverage --updateSnapshot'),
+ watch: crossEnv('vitest'),
openCoverage: 'open coverage/lcov-report/index.html',
},
build: {
- description: 'delete the dist directory and run babel to build the files',
- script: series(
- rimraf('dist'),
- 'babel --out-dir dist --ignore "src/__tests__/**/*","src/__mocks__/**/*" src'
- ),
+ description:
+ 'delete the dist directory and run Rollup to build the files',
+ script: series(rimraf('dist'), 'rollup -c'),
},
lint: {
description: 'lint the entire project',
@@ -57,7 +49,11 @@ module.exports = {
validate: {
description:
'This runs several scripts to make sure things look good before committing or on clean install',
- script: concurrent.nps('lint', 'build', 'test'),
+ script: concurrent([
+ 'nps -c ./package-scripts.cjs lint',
+ 'nps -c ./package-scripts.cjs build',
+ 'nps -c ./package-scripts.cjs test',
+ ]),
},
format: {
description: 'Formats everything with prettier-eslint',
diff --git a/package.json b/package.json
index 41edec45..aaab0140 100644
--- a/package.json
+++ b/package.json
@@ -2,12 +2,14 @@
"name": "prettier-eslint",
"version": "16.3.0",
"description": "Formats your JavaScript using prettier followed by eslint --fix",
- "main": "dist/index.js",
+ "main": "dist/index.cjs.js",
+ "module": "dist/index.esm.js",
"types": "types/index.d.ts",
+ "type": "module",
"scripts": {
"prepare": "husky install",
- "start": "nps",
- "test": "nps test"
+ "start": "nps -c ./package-scripts.cjs",
+ "test": "nps -c ./package-scripts.cjs test"
},
"files": [
"dist",
@@ -52,20 +54,29 @@
"@babel/preset-env": "^7.22.9",
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.2",
+ "@rollup/plugin-commonjs": "^28.0.1",
+ "@rollup/plugin-json": "^6.1.0",
+ "@rollup/plugin-node-resolve": "^15.3.0",
"@types/eslint": "^8.4.2",
+ "@vitest/coverage-v8": "^2.1.8",
+ "@vitest/ui": "^2.1.8",
"all-contributors-cli": "^6.7.0",
"eslint-config-kentcdodds": "^20.5.0",
"eslint-plugin-node-dependencies": "^0.11.0",
"husky": "^8.0.1",
"jest": "^29.6.2",
+ "memfs": "^4.14.1",
"nps": "^5.7.1",
"nps-utils": "^1.3.0",
"prettier-eslint-cli": "^8.0.0",
"prettier-plugin-svelte": "^3.1.2",
"rimraf": "^5.0.5",
+ "rollup": "^4.28.0",
+ "rollup-plugin-auto-external": "^2.0.0",
"strip-indent": "^3.0.0",
"svelte": "^4.2.9",
- "svelte-eslint-parser": "^0.33.1"
+ "svelte-eslint-parser": "^0.33.1",
+ "vitest": "^2.1.8"
},
"engines": {
"node": ">=16.10.0"
diff --git a/prettier.config.mjs b/prettier.config.mjs
new file mode 100644
index 00000000..a4ecd0ed
--- /dev/null
+++ b/prettier.config.mjs
@@ -0,0 +1,4 @@
+export default {
+ arrowParens: 'avoid',
+ singleQuote: true,
+};
diff --git a/rollup.config.mjs b/rollup.config.mjs
new file mode 100644
index 00000000..31bb1f95
--- /dev/null
+++ b/rollup.config.mjs
@@ -0,0 +1,31 @@
+import autoExternal from 'rollup-plugin-auto-external';
+import commonjs from '@rollup/plugin-commonjs';
+import json from '@rollup/plugin-json';
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+
+export default {
+ input: 'src/index.mjs',
+ output: [
+ {
+ file: 'dist/cjs/index.js',
+ format: 'cjs',
+ name: 'cjs-bundle',
+ },
+ {
+ file: 'dist/esm/index.js',
+ format: 'esm',
+ name: 'esm-bundle',
+ },
+ ],
+ external: ['fs', 'node:path'],
+ plugins: [
+ autoExternal(),
+ commonjs({
+ include: /node_modules/,
+ requireReturnsDefault: 'auto',
+ strictRequires: 'debug',
+ }),
+ nodeResolve({ preferBuiltins: true }),
+ json(),
+ ],
+};
diff --git a/src/__mocks__/eslint.js b/src/__mocks__/eslint.mjs
similarity index 50%
rename from src/__mocks__/eslint.js
rename to src/__mocks__/eslint.mjs
index dced55a5..749088c1 100644
--- a/src/__mocks__/eslint.js
+++ b/src/__mocks__/eslint.mjs
@@ -1,40 +1,16 @@
+import { vi } from 'vitest';
+
// this mock file is so eslint doesn't attempt to actually
// search around the file system for stuff
-const eslint = jest.requireActual('eslint');
-const { ESLint } = eslint;
-
-const mockCalculateConfigForFileSpy = jest.fn(mockCalculateConfigForFile);
-mockCalculateConfigForFileSpy.overrides = {};
-const mockLintTextSpy = jest.fn(mockLintText);
-
-module.exports = Object.assign(eslint, {
- ESLint: jest.fn(MockESLint),
- mock: {
- calculateConfigForFile: mockCalculateConfigForFileSpy,
- lintText: mockLintTextSpy
- }
-});
-
-function MockESLint(...args) {
- global.__PRETTIER_ESLINT_TEST_STATE__.eslintPath = __filename;
- const eslintInstance = new ESLint(...args);
- eslintInstance.calculateConfigForFile = mockCalculateConfigForFileSpy;
- eslintInstance._originalLintText = eslintInstance.lintText;
- eslintInstance.lintText = mockLintTextSpy;
- return eslintInstance;
-}
-
-MockESLint.prototype = Object.create(ESLint.prototype);
+const eslintActual = await vi.importActual('eslint');
+const { ESLint } = eslintActual;
// eslint-disable-next-line complexity
function mockCalculateConfigForFile(filePath) {
- if (mockCalculateConfigForFileSpy.throwError) {
- throw mockCalculateConfigForFileSpy.throwError;
- }
if (!filePath) {
return {
- rules: {}
+ rules: {},
};
}
if (filePath.includes('default-config')) {
@@ -46,7 +22,7 @@ function mockCalculateConfigForFile(filePath) {
quotes: [
2,
'single',
- { avoidEscape: true, allowTemplateLiterals: true }
+ { avoidEscape: true, allowTemplateLiterals: true },
],
'comma-dangle': [
2,
@@ -55,26 +31,44 @@ function mockCalculateConfigForFile(filePath) {
objects: 'always-multiline',
imports: 'always-multiline',
exports: 'always-multiline',
- functions: 'always-multiline'
- }
+ functions: 'always-multiline',
+ },
],
- 'arrow-parens': [2, 'as-needed']
- }
+ 'arrow-parens': [2, 'as-needed'],
+ },
};
} else if (filePath.includes('fixtures/paths')) {
return { rules: {} };
} else {
throw new Error(
`Your mock filePath (${filePath})` +
- ' does not have a handler for finding the config'
+ ' does not have a handler for finding the config',
);
}
}
-function mockLintText(...args) {
+const mockLintTextSpy = vi.fn(function mockLintText(...args) {
/* eslint no-invalid-this:0 */
if (mockLintTextSpy.throwError) {
throw mockLintTextSpy.throwError;
}
return this._originalLintText(...args);
+});
+
+const calculateConfigForFileSpy = vi.spyOn(ESLint.prototype, 'calculateConfigForFile').mockImplementation(mockCalculateConfigForFile);
+const lintTextSpy = vi.spyOn(ESLint.prototype, 'lintText');
+
+function MockESLint(...args) {
+ global.__PRETTIER_ESLINT_TEST_STATE__.eslintPath = __filename;
+ return new ESLint(...args);
}
+
+export default {
+ ...eslintActual.default,
+ ESLint: vi.fn(MockESLint),
+};
+
+export const helpers = {
+ getCalculateConfigForFileSpy: () => calculateConfigForFileSpy,
+ getLintTextSpy: () => lintTextSpy,
+};
diff --git a/src/__mocks__/fs.js b/src/__mocks__/fs.js
deleted file mode 100644
index 02f17367..00000000
--- a/src/__mocks__/fs.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const fs = jest.requireActual('fs');
-module.exports = {
- ...fs,
- readFileSync: jest.fn(filename => {
- if (/package\.json$/.test(filename)) {
- return '{"name": "fake", "version": "0.0.0", "prettier": {}}';
- } else if (/\.(j|t)s$/.test(filename)) {
- return 'var fake = true';
- }
-
- return '';
- })
-};
diff --git a/src/__mocks__/fs.mjs b/src/__mocks__/fs.mjs
new file mode 100644
index 00000000..c931ed89
--- /dev/null
+++ b/src/__mocks__/fs.mjs
@@ -0,0 +1,25 @@
+// This follows the advice of the Vitest Mocking guide, and mocks the full
+// filesystem with an in-memory replacement.
+// https://vitest.dev/guide/mocking#file-system
+const { createFsFromVolume, memfs, Volume } = require('memfs');
+import path from 'node:path';
+
+const files = [
+ 'foo.js',
+ 'package.json',
+ 'node_modules/eslint/index.js',
+ 'node_modules/prettier/index.js',
+];
+
+const volume = new Volume();
+
+volume.fromJSON(
+ Object.fromEntries(
+ files.map(file => {
+ return [`./${file}`, `.tests/fixtures/paths/${file}`];
+ }),
+ ),
+ `${path.dirname(__filename)}/../../tests/fixtures/paths`,
+);
+
+export default createFsFromVolume(volume);
diff --git a/src/__mocks__/loglevel-colored-level-prefix.js b/src/__mocks__/loglevel-colored-level-prefix.js
deleted file mode 100644
index f79f744a..00000000
--- a/src/__mocks__/loglevel-colored-level-prefix.js
+++ /dev/null
@@ -1,36 +0,0 @@
-module.exports = getLogger;
-const logger = {
- setLevel: jest.fn(),
- trace: jest.fn(getTestImplementation('trace')),
- debug: jest.fn(getTestImplementation('debug')),
- info: jest.fn(getTestImplementation('info')),
- warn: jest.fn(getTestImplementation('warn')),
- error: jest.fn(getTestImplementation('error'))
-};
-const mock = { clearAll, logger, logThings: [] };
-
-Object.assign(module.exports, {
- mock
-});
-
-function getLogger() {
- return logger;
-}
-
-function clearAll() {
- Object.keys(logger).forEach(name => {
- if (logger[name].mock) {
- logger[name].mockClear();
- }
- });
-}
-
-function getTestImplementation(level) {
- return testLogImplementation;
-
- function testLogImplementation(...args) {
- if (mock.logThings === 'all' || mock.logThings.indexOf(level) !== -1) {
- console.log(level, ...args); // eslint-disable-line no-console
- }
- }
-}
diff --git a/src/__mocks__/loglevel-colored-level-prefix.mjs b/src/__mocks__/loglevel-colored-level-prefix.mjs
new file mode 100644
index 00000000..b3ca386a
--- /dev/null
+++ b/src/__mocks__/loglevel-colored-level-prefix.mjs
@@ -0,0 +1,32 @@
+import { vi } from 'vitest';
+
+const logger = {
+ setLevel: vi.fn(),
+ trace: vi.fn(getTestImplementation('trace')),
+ debug: vi.fn(getTestImplementation('debug')),
+ info: vi.fn(getTestImplementation('info')),
+ warn: vi.fn(getTestImplementation('warn')),
+ error: vi.fn(getTestImplementation('error'))
+};
+
+const logThings = [];
+
+function mockClear() {
+ Object.values(logger).forEach((mock) => {
+ mock.mockClear();
+ });
+}
+
+function getTestImplementation(level) {
+ return function testLogImplementation(...args) {
+ if (logThings === 'all' || logThings.indexOf(level) !== -1) {
+ console.log(level, ...args); // eslint-disable-line no-console
+ }
+ }
+}
+
+export default function getLogger() {
+ return logger;
+}
+
+export const helpers = { mockClear, getLogger: () => logger, logThings };
diff --git a/src/__mocks__/prettier.js b/src/__mocks__/prettier.js
deleted file mode 100644
index 52362e84..00000000
--- a/src/__mocks__/prettier.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const prettier = jest.requireActual('prettier');
-const { format } = prettier;
-
-module.exports = prettier;
-
-const mockFormatSpy = jest.fn(mockFormat);
-
-Object.assign(prettier, {
- format: mockFormatSpy,
- resolveConfig: jest.fn()
-});
-
-function mockFormat(...args) {
- global.__PRETTIER_ESLINT_TEST_STATE__.prettierPath = __filename;
- if (mockFormatSpy.throwError) {
- throw mockFormatSpy.throwError;
- }
- return format(...args);
-}
diff --git a/src/__mocks__/prettier.mjs b/src/__mocks__/prettier.mjs
new file mode 100644
index 00000000..f464af7b
--- /dev/null
+++ b/src/__mocks__/prettier.mjs
@@ -0,0 +1,17 @@
+import { vi } from 'vitest';
+const prettier = await vi.importActual('prettier');
+const { format } = prettier;
+import prettier from 'prettier';
+
+const mockFormatSpy = vi.fn(mockFormat);
+
+function mockFormat(...args) {
+ global.__PRETTIER_ESLINT_TEST_STATE__.prettierPath = __filename;
+ if (mockFormatSpy.throwError) {
+ throw mockFormatSpy.throwError;
+ }
+
+ return format(...args);
+}
+
+export default { ...prettier, format: mockFormatSpy, resolveConfig: vi.fn() };
diff --git a/src/__tests__/index.js b/src/__tests__/index.test.mjs
similarity index 60%
rename from src/__tests__/index.js
rename to src/__tests__/index.test.mjs
index 4c83d71e..5648575f 100644
--- a/src/__tests__/index.js
+++ b/src/__tests__/index.test.mjs
@@ -1,39 +1,68 @@
/* eslint no-console:0, import/default:0 */
-import path from 'path';
-import fsMock from 'fs';
+import path from 'node:path';
+import requireRelative from 'require-relative';
+import { beforeEach, expect, test, vi } from 'vitest';
import stripIndent from 'strip-indent';
-import eslintMock from 'eslint';
-import prettierMock from 'prettier';
-import loglevelMock from 'loglevel-colored-level-prefix';
-import {format, analyze} from '../';
+import eslintMock, {
+ helpers as eslintMockHelpers,
+} from '../__mocks__/eslint.mjs';
+import fsMock from '../__mocks__/fs.mjs';
+import prettierMock from '../__mocks__/prettier.mjs';
+import loglevelMock, {
+ helpers as loglevelMockHelpers,
+} from '../__mocks__/loglevel-colored-level-prefix';
+
+// Mock eslint and prettier at their resolved locations.
+// vitest module mocking is keyed on the string passed to `import()`,
+// so we have to get the on-disk location right.
+// We use `doMock` to prevent their being hoisted above this test's
+// import statements, since they're dynamically imported.
+// https://vitest.dev/api/vi.html#vi-domock
+vi.doMock(requireRelative.resolve('eslint'), () => {
+ return { default: eslintMock };
+});
+
+vi.doMock(requireRelative.resolve('prettier'), () => {
+ return { default: prettierMock };
+});
+
+// loglevel-colored-level-prefix is not a dynamic import
+vi.mock('fs', () => {
+ return { default: fsMock };
+});
+vi.mock('loglevel-colored-level-prefix', () => {
+ return { default: loglevelMock };
+});
-jest.mock('fs');
+import { format, analyze } from '../';
-const {
- mock: { logger }
-} = loglevelMock;
-// loglevelMock.mock.logThings = ['debug']
+const logger = loglevelMockHelpers.getLogger();
-const tests = [
+beforeEach(() => {
+ vi.clearAllMocks();
+ global.__PRETTIER_ESLINT_TEST_STATE__ = {};
+});
+
+const formatTests = [
{
title: 'sanity test',
- input: {
+ options: {
text: defaultInputText(),
- eslintConfig: getESLintConfigWithDefaultRules()
+ eslintConfig: getESLintConfigWithDefaultRules(),
},
- output: defaultOutput()
+ output: defaultOutput(),
},
{
title: 'README example',
- input: {
+ options: {
text: 'const {foo} = bar',
eslintConfig: {
parserOptions: { ecmaVersion: 7 },
- rules: { semi: ['error', 'never'] }
+ rules: { semi: ['error', 'never'] },
},
- prettierOptions: { bracketSpacing: true }
+ prettierOptions: { bracketSpacing: true },
},
- output: 'const { foo } = bar'
+ output: 'const { foo } = bar',
},
{
// this one's actually hard to test now. This test doesn't
@@ -43,230 +72,237 @@ const tests = [
// honestly not sure how to test that prettier fixed
// something that eslint fixed
title: 'with prettierLast: true',
- input: {
+ options: {
text: defaultInputText(),
filePath: path.resolve('./mock/default-config.js'),
- prettierLast: true
+ prettierLast: true,
},
- output: prettierLastOutput()
+ output: prettierLastOutput(),
},
{
title: 'with a filePath and no config',
- input: {
+ options: {
text: defaultInputText(),
- filePath: path.resolve('./mock/default-config.js')
+ filePath: path.resolve('./mock/default-config.js'),
},
- output: defaultOutput()
+ output: defaultOutput(),
},
{
title: 'with a default config and overrides',
- input: {
+ options: {
text: 'const { foo } = bar;',
eslintConfig: {
// Won't be overridden
parserOptions: {
- ecmaVersion: 7
+ ecmaVersion: 7,
},
rules: {
// Will be overridden
semi: ['error', 'always'],
// Won't be overridden
- 'object-curly-spacing': ['error', 'never']
- }
+ 'object-curly-spacing': ['error', 'never'],
+ },
},
- filePath: path.resolve('./mock/default-config.js')
+ filePath: path.resolve('./mock/default-config.js'),
},
- output: 'const {foo} = bar'
+ output: 'const {foo} = bar',
},
{
title: 'with an empty config and fallbacks',
- input: {
+ options: {
text: 'const { foo } = bar;',
eslintConfig: {},
filePath: path.resolve('./mock/default-config.js'),
- fallbackPrettierOptions: { bracketSpacing: false }
+ fallbackPrettierOptions: { bracketSpacing: false },
},
- output: 'const {foo} = bar'
+ output: 'const {foo} = bar',
},
{
title: 'without a filePath and no config',
- input: { text: defaultInputText() },
- output: noopOutput()
+ options: { text: defaultInputText() },
+ output: noopOutput(),
},
{
title: 'inferring bracketSpacing',
- input: {
+ options: {
text: 'var foo = {bar: baz};',
- eslintConfig: { rules: { 'object-curly-spacing': ['error', 'always'] } }
+ eslintConfig: { rules: { 'object-curly-spacing': ['error', 'always'] } },
+ },
+ output: 'var foo = { bar: baz };',
+ },
+ {
+ title: 'inferring bracketSpacing with eslint object-curly-spacing options',
+ options: {
+ text: 'var foo = {bar: {baz: qux}};\nvar fop = {bar: [1, 2, 3]};',
+ eslintConfig: {
+ rules: {
+ 'object-curly-spacing': [
+ 'error',
+ 'always',
+ { objectsInObjects: false, arraysInObjects: false },
+ ],
+ },
+ },
},
- output: 'var foo = { bar: baz };'
+ output: 'var foo = { bar: { baz: qux }};\nvar fop = { bar: [1, 2, 3]};',
},
{
title: 'inferring bracketSpacing with eslint object-curly-spacing options',
- input: {
+ options: {
text: 'var foo = {bar: {baz: qux}};\nvar fop = {bar: [1, 2, 3]};',
eslintConfig: {
rules: {
'object-curly-spacing': [
'error',
'always',
- { objectsInObjects: false, arraysInObjects: false }
- ]
- }
- }
+ { objectsInObjects: false, arraysInObjects: false },
+ ],
+ },
+ },
},
- output: 'var foo = { bar: { baz: qux }};\nvar fop = { bar: [1, 2, 3]};'
+ output: 'var foo = { bar: { baz: qux }};\nvar fop = { bar: [1, 2, 3]};',
},
{
title: 'with a filePath-aware config',
- input: {
+ options: {
text: 'var x = 0;',
eslintConfig: {
rules: { 'no-var': 'error' },
- ignorePattern: 'should-be-ignored'
+ ignorePattern: 'should-be-ignored',
},
- filePath: path.resolve('should-be-ignored.js')
+ filePath: path.resolve('should-be-ignored.js'),
},
- output: 'var x = 0;'
+ output: 'var x = 0;',
},
- // if you have a bug report or something,
- // go ahead and add a test case here
{
+ // if you have a bug report or something,
+ // go ahead and add a test case here
title: 'with code that needs no fixing',
- input: {
+ options: {
text: 'var [foo, { bar }] = window.APP;',
- eslintConfig: { rules: {} }
+ eslintConfig: { rules: {} },
},
- output: 'var [foo, { bar }] = window.APP;'
+ output: 'var [foo, { bar }] = window.APP;',
},
{
title: 'accepts config globals as array',
- input: {
+ options: {
text: defaultInputText(),
- eslintConfig: { globals: ['window:writable'] }
+ eslintConfig: { globals: ['window:writable'] },
},
- output: noopOutput()
+ output: noopOutput(),
},
{
title: 'CSS example',
- input: {
+ options: {
text: '.stop{color:red};',
- filePath: path.resolve('./test.css')
+ filePath: path.resolve('./test.css'),
},
- output: '.stop {\n color: red;\n}'
+ output: '.stop {\n color: red;\n}',
},
{
title: 'LESS example',
- input: {
+ options: {
text: '.stop{color:red};',
- filePath: path.resolve('./test.less')
+ filePath: path.resolve('./test.less'),
},
- output: '.stop {\n color: red;\n}'
+ output: '.stop {\n color: red;\n}',
},
{
title: 'SCSS example',
- input: {
+ options: {
text: '.stop{color:red};',
- filePath: path.resolve('./test.scss')
+ filePath: path.resolve('./test.scss'),
},
- output: '.stop {\n color: red;\n}'
+ output: '.stop {\n color: red;\n}',
},
{
title: 'TypeScript example',
- input: {
+ options: {
text: 'function Foo (this: void) { return this; }',
- filePath: path.resolve('./test.ts')
+ filePath: path.resolve('./test.ts'),
},
- output: 'function Foo(this: void) {\n return this;\n}'
+ output: 'function Foo(this: void) {\n return this;\n}',
},
{
title: 'Vue.js example',
- input: {
+ options: {
eslintConfig: {
rules: {
- 'space-before-function-paren': [2, 'always']
- }
+ 'space-before-function-paren': [2, 'always'],
+ },
},
text: '\n \n\n\n',
- filePath: path.resolve('./test.vue')
+ filePath: path.resolve('./test.vue'),
},
+
output:
- '\n \n\n\n'
+ '\n \n\n\n',
},
{
title: 'Svelte example',
- input: {
+ options: {
prettierOptions: {
- plugins: ['prettier-plugin-svelte'],
- overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }]
- },
+ plugins: ['prettier-plugin-svelte'],
+ overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }],
+ },
text: '\n
test
\n',
- filePath: path.resolve('./test.svelte')
+ filePath: path.resolve('./test.svelte'),
},
+
output:
- '\n\ntest
\n\n'
+ '\n\ntest
\n\n',
},
{
title: 'GraphQL example',
- input: {
+ options: {
text: 'type Query{test: Test}',
- filePath: path.resolve('./test.gql')
+ filePath: path.resolve('./test.gql'),
},
- output: 'type Query {\n test: Test\n}'
+ output: 'type Query {\n test: Test\n}',
},
{
title: 'JSON example',
- input: {
+ options: {
text: '{ "foo": "bar"}',
- filePath: path.resolve('./test.json')
+ filePath: path.resolve('./test.json'),
},
- output: '{ "foo": "bar" }'
+ output: '{ "foo": "bar" }',
},
+
{
title: 'Markdown example',
- input: {
+ options: {
text: '# Foo\n _bar_',
- filePath: path.resolve('./test.md')
+ filePath: path.resolve('./test.md'),
},
- output: '# Foo\n\n_bar_'
+ output: '# Foo\n\n_bar_',
},
+
{
title: 'Test eslintConfig.globals as an object',
- input: {
+ options: {
text: 'var foo = { "bar": "baz"}',
eslintConfig: {
globals: {
- someGlobal: true
- }
- }
+ someGlobal: true,
+ },
+ },
},
- output: 'var foo = { bar: "baz" };'
- }
+ output: 'var foo = { bar: "baz" };',
+ },
];
-beforeEach(() => {
- eslintMock.mock.lintText.mockClear();
- eslintMock.mock.calculateConfigForFile.mockClear();
- prettierMock.format.mockClear();
- prettierMock.resolveConfig.mockClear();
- fsMock.readFileSync.mockClear();
- loglevelMock.mock.clearAll();
- global.__PRETTIER_ESLINT_TEST_STATE__ = {};
-});
-
-tests.forEach(({ title, modifier, input, output }) => {
- let fn = test;
- if (modifier) {
- fn = test[modifier];
- }
- fn(title, async () => {
- input.text = stripIndent(input.text).trim();
- const expected = stripIndent(output).trim();
- const actual = await format(input);
- // adding the newline in the expected because
- // prettier adds a newline to the end of the input
- expect(actual).toBe(`${expected}\n`);
+test.for(formatTests)('format $title', async ({ options, output }) => {
+ // A newline is added to the expected output to account
+ // for prettier's behavior.
+ const expected = `${stripIndent(output).trim()}\n`;
+ const actual = await format({
+ ...options,
+ text: stripIndent(options.text).trim(),
});
+
+ expect(actual).toBe(expected);
});
test('analyze returns the messages', async () => {
@@ -274,29 +310,33 @@ test('analyze returns the messages', async () => {
const result = await analyze({
text,
eslintConfig: {
- rules: { 'no-var': 'error' }
- }
- })
+ rules: { 'no-var': 'error' },
+ },
+ });
expect(result.output).toBe(`${text}\n`);
expect(result.messages).toHaveLength(1);
const msg = result.messages[0];
expect(msg.ruleId).toBe('no-var');
expect(msg.column).toBe(1);
expect(msg.endColumn).toBe(11);
-})
+});
test('failure to fix with eslint throws and logs an error', async () => {
- const { lintText } = eslintMock.mock;
+ const lintText = eslintMockHelpers.getLintTextSpy();
+
const error = new Error('Something happened');
- lintText.throwError = error;
+ lintText.mockImplementationOnce(() => {
+ throw error;
+ });
- await expect(() => format({ text: '' })).rejects.toThrowError(error);
+ await expect(async () => await format({ text: '' })).rejects.toThrowError(
+ error,
+ );
expect(logger.error).toHaveBeenCalledTimes(1);
- lintText.throwError = null;
});
test('logLevel is used to configure the logger', async () => {
- logger.setLevel = jest.fn();
+ logger.setLevel = vi.fn();
await format({ text: '', logLevel: 'silent' });
expect(logger.setLevel).toHaveBeenCalledTimes(1);
expect(logger.setLevel).toHaveBeenCalledWith('silent');
@@ -312,24 +352,24 @@ test('when prettier throws, log to logger.error and throw the error', async () =
});
test('can accept a path to an eslint module and uses that instead.', async () => {
- const eslintPath = path.join(__dirname, '../__mocks__/eslint');
+ const eslintPath = path.join(__dirname, '../__mocks__/eslint.mjs');
await format({ text: '', eslintPath });
- expect(eslintMock.mock.lintText).toHaveBeenCalledTimes(1);
+ expect(eslintMockHelpers.getLintTextSpy()).toHaveBeenCalledTimes(1);
});
test('fails with an error if the eslint module cannot be resolved.', async () => {
const eslintPath = path.join(
__dirname,
- '../__mocks__/non-existent-eslint-module'
+ '../__mocks__/non-existent-eslint-module',
);
await expect(() => format({ text: '', eslintPath })).rejects.toThrowError(
- /non-existent-eslint-module/
+ /non-existent-eslint-module/,
);
expect(logger.error).toHaveBeenCalledTimes(1);
const errorString = expect.stringMatching(
- /trouble getting.*?eslint.*non-existent-eslint-module/
+ /trouble getting.*?eslint.*non-existent-eslint-module/,
);
expect(logger.error).toHaveBeenCalledWith(errorString);
@@ -345,15 +385,15 @@ test('can accept a path to a prettier module and uses that instead.', async () =
test('fails with an error if the prettier module cannot be resolved.', async () => {
const prettierPath = path.join(
__dirname,
- '../__mocks__/non-existent-prettier-module'
+ '../__mocks__/non-existent-prettier-module',
);
await expect(() => format({ text: '', prettierPath })).rejects.toThrowError(
- /non-existent-prettier-module/
+ /non-existent-prettier-module/,
);
expect(logger.error).toHaveBeenCalledTimes(1);
const errorString = expect.stringMatching(
- /trouble getting.*?eslint.*non-existent-prettier-module/
+ /trouble getting.*?eslint.*non-existent-prettier-module/,
);
expect(logger.error).toHaveBeenCalledWith(errorString);
});
@@ -363,12 +403,13 @@ test('resolves to the eslint module relative to the given filePath', async () =>
await format({ text: '', filePath });
const stateObj = {
eslintPath: require.resolve(
- '../../tests/fixtures/paths/node_modules/eslint/index.js'
+ '../../tests/fixtures/paths/node_modules/eslint/index.js',
),
prettierPath: require.resolve(
- '../../tests/fixtures/paths/node_modules/prettier/index.js'
- )
+ '../../tests/fixtures/paths/node_modules/prettier/index.js',
+ ),
};
+ // console.dir({stateObj});
expect(global.__PRETTIER_ESLINT_TEST_STATE__).toMatchObject(stateObj);
});
@@ -377,32 +418,32 @@ test('resolves to the local eslint module', async () => {
await format({ text: '', filePath });
expect(global.__PRETTIER_ESLINT_TEST_STATE__).toMatchObject({
// without Jest's mocking, these would actually resolve to the
- // project modules :) The fact that jest's mocking is being
+ // project modules :) The fact that vitest's mocking is being
// applied is good enough for this test.
- eslintPath: require.resolve('../__mocks__/eslint'),
- prettierPath: require.resolve('../__mocks__/prettier')
+ eslintPath: require.resolve('../__mocks__/eslint.mjs'),
+ prettierPath: require.resolve('../__mocks__/prettier.mjs'),
});
});
test('reads text from fs if filePath is provided but not text', async () => {
- const readFileSyncMockSpy = jest.spyOn(fsMock, 'readFileSync');
-
+ const spy = vi.spyOn(fsMock, 'readFileSync').mockImplementation(() => {
+ return defaultInputText();
+ });
const filePath = '/blah-blah/some-file.js';
await format({ filePath });
- expect(readFileSyncMockSpy).toHaveBeenCalledWith(filePath, 'utf8');
+ expect(spy).toHaveBeenCalledWith(filePath, 'utf8');
});
test('logs error if it cannot read the file from the filePath', async () => {
- const originalMock = fsMock.readFileSync;
- fsMock.readFileSync = jest.fn(() => {
+ const spy = vi.spyOn(fsMock, 'readFileSync').mockImplementationOnce(() => {
throw new Error('some error');
});
+
await expect(() =>
- format({ filePath: '/some-path.js' })
+ format({ filePath: '/some-path.js' }),
).rejects.toThrowError(/some error/);
expect(logger.error).toHaveBeenCalledTimes(1);
- fsMock.readFileSync = originalMock;
});
test('calls prettier.resolveConfig with the file path', async () => {
@@ -410,7 +451,7 @@ test('calls prettier.resolveConfig with the file path', async () => {
await format({
filePath,
text: defaultInputText(),
- eslintConfig: getESLintConfigWithDefaultRules()
+ eslintConfig: getESLintConfigWithDefaultRules(),
});
expect(prettierMock.resolveConfig).toHaveBeenCalledTimes(1);
expect(prettierMock.resolveConfig).toHaveBeenCalledWith(filePath);
@@ -425,7 +466,7 @@ test('does not raise an error if prettier.resolveConfig is not defined', async (
return format({
filePath,
text: defaultInputText(),
- eslintConfig: getESLintConfigWithDefaultRules()
+ eslintConfig: getESLintConfigWithDefaultRules(),
});
}
@@ -440,7 +481,7 @@ test('logs if there is a problem making the CLIEngine', async () => {
throw error;
});
await expect(() => format({ text: '' })).rejects.toThrowError(error);
- eslintMock.ESLint.mockReset();
+ eslintMock.ESLint.mockRestore();
expect(logger.error).toHaveBeenCalledTimes(1);
});
@@ -459,12 +500,12 @@ function getESLintConfigWithDefaultRules(overrides) {
objects: 'always-multiline',
imports: 'always-multiline',
exports: 'always-multiline',
- functions: 'always-multiline'
- }
+ functions: 'always-multiline',
+ },
],
'arrow-parens': [2, 'as-needed'],
- ...overrides
- }
+ ...overrides,
+ },
};
}
diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js
deleted file mode 100644
index 62f1fa69..00000000
--- a/src/__tests__/utils.js
+++ /dev/null
@@ -1,308 +0,0 @@
-import path from 'path';
-import { getOptionsForFormatting } from '../utils';
-
-const getPrettierOptionsFromESLintRulesTests = [
- {
- rules: {
- 'max-len': [2, 120, 2],
- indent: [2, 2, { SwitchCase: 1 }],
- quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }],
- 'comma-dangle': [
- 2,
- {
- arrays: 'always-multiline',
- objects: 'always-multiline',
- imports: 'always-multiline',
- exports: 'always-multiline',
- functions: 'always-multiline'
- }
- ],
- 'object-curly-spacing': [2, 'never']
- },
- options: {
- printWidth: 120,
- tabWidth: 2,
- singleQuote: true,
- trailingComma: 'all',
- bracketSpacing: false
- }
- },
- {
- rules: { 'object-curly-spacing': [2, 'always'] },
- options: { bracketSpacing: true }
- },
- {
- rules: { 'object-curly-spacing': [2, 'never'] },
- options: { bracketSpacing: false }
- },
- { rules: { 'max-len': 2 }, options: {} },
- {
- rules: { 'comma-dangle': [2, 'never'] },
- options: { trailingComma: 'none' }
- },
- {
- rules: { 'comma-dangle': [2, 'always'] },
- options: { trailingComma: 'es5' }
- },
- {
- rules: {
- 'comma-dangle': [
- 2,
- {
- arrays: 'always-multiline',
- objects: 'always-multiline',
- imports: 'always-multiline',
- exports: 'always-multiline',
- functions: 'always-multiline'
- }
- ]
- },
- options: { trailingComma: 'all' }
- },
- {
- rules: {
- 'comma-dangle': [
- 2,
- {
- arrays: 'always-multiline',
- objects: 'always-multiline',
- imports: 'always-multiline',
- exports: 'always-multiline',
- functions: 'never'
- }
- ]
- },
- options: { trailingComma: 'es5' }
- },
- {
- rules: {
- 'comma-dangle': [
- 2,
- {
- arrays: 'never',
- objects: 'never',
- imports: 'never',
- exports: 'never',
- functions: 'never'
- }
- ]
- },
- options: { trailingComma: 'none' }
- },
- {
- rules: { 'max-len': ['error', { code: 120 }] },
- options: { printWidth: 120 }
- },
- { rules: { quotes: [2, 'double'] }, options: { singleQuote: false } },
- { rules: { quotes: [2, 'backtick'] }, options: { singleQuote: false } },
- {
- rules: {
- 'comma-dangle': [
- 2,
- {
- imports: 'never',
- exports: 'never'
- }
- ]
- },
- options: { trailingComma: 'none' }
- },
- {
- rules: { 'comma-dangle': [2, 'always-multiline'] },
- options: { trailingComma: 'es5' }
- },
- {
- rules: {},
- options: { bracketSameLine: true },
- fallbackPrettierOptions: { bracketSameLine: true }
- },
- {
- rules: { 'react/jsx-closing-bracket-location': [2, 'after-props'] },
- options: { bracketSameLine: true }
- },
- {
- rules: { 'react/jsx-closing-bracket-location': [2, 'tag-aligned'] },
- options: { bracketSameLine: false }
- },
- {
- rules: {
- 'react/jsx-closing-bracket-location': [
- 2,
- {
- nonEmpty: 'after-props'
- }
- ]
- },
- options: { bracketSameLine: true }
- },
- {
- rules: {
- 'arrow-parens': [2, 'always']
- },
- options: { arrowParens: 'always' }
- },
- {
- rules: {
- 'arrow-parens': [2, 'as-needed']
- },
- options: { arrowParens: 'avoid' }
- },
- {
- rules: {
- 'prettier/prettier': [2, { singleQuote: false }],
- quotes: [2, 'single']
- },
- options: {
- singleQuote: false
- }
- },
-
- // If an ESLint rule is disabled fall back to prettier defaults.
- { rules: { 'max-len': [0, { code: 120 }] }, options: {} },
- { rules: { quotes: ['off', 'single'] }, options: {} },
- { rules: { quotes: ['off', 'backtick'] }, options: {} },
- { rules: { semi: 'off' }, options: {} },
- { rules: { semi: ['off', 'never'] }, options: {} },
- { rules: { semi: ['warn', 'always'] }, options: {} },
- { rules: { semi: ['warn', 'always'] }, options: { semi: true } },
- { rules: { semi: ['error', 'never'] }, options: { semi: false } },
- { rules: { semi: [2, 'never'] }, options: { semi: false } },
- { rules: { semi: [2, 'never'] }, options: { semi: false } },
- { rules: { indent: 'off' }, options: {} },
- { rules: { indent: ['off', 'tab'] }, options: {} },
- { rules: { indent: ['warn', 2] }, options: { tabWidth: 2 } },
- { rules: { indent: ['warn', 4] }, options: { tabWidth: 4 } },
- { rules: { indent: ['error', 'tab'] }, options: { useTabs: true } },
- { rules: { indent: [2, 'tab'] }, options: { useTabs: true } },
- { rules: { 'react/jsx-closing-bracket-location': [0] }, options: {} },
- { rules: { 'arrow-parens': [0] }, options: {} }
-];
-
-const eslintPath = path.join(__dirname, '../__mocks__/eslint');
-
-beforeEach(() => {
- global.__PRETTIER_ESLINT_TEST_STATE__ = {};
-});
-
-getPrettierOptionsFromESLintRulesTests.forEach(
- ({ rules, options, prettierOptions, fallbackPrettierOptions }, index) => {
- test(`getPrettierOptionsFromESLintRulesTests ${index}`, () => {
- const { prettier } = getOptionsForFormatting(
- { rules },
- prettierOptions,
- fallbackPrettierOptions
- );
- expect(prettier).toMatchObject(options);
- });
- }
-);
-
-test('if prettierOptions are provided, those are preferred', () => {
- const { prettier } = getOptionsForFormatting(
- { rules: { quotes: [2, 'single'] } },
- {
- singleQuote: false
- },
- undefined,
- eslintPath
- );
- expect(prettier).toMatchObject({ singleQuote: false });
-});
-
-// eslint-disable-next-line max-len
-test('if fallbacks are provided, those are preferred over disabled eslint rules', () => {
- const { prettier } = getOptionsForFormatting(
- {
- rules: {
- quotes: [0]
- }
- },
- {},
- {
- singleQuote: true
- }
- );
- expect(prettier).toMatchObject({ singleQuote: true });
-});
-
-test('if fallbacks are provided, those are used if not found in eslint', () => {
- const { prettier } = getOptionsForFormatting(
- { rules: {} },
- undefined,
- {
- singleQuote: false
- }
- );
- expect(prettier).toMatchObject({ singleQuote: false });
-});
-
-test('eslint max-len.tabWidth value should be used for tabWidth when tabs are used', () => {
- const { prettier } = getOptionsForFormatting(
- {
- rules: {
- indent: ['error', 'tab'],
- 'max-len': [
- 2,
- {
- tabWidth: 4
- }
- ]
- }
- },
- undefined,
- undefined
- );
-
- expect(prettier).toMatchObject({
- tabWidth: 4,
- useTabs: true
- });
-});
-
-test('eslint config has only necessary properties', () => {
- const { eslint } = getOptionsForFormatting(
- {
- globals: ['window:false'],
- rules: { 'no-with': 'error', quotes: [2, 'single'] }
- },
- undefined,
- undefined
- );
- expect(eslint).toMatchObject({
- fix: true,
- useEslintrc: false,
- rules: { quotes: [2, 'single'] }
- });
-});
-
-test('useEslintrc is set to the given config value', () => {
- const { eslint } = getOptionsForFormatting(
- { useEslintrc: true, rules: {} },
- undefined,
- undefined
- );
- expect(eslint).toMatchObject({ fix: true, useEslintrc: true });
-});
-
-test('Turn off unfixable rules', () => {
- const { eslint } = getOptionsForFormatting(
- {
- rules: {
- 'global-require': 'error',
- quotes: ['error', 'double']
- }
- },
- undefined,
- undefined
- );
-
- expect(eslint).toMatchObject({
- rules: {
- 'global-require': ['off'],
- quotes: ['error', 'double']
- },
- fix: true,
- globals: {},
- useEslintrc: false
- });
-});
diff --git a/src/__tests__/utils.test.mjs b/src/__tests__/utils.test.mjs
new file mode 100644
index 00000000..7e41da88
--- /dev/null
+++ b/src/__tests__/utils.test.mjs
@@ -0,0 +1,399 @@
+import path from 'node:path';
+import { beforeEach, expect, test } from 'vitest';
+import { getOptionsForFormatting } from '../utils.mjs';
+
+const getPrettierOptionsFromESLintRulesTests = [
+ {
+ title: 'all rules interaction',
+ rules: {
+ title: 'all rules interaction',
+ 'max-len': [2, 120, 2],
+ indent: [2, 2, { SwitchCase: 1 }],
+ quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }],
+ 'comma-dangle': [
+ 2,
+ {
+ arrays: 'always-multiline',
+ objects: 'always-multiline',
+ imports: 'always-multiline',
+ exports: 'always-multiline',
+ functions: 'always-multiline',
+ },
+ ],
+ 'object-curly-spacing': [2, 'never'],
+ },
+ options: {
+ printWidth: 120,
+ tabWidth: 2,
+ singleQuote: true,
+ trailingComma: 'all',
+ bracketSpacing: false,
+ },
+ },
+ {
+ title: 'object-curly-spacing, always',
+ rules: { 'object-curly-spacing': [2, 'always'] },
+ options: { bracketSpacing: true },
+ },
+ {
+ title: 'object-curly-spacing, never',
+ rules: { 'object-curly-spacing': [2, 'never'] },
+ options: { bracketSpacing: false },
+ },
+ {
+ title: 'max-len',
+ rules: { 'max-len': 2 },
+ options: {},
+ },
+ {
+ title: 'comma-dangle, never',
+ rules: { 'comma-dangle': [2, 'never'] },
+ options: { trailingComma: 'none' },
+ },
+ {
+ title: 'comma-dangle, always',
+ rules: { 'comma-dangle': [2, 'always'] },
+ options: { trailingComma: 'es5' },
+ },
+ {
+ title: 'comma-dangle, always-multiline',
+ rules: {
+ 'comma-dangle': [
+ 2,
+ {
+ arrays: 'always-multiline',
+ objects: 'always-multiline',
+ imports: 'always-multiline',
+ exports: 'always-multiline',
+ functions: 'always-multiline',
+ },
+ ],
+ },
+ options: { trailingComma: 'all' },
+ },
+ {
+ title: 'comma-dangle, always-multiline except functions',
+ rules: {
+ 'comma-dangle': [
+ 2,
+ {
+ arrays: 'always-multiline',
+ objects: 'always-multiline',
+ imports: 'always-multiline',
+ exports: 'always-multiline',
+ functions: 'never',
+ },
+ ],
+ },
+ options: { trailingComma: 'es5' },
+ },
+ {
+ title: 'comma-dangle, never',
+ rules: {
+ 'comma-dangle': [
+ 2,
+ {
+ arrays: 'never',
+ objects: 'never',
+ imports: 'never',
+ exports: 'never',
+ functions: 'never',
+ },
+ ],
+ },
+ options: { trailingComma: 'none' },
+ },
+ {
+ title: 'max-len, code 120',
+ rules: { 'max-len': ['error', { code: 120 }] },
+ options: { printWidth: 120 },
+ },
+ {
+ title: 'quotes, double',
+ rules: { quotes: [2, 'double'] },
+ options: { singleQuote: false },
+ },
+ {
+ title: 'quotes, backtick',
+ rules: { quotes: [2, 'backtick'] },
+ options: { singleQuote: false },
+ },
+ {
+ title: 'comma-dangle, imports never, exports never',
+ rules: {
+ 'comma-dangle': [
+ 2,
+ {
+ imports: 'never',
+ exports: 'never',
+ },
+ ],
+ },
+ options: { trailingComma: 'none' },
+ },
+ {
+ title: 'comma-dangle, always-multiline',
+ rules: { 'comma-dangle': [2, 'always-multiline'] },
+ options: { trailingComma: 'es5' },
+ },
+ {
+ title: 'no rules',
+ rules: {},
+ options: { bracketSameLine: true },
+ fallbackPrettierOptions: { bracketSameLine: true },
+ },
+ {
+ title: 'react/jsx-closing-bracket-location, after-props',
+ rules: { 'react/jsx-closing-bracket-location': [2, 'after-props'] },
+ options: { bracketSameLine: true },
+ },
+ {
+ title: 'react/jsx-closing-bracket-location, tag-aligned',
+ rules: { 'react/jsx-closing-bracket-location': [2, 'tag-aligned'] },
+ options: { bracketSameLine: false },
+ },
+ {
+ title: 'react/jsx-closing-bracket-location, after-props',
+ rules: {
+ 'react/jsx-closing-bracket-location': [
+ 2,
+ {
+ nonEmpty: 'after-props',
+ },
+ ],
+ },
+ options: { bracketSameLine: true },
+ },
+ {
+ title: 'arrow-parens, always',
+ rules: {
+ 'arrow-parens': [2, 'always'],
+ },
+ options: { arrowParens: 'always' },
+ },
+ {
+ title: 'arrow-parens as-needed',
+ rules: {
+ 'arrow-parens': [2, 'as-needed'],
+ },
+ options: { arrowParens: 'avoid' },
+ },
+ {
+ title: 'prettier/prettier & quotes conflict',
+ rules: {
+ 'prettier/prettier': [2, { singleQuote: false }],
+ quotes: [2, 'single'],
+ },
+ options: {
+ singleQuote: false,
+ },
+ },
+
+ // If an ESLint rule is disabled fall back to prettier defaults.
+ {
+ title: 'max-len off, prettier defaults',
+ rules: { 'max-len': [0, { code: 120 }] },
+ options: {},
+ },
+ {
+ title: 'quotes off, single, prettier defaults',
+ rules: { quotes: ['off', 'single'] },
+ options: {},
+ },
+ {
+ title: 'quotes off, backtick, prettier defaults',
+ rules: { quotes: ['off', 'backtick'] },
+ options: {},
+ },
+ {
+ title: 'semi off, prettier defaults',
+ rules: { semi: 'off' },
+ options: {},
+ },
+ {
+ title: 'semi off, never, prettier defaults',
+ rules: { semi: ['off', 'never'] },
+ options: {},
+ },
+ {
+ title: 'semi warn, always, prettier defaults',
+ rules: { semi: ['warn', 'always'] },
+ options: { semi: true },
+ },
+ {
+ title: 'semi error, never, prettier defaults',
+ rules: { semi: ['error', 'never'] },
+ options: { semi: false },
+ },
+ {
+ title: 'semi error, never (2), prettier defaults',
+ rules: { semi: [2, 'never'] },
+ options: { semi: false },
+ },
+ {
+ title: 'indent off, prettier defaults',
+ rules: { indent: 'off' },
+ options: {},
+ },
+ {
+ title: 'indent off, tab, prettier defaults',
+ rules: { indent: ['off', 'tab'] },
+ options: {},
+ },
+ {
+ title: 'indent warn, 2, prettier defaults',
+ rules: { indent: ['warn', 2] },
+ options: { tabWidth: 2 },
+ },
+ {
+ title: 'indent warn, 4, prettier defaults',
+ rules: { indent: ['warn', 4] },
+ options: { tabWidth: 4 },
+ },
+ {
+ title: 'indent error, tab, prettier defaults',
+ rules: { indent: ['error', 'tab'] },
+ options: { useTabs: true },
+ },
+ {
+ title: 'indent error (2), tab, prettier defaults',
+ rules: { indent: [2, 'tab'] },
+ options: { useTabs: true },
+ },
+ {
+ title: 'react/jsx-closing-bracket-location off, prettier defaults',
+ rules: { 'react/jsx-closing-bracket-location': [0] },
+ options: {},
+ },
+ {
+ title: 'arrow-parents off, prettier defaults',
+ rules: { 'arrow-parens': [0] },
+ options: {},
+ },
+];
+
+const eslintPath = path.join(__dirname, '../__mocks__/eslint');
+
+beforeEach(() => {
+ global.__PRETTIER_ESLINT_TEST_STATE__ = {};
+});
+
+test.for(getPrettierOptionsFromESLintRulesTests)(
+ 'getPrettierOptionsFromESLintRules $title',
+ ({ rules, options, prettierOptions, fallbackPrettierOptions }) => {
+ const { prettier } = getOptionsForFormatting(
+ { rules },
+ prettierOptions,
+ fallbackPrettierOptions,
+ );
+
+ expect(prettier).toStrictEqual(options);
+ },
+);
+
+test('if prettierOptions are provided, those are preferred', () => {
+ const { prettier } = getOptionsForFormatting(
+ { rules: { quotes: [2, 'single'] } },
+ {
+ singleQuote: false,
+ },
+ undefined,
+ eslintPath,
+ );
+ expect(prettier).toMatchObject({ singleQuote: false });
+});
+
+// eslint-disable-next-line max-len
+test('if fallbacks are provided, those are preferred over disabled eslint rules', () => {
+ const { prettier } = getOptionsForFormatting(
+ {
+ rules: {
+ quotes: [0],
+ },
+ },
+ {},
+ {
+ singleQuote: true,
+ },
+ );
+ expect(prettier).toMatchObject({ singleQuote: true });
+});
+
+test('if fallbacks are provided, those are used if not found in eslint', () => {
+ const { prettier } = getOptionsForFormatting({ rules: {} }, undefined, {
+ singleQuote: false,
+ });
+ expect(prettier).toMatchObject({ singleQuote: false });
+});
+
+test('eslint max-len.tabWidth value should be used for tabWidth when tabs are used', () => {
+ const { prettier } = getOptionsForFormatting(
+ {
+ rules: {
+ indent: ['error', 'tab'],
+ 'max-len': [
+ 2,
+ {
+ tabWidth: 4,
+ },
+ ],
+ },
+ },
+ undefined,
+ undefined,
+ );
+
+ expect(prettier).toMatchObject({
+ tabWidth: 4,
+ useTabs: true,
+ });
+});
+
+test('eslint config has only necessary properties', () => {
+ const { eslint } = getOptionsForFormatting(
+ {
+ globals: ['window:false'],
+ rules: { 'no-with': 'error', quotes: [2, 'single'] },
+ },
+ undefined,
+ undefined,
+ );
+ expect(eslint).toStrictEqual({
+ fix: true,
+ useEslintrc: false,
+ globals: ['window:false'],
+ rules: { ['no-with']: 'error', quotes: [2, 'single'] },
+ });
+});
+
+test('useEslintrc is set to the given config value', () => {
+ const { eslint } = getOptionsForFormatting(
+ { useEslintrc: true, rules: {} },
+ undefined,
+ undefined,
+ );
+ expect(eslint).toMatchObject({ fix: true, useEslintrc: true });
+});
+
+test('Turn off unfixable rules', () => {
+ const { eslint } = getOptionsForFormatting(
+ {
+ rules: {
+ 'global-require': ['off'],
+ quotes: ['error', 'double'],
+ },
+ },
+ undefined,
+ undefined,
+ );
+
+ expect(eslint).toMatchObject({
+ rules: {
+ 'global-require': ['off'],
+ quotes: ['error', 'double'],
+ },
+ fix: true,
+ globals: {},
+ useEslintrc: false,
+ });
+});
diff --git a/src/index.js b/src/index.mjs
similarity index 74%
rename from src/index.js
rename to src/index.mjs
index 4661b274..ef63fa3d 100644
--- a/src/index.js
+++ b/src/index.mjs
@@ -2,20 +2,17 @@
/* eslint complexity: [1, 13] */
import fs from 'fs';
-import path from 'path';
+import path from 'node:path';
import requireRelative from 'require-relative';
import prettyFormat from 'pretty-format';
import { oneLine, stripIndent } from 'common-tags';
import indentString from 'indent-string';
import getLogger from 'loglevel-colored-level-prefix';
import merge from 'lodash.merge';
-import { getESLint, getOptionsForFormatting, requireModule } from './utils';
+import { getESLint, getOptionsForFormatting, importModule } from './utils.mjs';
const logger = getLogger({ prefix: 'prettier-eslint' });
-// CommonJS + ES6 modules... is it worth it? Probably not...
-module.exports = format;
-
/**
* Formats the text with prettier and then eslint based on the given options
* @param {String} options.filePath - the path of the file being formatted
@@ -40,8 +37,8 @@ module.exports = format;
* @param {Boolean} options.prettierLast - Run Prettier Last
* @return {Promise} - the formatted string
*/
-async function format(options) {
- const {output} = await analyze(options);
+export async function format(options) {
+ const { output } = await analyze(options);
return output;
}
@@ -52,12 +49,13 @@ async function format(options) {
* properties `output` giving the formatted code and `messages` giving
* any error messages generated in the analysis.
* @param {Object} identical to options parameter of `format`
- * @returns {Promise