diff --git a/package.json b/package.json index d3b9d92..7f46941 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "type": "module", - "version": "1.1.13", + "version": "1.1.14", "name": "@mp281x/shared-config", "publishConfig": { "registry": "https://npm.pkg.github.com/@mp281x" }, @@ -34,9 +34,10 @@ "eslint": "9.2.0", "@eslint/js": "9.2.0", "typescript-eslint": "7.8.0", - "eslint-config-prettier": "9.1.0", + "eslint-plugin-svelte": "2.39.0", "eslint-plugin-unicorn": "53.0.0", - "eslint-plugin-svelte": "2.39.0" + "eslint-config-prettier": "9.1.0", + "eslint-plugin-perfectionist": "2.10.0" }, "peerDependencies": { diff --git a/src/eslint.ts b/src/eslint.ts index e35f41e..1f64630 100644 --- a/src/eslint.ts +++ b/src/eslint.ts @@ -1,6 +1,7 @@ +/* eslint-disable perfectionist/sort-imports */ /* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ // @ts-expect-error: no type definitions import eslint from '@eslint/js' @@ -11,6 +12,8 @@ import type { ConfigWithExtends } from 'typescript-eslint' import prettier from 'eslint-config-prettier' // @ts-expect-error: no type definitions import unicorn from 'eslint-plugin-unicorn' +// @ts-expect-error: no type definitions +import perfectionist from 'eslint-plugin-perfectionist' import svelte from 'eslint-plugin-svelte' import svelteParser from 'svelte-eslint-parser' @@ -21,34 +24,34 @@ export default ts.config( { ignores: parseConfig('.gitignore') ?? [] }, // typescript { - files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/*.svelte'], extends: [...ts.configs.strictTypeChecked], + files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/*.svelte'], rules: { - '@typescript-eslint/array-type': 'error', // use X[] instead of Array '@typescript-eslint/ban-types': 'error', // disable problematic types -> {} - '@typescript-eslint/consistent-generic-constructors': 'error', // specify types on constructor instead of on the variable - '@typescript-eslint/consistent-indexed-object-style': 'error', // use Record instead of {[key: string]:string} - '@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': false }], // allow only the @ts-expect-error - '@typescript-eslint/consistent-type-assertions': ['error', { assertionStyle: 'as' }], // disable as (as const still work) + '@typescript-eslint/no-shadow': 'error', // disable variable in inner scope with the same name o another variable + '@typescript-eslint/array-type': 'error', // use X[] instead of Array + '@typescript-eslint/no-unused-vars': 'off', // disable error for unused variables or params (already checked by typescript) '@typescript-eslint/unbound-method': 'off', // cause errors with the typescript compiler api - '@typescript-eslint/consistent-type-definitions': ['error', 'type'], // disable interface - '@typescript-eslint/consistent-type-exports': 'error', // separate type export - '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off', // allow writing if (var1 === false) instead of only if (!var1) - '@typescript-eslint/no-confusing-void-expression': 'off', // enable returning void -> ()=>console.log("ok") - '@typescript-eslint/method-signature-style': 'error', // allow only arrow functions in types + '@typescript-eslint/prefer-for-of': 'error', // use for (const x of []) instead of normal for loop + '@typescript-eslint/no-unsafe-argument': 'off', // the rules doesn't work as expected + '@typescript-eslint/no-require-imports': 'error', // disable require '@typescript-eslint/no-inferrable-types': 'error', // don't specify type for inferrable types - '@typescript-eslint/no-meaningless-void-operator': 'off', // allow returning void, return void console.log("ok") '@typescript-eslint/no-non-null-assertion': 'off', // allow non null assestion - '@typescript-eslint/no-require-imports': 'error', // disable require - '@typescript-eslint/no-unnecessary-condition': ['error', { allowConstantLoopConditions: true }], // diable const condition exept for loops - '@typescript-eslint/prefer-for-of': 'error', // use for (const x of []) instead of normal for loop '@typescript-eslint/prefer-function-type': 'error', // disable defining function with this { (): string } instead of ()=>string + '@typescript-eslint/method-signature-style': 'error', // allow only arrow functions in types + '@typescript-eslint/consistent-type-exports': 'error', // separate type export + '@typescript-eslint/no-confusing-void-expression': 'off', // enable returning void -> ()=>console.log("ok") + '@typescript-eslint/no-meaningless-void-operator': 'off', // allow returning void, return void console.log("ok") '@typescript-eslint/require-array-sort-compare': 'error', // require compare function as sort argument '@typescript-eslint/switch-exhaustiveness-check': 'error', // make switch check all cases - '@typescript-eslint/no-unused-vars': 'off', // disable error for unused variables or params (already checked by typescript) + '@typescript-eslint/consistent-generic-constructors': 'error', // specify types on constructor instead of on the variable + '@typescript-eslint/consistent-indexed-object-style': 'error', // use Record instead of {[key: string]:string} + '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off', // allow writing if (var1 === false) instead of only if (!var1) + '@typescript-eslint/consistent-type-definitions': ['error', 'type'], // disable interface + '@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': false }], // allow only the @ts-expect-error + '@typescript-eslint/consistent-type-assertions': ['error', { assertionStyle: 'as' }], // disable as (as const still work) '@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }], // enable using number and other - '@typescript-eslint/no-unsafe-argument': 'off', // the rules doesn't work as expected - '@typescript-eslint/no-shadow': 'error', // disable variable in inner scope with the same name o another variable + '@typescript-eslint/no-unnecessary-condition': ['error', { allowConstantLoopConditions: true }], // diable const condition exept for loops // enforce variable naming style '@typescript-eslint/naming-convention': ['error', { selector: 'objectLiteralProperty', format: ['camelCase', 'snake_case', 'UPPER_CASE'] }] @@ -56,55 +59,85 @@ export default ts.config( }, // js/ts { - files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/*.svelte'], + plugins: { unicorn, perfectionist }, extends: [prettier, eslint.configs.recommended], - plugins: { unicorn }, + files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx', '**/*.svelte'], rules: { - 'no-var': 'error', // disable var keyword 'no-undef': 'off', // already checked by typescript - 'no-constant-condition': 'off', // already checked by typescript - 'no-unused-vars': 'off', // already checked by typescript - 'spaced-comment': ['error', 'always'], // space after comment - 'array-callback-return': ['error', { checkForEach: true, allowVoid: true, allowImplicit: false }], // check for return type in map, foreach, ... - 'no-restricted-syntax': ['error', 'TryStatement > FinallyClause'], // disable try-finally - 'no-promise-executor-return': 'error', // no return in custom promise - 'no-self-compare': 'error', // don't compare variable to itself - 'no-template-curly-in-string': 'error', // error if ${} it's used in a normal string (not template) - 'curly': ['error', 'multi-or-nest'], // disable {} if it can stay inline - 'eqeqeq': ['error', 'always'], // require triple = - 'grouped-accessor-pairs': ['error', 'getBeforeSet'], // require getters and setters to be near each other - 'no-array-constructor': 'error', // force consistent array of x element definitions + 'no-var': 'error', // disable var keyword + 'no-shadow': 'off', // already checked by the eslint typescript plugin 'no-bitwise': 'error', // error if using | or & instead of || or && + 'no-plusplus': 'error', // disable ++ and -- + 'no-sequences': 'error', // disable multiple operation inline separated by , + 'no-unused-vars': 'off', // already checked by typescript 'no-else-return': 'error', // disable else if the if has a return - 'no-empty': ['error', { allowEmptyCatch: true }], // no empty block (catch excluded) - 'no-extend-native': 'error', // don't add property to global object - 'no-extra-boolean-cast': ['error', { enforceForLogicalOperands: true }], // don't use more than one ! before a variable - 'no-implicit-coercion': 'error', // no implicit type conversion 1+"1" -> string 'no-multi-assign': 'error', // disable multi assing const a = b = 1 - 'no-negated-condition': 'error', // no else after negation in if. if (!var1) - 'no-plusplus': 'error', // disable ++ and -- + 'no-self-compare': 'error', // don't compare variable to itself + 'yoda': ['error', 'never'], // variable to check before literal -> var1 === true + 'no-extend-native': 'error', // don't add property to global object 'no-return-assign': 'error', // no variable assignment in return - 'no-sequences': 'error', // disable multiple operation inline separated by , - 'no-shadow': 'off', // already checked by the eslint typescript plugin 'object-shorthand': 'error', // disable object value with the same name as the key - 'operator-assignment': 'error', // use x+=1 instead of x = x+1 + 'eqeqeq': ['error', 'always'], // require triple = 'symbol-description': 'error', // allow only sibols with description ok: Symbol("a"), err: Symbol() - 'yoda': ['error', 'never'], // variable to check before literal -> var1 === true - 'func-style': ['error', 'expression', { allowArrowFunctions: true }], // allow only const a = () =>{} or const a = function a() {} + 'no-constant-condition': 'off', // already checked by typescript + 'operator-assignment': 'error', // use x+=1 instead of x = x+1 + 'no-array-constructor': 'error', // force consistent array of x element definitions + 'no-implicit-coercion': 'error', // no implicit type conversion 1+"1" -> string + 'no-negated-condition': 'error', // no else after negation in if. if (!var1) + 'curly': ['error', 'multi-or-nest'], // disable {} if it can stay inline + 'no-promise-executor-return': 'error', // no return in custom promise + 'spaced-comment': ['error', 'always'], // space after comment + 'no-template-curly-in-string': 'error', // error if ${} it's used in a normal string (not template) 'arrow-body-style': ['error', 'as-needed'], // don't use {} for inline return + 'no-empty': ['error', { allowEmptyCatch: true }], // no empty block (catch excluded) + 'grouped-accessor-pairs': ['error', 'getBeforeSet'], // require getters and setters to be near each other + 'no-restricted-syntax': ['error', 'TryStatement > FinallyClause'], // disable try-finally 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], // allow only arrow functions in callback + 'func-style': ['error', 'expression', { allowArrowFunctions: true }], // allow only const a = () =>{} or const a = function a() {} + 'no-extra-boolean-cast': ['error', { enforceForLogicalOperands: true }], // don't use more than one ! before a variable + 'array-callback-return': ['error', { allowVoid: true, checkForEach: true, allowImplicit: false }], // check for return type in map, foreach, ... // other js/ts rules from the unicorn package - 'unicorn/consistent-destructuring': 'error', // consistent deconstruct (deconstruct all or nothing) + 'unicorn/no-null': 'error', // disable null (it works inside of triple = if) + 'unicorn/no-for-loop': 'error', // disable for loop when it can be replaced with a for-of + 'unicorn/no-thenable': 'error', // disable .then + 'unicorn/no-new-array': 'error', // creare new array of x elements only with Array.from({length: lenght}) instead of new Array(100) 'unicorn/error-message': 'error', // require message when creating an error new Error("msg") 'unicorn/new-for-builtins': 'error', // require the new keyword when necessary and removes it when is not - 'unicorn/no-for-loop': 'error', // disable for loop when it can be replaced with a for-of 'unicorn/no-instanceof-array': 'error', // prevents bug when checking if a variable is an array - 'unicorn/no-new-array': 'error', // creare new array of x elements only with Array.from({length: lenght}) instead of new Array(100) - 'unicorn/no-null': 'error', // disable null (it works inside of triple = if) - 'unicorn/no-thenable': 'error', // disable .then + 'unicorn/consistent-destructuring': 'error', // consistent deconstruct (deconstruct all or nothing) + 'unicorn/prefer-string-trim-start-end': 'error', // use trimStart/trimEnd instead of trimLeft/trimRight 'unicorn/prefer-string-starts-ends-with': 'error', // use "".startsWith or "".endsWith instead of regex - 'unicorn/prefer-string-trim-start-end': 'error' // use trimStart/trimEnd instead of trimLeft/trimRight + + // style rules + 'perfectionist/sort-maps': ['error', { type: 'line-length' }], + 'perfectionist/sort-classes': ['error', { type: 'line-length' }], + 'perfectionist/sort-exports': ['error', { type: 'line-length' }], + 'perfectionist/sort-jsx-props': ['error', { type: 'line-length' }], + 'perfectionist/sort-intersection-types': ['error', { type: 'line-length' }], + 'perfectionist/sort-union-types': ['error', { 'nullable-last': true, 'type': 'line-length' }], + 'perfectionist/sort-array-includes': ['error', { 'spread-last': true, 'type': 'line-length' }], + 'perfectionist/sort-named-exports': ['error', { 'type': 'line-length', 'group-kind': 'types-first' }], + 'perfectionist/sort-named-imports': ['error', { 'type': 'line-length', 'group-kind': 'types-first' }], + 'perfectionist/sort-object-types': ['error', { 'type': 'line-length', 'groups': ['multiline'], 'partition-by-new-line': true }], + // 'perfectionist/sort-objects': ['error', { 'type': 'line-length', 'partition-by-comment': true, 'partition-by-new-line': true }], + 'perfectionist/sort-svelte-attributes': ['error', { type: 'line-length', groups: ['multiline', 'shorthand', 'svelte-shorthand'] }], + 'perfectionist/sort-imports': [ + 'error', + { + 'type': 'line-length', + 'internal-pattern': ['$lib/**', '~/**'], + 'groups': [ + ['builtin-type', 'external-type'], + ['builtin', 'external'], + + ['internal-type', 'parent-type', 'sibling-type', 'index-type'], + ['internal', 'parent', 'sibling', 'index'], + + ['side-effect', 'object', 'unknown', 'style', 'side-effect-style'] + ] + } + ] } }, // svelte @@ -114,15 +147,15 @@ export default ts.config( extends: svelte.configs['flat/recommended'], languageOptions: { parser: svelteParser, parserOptions: { parser: ts.parser } }, rules: { - 'svelte/infinite-reactive-loop': 'error', // prevent reactivity bug - 'svelte/no-export-load-in-svelte-module-in-kit-pages': 'error', // no function called load in script - 'svelte/no-reactive-reassign': 'error', // don't readding derived reactive values 'svelte/no-store-async': 'error', // disable async await in stores + 'svelte/sort-attributes': 'error', // html attributes needs to be sorted + 'svelte/no-reactive-reassign': 'error', // don't readding derived reactive values + 'svelte/no-useless-mustaches': 'error', // don't allow useless {} + 'svelte/infinite-reactive-loop': 'error', // prevent reactivity bug 'svelte/valid-prop-names-in-kit-pages': 'error', // disable invalid exports in +page.svelte file - 'svelte/block-lang': ['error', { enforceScriptPresent: true, script: ['ts'] }], // require lang="ts" in the script tag 'svelte/no-immutable-reactive-statements': 'error', // disable reactive statement for const values - 'svelte/no-useless-mustaches': 'error', // don't allow useless {} - 'svelte/sort-attributes': 'error', // html attributes needs to be sorted + 'svelte/no-export-load-in-svelte-module-in-kit-pages': 'error', // no function called load in script + 'svelte/block-lang': ['error', { script: ['ts'], enforceScriptPresent: true }], // require lang="ts" in the script tag // rules that doesn't work in svelte 5 '@typescript-eslint/no-unsafe-call': 'off', diff --git a/src/lib/cliHandlers.ts b/src/lib/cliHandlers.ts index 7b18b3c..23d2058 100644 --- a/src/lib/cliHandlers.ts +++ b/src/lib/cliHandlers.ts @@ -18,7 +18,7 @@ export const handleKeypress = () => { // handle console clear and exit readline.emitKeypressEvents(process.stdin) process.stdin.setRawMode(true) - process.stdin.on('keypress', (_, key: { ctrl: boolean; name: string }) => { + process.stdin.on('keypress', (_, key: { name: string; ctrl: boolean }) => { if (key.ctrl && key.name === 'c') return process.exit() if (key.name === 'return') { console.log('\n'.repeat(10000)) diff --git a/src/lib/execCmd.ts b/src/lib/execCmd.ts index fa9d8dd..d213bc2 100644 --- a/src/lib/execCmd.ts +++ b/src/lib/execCmd.ts @@ -1,6 +1,7 @@ -import { log } from './logger.ts' -import { spawn } from 'child_process' import fs from 'fs' +import { spawn } from 'child_process' + +import { log } from './logger.ts' export const getPackageManager = () => { const currentDir = fs.readdirSync('.') @@ -15,7 +16,7 @@ export const getPackageManager = () => { export const asyncCommands: Promise[] = [] -type ExecCmd = { title: string; cmd: string[]; customCmd?: string; mode: 'sync' | 'async'; cwd?: string } +type ExecCmd = { cwd?: string; title: string; cmd: string[]; customCmd?: string; mode: 'sync' | 'async' } export const execCmd = async ({ title, cmd, customCmd, mode, cwd }: ExecCmd) => { const execPromise = new Promise((resolve, _) => { const output = spawn(customCmd ?? getPackageManager(), cmd, { @@ -40,7 +41,7 @@ export const execCmd = async ({ title, cmd, customCmd, mode, cwd }: ExecCmd) => return void asyncCommands.push(execPromise) } -type ReadLogFile = { title: string; cwd: string } +type ReadLogFile = { cwd: string; title: string } export const readLogFile = async ({ title, cwd }: ReadLogFile) => { // clear the log file const logFile = `${cwd}/lsp-plugin.log` diff --git a/src/lib/findGlob.ts b/src/lib/findGlob.ts index ab93082..5a4cd33 100644 --- a/src/lib/findGlob.ts +++ b/src/lib/findGlob.ts @@ -4,6 +4,7 @@ declare module 'node:fs' { } import fs from 'node:fs' + import { parseConfig } from './parseConfig' const gitIgnore = parseConfig(`${process.cwd()}/.gitignore`) ?? [] diff --git a/src/lib/findProjects.ts b/src/lib/findProjects.ts index 3d24400..5582543 100644 --- a/src/lib/findProjects.ts +++ b/src/lib/findProjects.ts @@ -1,6 +1,7 @@ import fs from 'node:fs' -import { parseConfig } from './parseConfig.ts' + import { findGlob } from './findGlob.ts' +import { parseConfig } from './parseConfig.ts' const getWorkspaceProjects = (dir: string) => { const globs: string[] = [] @@ -15,7 +16,7 @@ const getWorkspaceProjects = (dir: string) => { return globs.flatMap((glob) => findGlob(glob, { cwd: dir })).filter((path) => fs.existsSync(`${dir}/${path}/package.json`)) } -export type Project = { name: string; scripts: string[]; lspPlugin: boolean; cwd: string; type: 'svelte' | 'node' } +export type Project = { cwd: string; name: string; scripts: string[]; lspPlugin: boolean; type: 'node' | 'svelte' } // find all the projects in a the monorepo/repo export const findProjects = (dir: string = process.cwd()): Project[] => { const projects = getWorkspaceProjects(dir).flatMap((project) => findProjects(project)) diff --git a/src/lib/logger.ts b/src/lib/logger.ts index beb754f..fa5a982 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -113,14 +113,14 @@ const vitestLogFormatter = (input: string, printLog: (txt: string) => void) => { const parseVitestOutput = (input: string): { name: string; errors: string[] }[] => { type VitestJSON = { - success: boolean testResults: [ { name: string status: 'passed' | 'failed' - assertionResults: [{ status: 'passed' | 'failed'; title: string; failureMessages: string[] }] + assertionResults: [{ title: string; failureMessages: string[]; status: 'passed' | 'failed' }] } ] + success: boolean } const vitestData = JSON.parse(input) as VitestJSON if (vitestData.success) return [] diff --git a/src/lib/parseConfig.ts b/src/lib/parseConfig.ts index 26ae462..b62f582 100644 --- a/src/lib/parseConfig.ts +++ b/src/lib/parseConfig.ts @@ -1,5 +1,5 @@ -import fs from 'node:fs' import yaml from 'yaml' +import fs from 'node:fs' // parse json/yaml/.gitignore and add the custom return type export const parseConfig = (path: string): Res | undefined => { diff --git a/src/task-runner.ts b/src/task-runner.ts index c3a8dca..21678a6 100644 --- a/src/task-runner.ts +++ b/src/task-runner.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { getArgs, handleKeypress } from './lib/cliHandlers.ts' -import { asyncCommands, execCmd, readLogFile } from './lib/execCmd.ts' import { findProjects } from './lib/findProjects.ts' +import { getArgs, handleKeypress } from './lib/cliHandlers.ts' +import { execCmd, readLogFile, asyncCommands } from './lib/execCmd.ts' handleKeypress() const { flags, task, cmd } = getArgs() @@ -9,10 +9,10 @@ const projects = findProjects() const monorepo = projects.length > 1 // apply formatting and linting rules -if (flags.includes('--apply')) { +if (flags.includes('--fix')) { await execCmd({ title: 'prettier', - cmd: ['prettier', '--ignore-path .gitignore', '--log-level warn', '--write', '.'], + cmd: ['prettier', '--ignore-path=.gitignore', '--log-level=warn', '--write', '.'], mode: 'sync' }) diff --git a/src/vitest.ts b/src/vitest.ts index 5eac680..20d669f 100644 --- a/src/vitest.ts +++ b/src/vitest.ts @@ -1,6 +1,7 @@ import { defineWorkspace } from 'vitest/config' -import { findProjects } from './lib/findProjects' + import { findGlob } from './lib/findGlob' +import { findProjects } from './lib/findProjects' export default defineWorkspace( findProjects().map(({ name, cwd }) => ({ diff --git a/tsup.config.ts b/tsup.config.ts index ed3be76..ccdc26a 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'tsup' + import { findGlob } from './src/lib/findGlob' export default defineConfig({