diff --git a/__tests__/categories/js/eslint/eslint.entrypoint.test.ts b/__tests__/categories/js/eslint/eslint.entrypoint.test.ts index e376c35..309123b 100644 --- a/__tests__/categories/js/eslint/eslint.entrypoint.test.ts +++ b/__tests__/categories/js/eslint/eslint.entrypoint.test.ts @@ -2,6 +2,7 @@ import * as fs from 'src/utils/fs'; import * as npm from 'src/utils/npm'; import * as mutation from 'src/utils/mutation'; import * as config from 'src/categories/js/eslint/eslint.config'; +import * as javascript from 'src/utils/javascript'; import { eslint } from 'src/categories/js/eslint/eslint.entrypoint'; import { createConfig } from './eslint-test.utils'; import { Config } from 'src/categories/js/eslint/config/config.interface'; @@ -18,6 +19,14 @@ const mutationMocked = jest.mocked(mutation); jest.mock('src/categories/js/eslint/eslint.config'); const configMocked = jest.mocked(config); +// we need to mock prettier because it doesn't work with jest +// TypeError: A dynamic import callback was invoked without --experimental-vm-modules +// https://github.com/prettier/prettier/issues/15769 +jest.mock('src/utils/javascript', () => ({ + format: jest.fn().mockImplementation(async (config) => config), +})); +const javascriptMocked = jest.mocked(javascript); + beforeEach(() => { jest.clearAllMocks(); }); @@ -57,15 +66,12 @@ describe('eslint', () => { configMocked.getConfig.mockReturnValueOnce(config); const configFile = `export default [ - { - ignores: [ - '__test__' - ], - } +{ignores: ["__test__"]} ];`; await eslint(); + expect(javascriptMocked.format).toHaveBeenCalledWith(configFile); expect(fsMocked.addFileToRoot).toHaveBeenCalledWith('eslint.config.mjs', configFile); }); @@ -74,12 +80,12 @@ describe('eslint', () => { configMocked.getConfig.mockReturnValueOnce(config); const configFile = `export default [ - {\n - } +{} ];`; await eslint(); + expect(javascriptMocked.format).toHaveBeenCalledWith(configFile); expect(fsMocked.addFileToRoot).toHaveBeenCalledWith('eslint.config.mjs', configFile); }); @@ -94,16 +100,12 @@ describe('eslint', () => { configMocked.getConfig.mockReturnValueOnce(config); const configFile = `export default [ - { - languageOptions: { - globals: {\n - } - }, - } +{languageOptions: {}} ];`; await eslint(); + expect(javascriptMocked.format).toHaveBeenCalledWith(configFile); expect(fsMocked.addFileToRoot).toHaveBeenCalledWith('eslint.config.mjs', configFile); }); @@ -114,14 +116,14 @@ describe('eslint', () => { configMocked.getConfig.mockReturnValueOnce(config); const configFile = `export default [ - __test1__, - __test2__, - {\n - } +__test1__, +__test2__, +{} ];`; await eslint(); + expect(javascriptMocked.format).toHaveBeenCalledWith(configFile); expect(fsMocked.addFileToRoot).toHaveBeenCalledWith('eslint.config.mjs', configFile); }); @@ -149,32 +151,14 @@ describe('eslint', () => { configMocked.getConfig.mockReturnValueOnce(config); const configFile = `// @ts-check -import globals from "globals";\n +import globals from "globals"; export default tseslint.config( - { - files: ['src/**/*.ts'], - ignores: [ - 'node_modules' - ], - languageOptions: { - globals: { - ...globals.window - }, - parserOptions: { - ecmaVersion: 2020 - } - }, - plugins: { - '@typescript-eslint': eslintPluginTs - }, - rules: { - 'no-console': 'warn' - }, - } +{files: ["src/**/*.ts"],ignores: ["node_modules"],languageOptions: {globals: {...globals.window},parserOptions: {"ecmaVersion":2020}},plugins: {'@typescript-eslint': eslintPluginTs},rules: {"no-console":"warn"}} );`; await eslint(); + expect(javascriptMocked.format).toHaveBeenCalledWith(configFile); expect(fsMocked.addFileToRoot).toHaveBeenCalledWith('eslint.config.mjs', configFile); }); diff --git a/__tests__/setup-after-env.ts b/__tests__/setup-after-env.ts new file mode 100644 index 0000000..723c181 --- /dev/null +++ b/__tests__/setup-after-env.ts @@ -0,0 +1,4 @@ +// we need to mock prettier in all tests because it doesn't work with jest +// TypeError: A dynamic import callback was invoked without --experimental-vm-modules +// https://github.com/prettier/prettier/issues/15769 +jest.mock('prettier', () => jest.fn()); diff --git a/__tests__/utils/javascript.test.ts b/__tests__/utils/javascript.test.ts new file mode 100644 index 0000000..50d79bd --- /dev/null +++ b/__tests__/utils/javascript.test.ts @@ -0,0 +1,31 @@ +import prettier from 'prettier'; +import { getConfig } from 'src/categories/js/prettier/prettier.config'; +import { defaultConfig } from 'src/categories/js/prettier/config/default.config'; +import { format } from 'src/utils/javascript'; + +// we need to mock prettier because it doesn't work with jest +// TypeError: A dynamic import callback was invoked without --experimental-vm-modules +// https://github.com/prettier/prettier/issues/15769 +jest.mock('prettier', () => ({ + format: jest.fn(), +})); +const prettierMocked = jest.mocked(prettier); + +jest.mock('src/categories/js/prettier/prettier.config', () => ({ getConfig: jest.fn() })); +const getConfigMocked = jest.mocked(getConfig); + +describe('format', () => { + test('formats input with default config', async () => { + const input = `const foo = "bar"`; + const formatted = `const foo = 'bar';`; + + getConfigMocked.mockImplementation(() => ({ config: { printWidth: 90 } }) as typeof defaultConfig); + prettierMocked.format.mockResolvedValueOnce(formatted); + + const actual = await format(input); + const expected = formatted; + + expect(actual).toBe(expected); + expect(prettierMocked.format).toHaveBeenCalledWith(input, { parser: 'typescript', printWidth: 90 }); + }); +}); diff --git a/jest.config.cjs b/jest.config.cjs index d3db317..5e3c946 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -9,7 +9,8 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', moduleDirectories: ['', 'node_modules'], - testRegex: '.*\.(spec|test)\.ts$', + testRegex: '.*.(spec|test).ts$', collectCoverageFrom: ['src/**/*.ts'], passWithNoTests: true, + setupFilesAfterEnv: ['./__tests__/setup-after-env.ts'], }; diff --git a/package-lock.json b/package-lock.json index d4d286c..3945d83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "dependencies": { "inquirer": "8.2.4", - "ora": "5.4.1" + "ora": "5.4.1", + "prettier": "^3.3.3" }, "bin": { "cli": "bin/cli.js" @@ -30,7 +31,6 @@ "husky": "^9.1.6", "jest": "^29.7.0", "lint-staged": "^15.2.10", - "prettier": "^3.3.3", "rimraf": "^6.0.1", "rollup": "^3.29.5", "rollup-plugin-typescript2": "^0.36.0", @@ -7697,7 +7697,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -14915,8 +14914,7 @@ "prettier": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==" }, "prettier-linter-helpers": { "version": "1.0.0", diff --git a/package.json b/package.json index 6566b00..1a5babe 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,6 @@ "husky": "^9.1.6", "jest": "^29.7.0", "lint-staged": "^15.2.10", - "prettier": "^3.3.3", "rimraf": "^6.0.1", "rollup": "^3.29.5", "rollup-plugin-typescript2": "^0.36.0", @@ -83,7 +82,8 @@ }, "dependencies": { "inquirer": "8.2.4", - "ora": "5.4.1" + "ora": "5.4.1", + "prettier": "^3.3.3" }, "lint-staged": { "*.ts": [ diff --git a/src/categories/js/eslint/eslint.entrypoint.ts b/src/categories/js/eslint/eslint.entrypoint.ts index af57aaf..396d692 100644 --- a/src/categories/js/eslint/eslint.entrypoint.ts +++ b/src/categories/js/eslint/eslint.entrypoint.ts @@ -4,55 +4,50 @@ import { addScripts, installDevelopmentDependencies } from 'src/utils/npm'; import { CONFIG_FILE_NAME, PACKAGE_NAME } from './eslint.const'; import { applyMutations } from 'src/utils/mutation'; import { Config } from './config/config.interface'; +import { format } from 'src/utils/javascript'; -const buildConfig = (config: Config) => { - const start = config.typescript ? `// @ts-check\n` : ''; +const optional = (value: T | undefined, map: (value: T) => string) => (value ? map(value) : ''); + +export const buildConfig = (config: Config) => { + const start = optional(config.typescript, () => '// @ts-check'); + + const imports = `${config.imports.map((item) => `${item};`).join('\n')}\n`.trim(); const exportStart = config.typescript ? `export default tseslint.config(` : `export default [`; const exportEnd = config.typescript ? `);` : `];`; - const imports = `${start}${config.imports.join(';\n')}${config.imports.length ? ';\n' : ''}`; - const configs = config.configs.map((item) => ` ${item},`).join('\n'); - - const files = config.eslintConfig.files - ? ` files: ${JSON.stringify(config.eslintConfig.files).replace(/"/gim, `'`)},` - : ''; - const ignores = config.eslintConfig.ignores - ? ` ignores: [\n${config.eslintConfig.ignores.map((item) => ` '${item}'`).join(',\n')}\n ],` - : ''; - - const globals = config.eslintConfig.languageOptions?.globals - ? config.eslintConfig.languageOptions.globals.map((item) => ` ...globals.${item}`).join(',\n') - : ''; - const parserOptions = config.eslintConfig.languageOptions?.parserOptions - ? ` parserOptions: {\n${Object.entries(config.eslintConfig.languageOptions.parserOptions) - .map(([key, value]) => ` ${key}: ${value}`) - .join(',\n')}\n }` - : ''; - - const plugins = config.eslintConfig.plugins - ? ` plugins: { ${Object.entries(config.eslintConfig.plugins) - .map(([key, value]) => `\n '${key}': ${value}`) - .join(',')}\n },` - : ''; - const rules = config.eslintConfig.rules - ? ` rules: ${JSON.stringify(config.eslintConfig.rules, null, 2) - .replace(/"/gim, "'") - .replace(/\n {2}/gim, '\n' + ' '.repeat(6)) - .replace('\n}', '\n }')},` - : ''; - - const languageOptions = config.eslintConfig.languageOptions - ? ` languageOptions: { - globals: { -${globals} - }${parserOptions ? `,\n${parserOptions}` : ''} - },` - : ''; - - const mainConfig = ` {\n${[files, ignores, languageOptions, plugins, rules].filter(Boolean).join('\n')}\n }`; - - return [imports, exportStart, configs, mainConfig, exportEnd].filter(Boolean).join('\n'); + const configs = config.configs.map((item) => `${item},`).join('\n'); + + const files = optional(config.eslintConfig.files, (value) => `files: ${JSON.stringify(value)}`); + const ignores = optional(config.eslintConfig.ignores, (value) => `ignores: ${JSON.stringify(value)}`); + + const plugins = optional( + config.eslintConfig.plugins, + (values) => + `plugins: {${Object.entries(values) + .map(([key, value]) => `'${key}': ${value}`) + .join(',')}}`, + ); + + const rules = optional(config.eslintConfig.rules, (value) => `rules: ${JSON.stringify(value)}`); + + const globals = optional( + config.eslintConfig.languageOptions?.globals, + (value) => `globals: {${value.map((item) => `...globals.${item}`).join(',')}}`, + ); + const parserOptions = optional( + config.eslintConfig.languageOptions?.parserOptions, + (value) => `parserOptions: ${JSON.stringify(value)}`, + ); + + const languageOptions = optional( + config.eslintConfig.languageOptions, + () => `languageOptions: {${[globals, parserOptions].filter(Boolean).join(',')}}`, + ); + + const mainConfig = `{${[files, ignores, languageOptions, plugins, rules].filter(Boolean).join(',')}}`; + + return [start, imports, exportStart, configs, mainConfig, exportEnd].filter(Boolean).join('\n'); }; export const eslint = async () => { @@ -63,7 +58,7 @@ export const eslint = async () => { await installDevelopmentDependencies(PACKAGE_NAME, ...dependencies); - const eslintConfig = buildConfig(config); + const eslintConfig = await format(buildConfig(config)); await addFileToRoot(CONFIG_FILE_NAME, eslintConfig); await addScripts(...scripts); diff --git a/src/utils/javascript.ts b/src/utils/javascript.ts new file mode 100644 index 0000000..2109687 --- /dev/null +++ b/src/utils/javascript.ts @@ -0,0 +1,8 @@ +import prettier from 'prettier'; +import { getConfig } from 'src/categories/js/prettier/prettier.config'; + +export const format = async (input: string) => { + const { config } = getConfig(); + + return await prettier.format(input, { parser: 'typescript', ...config } as prettier.Options); +};