UNPKG

@kouts/eslint-config

Version:
920 lines (910 loc) 28.5 kB
'use strict'; var js = require('@eslint/js'); var pluginHtml = require('eslint-plugin-html'); var pluginPrettier = require('eslint-plugin-prettier/recommended'); var pluginSimpleImportSort = require('eslint-plugin-simple-import-sort'); var node_fs = require('node:fs'); var node_path = require('node:path'); var compat = require('@eslint/compat'); var gitignoreToMinimatch = require('@humanwhocodes/gitignore-to-minimatch'); var stylistic = require('@stylistic/eslint-plugin'); var importX = require('eslint-plugin-import-x'); var pluginN = require('eslint-plugin-n'); var pluginPromise = require('eslint-plugin-promise'); var findUp = require('find-up'); var globals = require('globals'); var tseslint = require('typescript-eslint'); var pluginVitest = require('@vitest/eslint-plugin'); var pluginVue = require('eslint-plugin-vue'); const plugins = { "@stylistic": stylistic, "import-x": importX, n: pluginN, promise: pluginPromise, // react: pluginReact, "typescript-eslint": tseslint }; const base = { name: "neostandard/base", languageOptions: { ecmaVersion: 2022, sourceType: "module", globals: { ...globals.es2022, ...globals.node, document: "readonly", navigator: "readonly", window: "readonly" } }, plugins: { n: plugins.n }, rules: { "no-var": "warn", "object-shorthand": ["warn", "properties"], "accessor-pairs": ["error", { setWithoutGet: true, enforceForClassMembers: true }], "array-callback-return": [ "error", { allowImplicit: false, checkForEach: false } ], camelcase: [ "error", { allow: ["^UNSAFE_"], properties: "never", ignoreGlobals: true } ], "constructor-super": "error", curly: ["error", "multi-line"], "default-case-last": "error", "dot-notation": ["error", { allowKeywords: true }], eqeqeq: ["error", "always", { null: "ignore" }], "new-cap": ["error", { newIsCap: true, capIsNew: false, properties: true }], "no-array-constructor": "error", "no-async-promise-executor": "error", "no-caller": "error", "no-case-declarations": "error", "no-class-assign": "error", "no-compare-neg-zero": "error", "no-cond-assign": "error", "no-const-assign": "error", "no-constant-condition": ["error", { checkLoops: false }], "no-control-regex": "error", "no-debugger": "error", "no-delete-var": "error", "no-dupe-args": "error", "no-dupe-class-members": "error", "no-dupe-keys": "error", "no-duplicate-case": "error", "no-useless-backreference": "error", "no-empty": ["error", { allowEmptyCatch: true }], "no-empty-character-class": "error", "no-empty-pattern": "error", "no-eval": "error", "no-ex-assign": "error", "no-extend-native": "error", "no-extra-bind": "error", "no-extra-boolean-cast": "error", "no-fallthrough": "error", "no-func-assign": "error", "no-global-assign": "error", "no-implied-eval": "error", "no-import-assign": "error", "no-invalid-regexp": "error", "no-irregular-whitespace": "error", "no-iterator": "error", "no-labels": ["error", { allowLoop: false, allowSwitch: false }], "no-lone-blocks": "error", "no-loss-of-precision": "error", "no-misleading-character-class": "error", "no-prototype-builtins": "error", "no-useless-catch": "error", "no-multi-str": "error", "no-new": "error", "no-new-func": "error", "no-object-constructor": "error", "no-new-native-nonconstructor": "error", "no-new-wrappers": "error", "no-obj-calls": "error", "no-octal": "error", "no-octal-escape": "error", "no-proto": "error", "no-redeclare": ["error", { builtinGlobals: false }], "no-regex-spaces": "error", "no-return-assign": ["error", "except-parens"], "no-self-assign": ["error", { props: true }], "no-self-compare": "error", "no-sequences": "error", "no-shadow-restricted-names": "error", "no-sparse-arrays": "error", "no-template-curly-in-string": "error", "no-this-before-super": "error", "no-throw-literal": "error", "no-undef": "error", "no-undef-init": "error", "no-unexpected-multiline": "error", "no-unmodified-loop-condition": "error", "no-unneeded-ternary": ["error", { defaultAssignment: false }], "no-unreachable": "error", "no-unreachable-loop": "error", "no-unsafe-finally": "error", "no-unsafe-negation": "error", "no-unused-expressions": [ "error", { allowShortCircuit: true, allowTernary: true, allowTaggedTemplates: true } ], "no-unused-vars": [ "error", { args: "none", caughtErrors: "none", ignoreRestSiblings: true, vars: "all" } ], "no-use-before-define": ["error", { functions: false, classes: false, variables: false }], "no-useless-call": "error", "no-useless-computed-key": "error", "no-useless-constructor": "error", "no-useless-escape": "error", "no-useless-rename": "error", "no-useless-return": "error", "no-void": "error", "no-with": "error", "one-var": ["error", { initialized: "never" }], "prefer-const": ["error", { destructuring: "all" }], "prefer-promise-reject-errors": "error", "prefer-regex-literals": ["error", { disallowRedundantWrapping: true }], "symbol-description": "error", "unicode-bom": ["error", "never"], "use-isnan": [ "error", { enforceForSwitchCase: true, enforceForIndexOf: true } ], "valid-typeof": ["error", { requireStringLiterals: true }], yoda: ["error", "never"], "n/handle-callback-err": ["error", "^(err|error)$"], "n/no-callback-literal": "error", "n/no-deprecated-api": "error", "n/no-exports-assign": "error", "n/no-new-require": "error", "n/no-path-concat": "error", "n/process-exit-as-throw": "error", "promise/param-names": "error" } }; const modernization = { name: "neostandard/modernization-since-standard-17", rules: { "dot-notation": "off", "n/no-deprecated-api": "warn" } }; const promise = compat.fixupConfigRules(pluginPromise.configs["flat/recommended"]); const promiseConfigs = Array.isArray(promise) ? promise : [promise]; const modernizationStyles = { name: "neostandard/style/modernization-since-standard-17", rules: { "@stylistic/comma-dangle": [ "warn", { arrays: "ignore", enums: "ignore", exports: "ignore", imports: "ignore", objects: "ignore" } ], "@stylistic/no-multi-spaces": ["error", { ignoreEOLComments: true }] } }; const style = { name: "neostandard/style", plugins: { "@stylistic": plugins["@stylistic"] }, rules: { "@stylistic/array-bracket-spacing": ["error", "never"], "@stylistic/arrow-spacing": ["error", { before: true, after: true }], "@stylistic/block-spacing": ["error", "always"], "@stylistic/brace-style": ["error", "1tbs", { allowSingleLine: true }], "@stylistic/comma-dangle": [ "error", { arrays: "never", objects: "never", imports: "never", exports: "never", functions: "never" } ], "@stylistic/comma-spacing": ["error", { before: false, after: true }], "@stylistic/comma-style": ["error", "last"], "@stylistic/computed-property-spacing": ["error", "never", { enforceForClassMembers: true }], "@stylistic/dot-location": ["error", "property"], "@stylistic/eol-last": "error", "@stylistic/func-call-spacing": ["error", "never"], "@stylistic/generator-star-spacing": ["error", { before: true, after: true }], "@stylistic/indent": [ "error", 2, { SwitchCase: 1, VariableDeclarator: 1, outerIIFEBody: 1, MemberExpression: 1, FunctionDeclaration: { parameters: 1, body: 1 }, FunctionExpression: { parameters: 1, body: 1 }, CallExpression: { arguments: 1 }, ArrayExpression: 1, ObjectExpression: 1, ImportDeclaration: 1, flatTernaryExpressions: false, ignoreComments: false, ignoredNodes: [ "TemplateLiteral *", "JSXElement", "JSXElement > *", "JSXAttribute", "JSXIdentifier", "JSXNamespacedName", "JSXMemberExpression", "JSXSpreadAttribute", "JSXExpressionContainer", "JSXOpeningElement", "JSXClosingElement", "JSXFragment", "JSXOpeningFragment", "JSXClosingFragment", "JSXText", "JSXEmptyExpression", "JSXSpreadChild" ], offsetTernaryExpressions: true } ], "@stylistic/key-spacing": ["error", { beforeColon: false, afterColon: true }], "@stylistic/keyword-spacing": ["error", { before: true, after: true }], "@stylistic/lines-between-class-members": ["error", "always", { exceptAfterSingleLine: true }], "@stylistic/multiline-ternary": ["error", "always-multiline"], "@stylistic/new-parens": "error", "@stylistic/no-extra-parens": ["error", "functions"], "@stylistic/no-floating-decimal": "error", "@stylistic/no-mixed-operators": [ "error", { groups: [ ["==", "!=", "===", "!==", ">", ">=", "<", "<="], ["&&", "||"], ["in", "instanceof"] ], allowSamePrecedence: true } ], "@stylistic/no-mixed-spaces-and-tabs": "error", "@stylistic/no-multi-spaces": "error", "@stylistic/no-multiple-empty-lines": ["error", { max: 1, maxBOF: 0, maxEOF: 0 }], "@stylistic/no-tabs": "error", "@stylistic/no-trailing-spaces": "error", "@stylistic/no-whitespace-before-property": "error", "@stylistic/object-curly-newline": ["error", { multiline: true, consistent: true }], "@stylistic/object-curly-spacing": ["error", "always"], "@stylistic/object-property-newline": ["error", { allowMultiplePropertiesPerLine: true }], "@stylistic/operator-linebreak": ["error", "after", { overrides: { "?": "before", ":": "before", "|>": "before" } }], "@stylistic/padded-blocks": ["error", { blocks: "never", switches: "never", classes: "never" }], "@stylistic/quote-props": ["error", "as-needed"], "@stylistic/quotes": ["error", "single", { avoidEscape: true, allowTemplateLiterals: false }], "@stylistic/rest-spread-spacing": ["error", "never"], "@stylistic/semi": ["error", "never"], "@stylistic/semi-spacing": ["error", { before: false, after: true }], "@stylistic/space-before-blocks": ["error", "always"], "@stylistic/space-before-function-paren": ["error", "always"], "@stylistic/space-in-parens": ["error", "never"], "@stylistic/space-infix-ops": "error", "@stylistic/space-unary-ops": ["error", { words: true, nonwords: false }], "@stylistic/spaced-comment": [ "error", "always", { line: { markers: ["*package", "!", "/", ",", "="] }, block: { balanced: true, markers: ["*package", "!", ",", ":", "::", "flow-include"], exceptions: ["*"] } } ], "@stylistic/template-curly-spacing": ["error", "never"], "@stylistic/template-tag-spacing": ["error", "never"], "@stylistic/wrap-iife": ["error", "any", { functionPrototypeMethods: true }], "@stylistic/yield-star-spacing": ["error", "both"] } }; const jsxIgnores = ["**/*.js", "**/*.mjs", "**/*.cjs", "**/*.ts"]; const jsx = { name: "neostandard/jsx", ignores: [...jsxIgnores], languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } /* * React support is temporarily disabled. * plugins: { * react: plugins.react, * }, * settings: { * react: { * version: '17', * }, * linkComponents: ['Link'], * }, * rules: { * 'react/jsx-boolean-value': 'error', * 'react/jsx-fragments': ['error', 'syntax'], * 'react/jsx-handler-names': 'error', * 'react/jsx-key': [ * 'error', * { * checkFragmentShorthand: true, * }, * ], * 'react/jsx-no-comment-textnodes': 'error', * 'react/jsx-no-duplicate-props': 'error', * 'react/jsx-no-target-blank': ['error', { enforceDynamicLinks: 'always' }], * 'react/jsx-no-undef': ['error', { allowGlobals: true }], * 'react/jsx-uses-react': 'error', * 'react/jsx-uses-vars': 'error', * 'react/no-children-prop': 'error', * 'react/no-danger-with-children': 'error', * 'react/no-deprecated': 'error', * 'react/no-direct-mutation-state': 'error', * 'react/no-find-dom-node': 'error', * 'react/no-is-mounted': 'error', * 'react/no-string-refs': [ * 'error', * { * noTemplateLiterals: true, * }, * ], * 'react/no-unescaped-entities': [ * 'error', * { * forbid: ['>', '}'], * }, * ], * 'react/no-render-return-value': 'error', * 'react/require-render-return': 'error', * 'react/self-closing-comp': 'error', * }, */ }; const jsxStyles = { name: "neostandard/style/jsx", ignores: [...jsxIgnores], rules: { "@stylistic/jsx-quotes": ["error", "prefer-single"], "@stylistic/jsx-closing-bracket-location": ["error", "tag-aligned"], "@stylistic/jsx-closing-tag-location": "error", "@stylistic/jsx-curly-brace-presence": [ "error", { props: "never", children: "never" } ], "@stylistic/jsx-curly-newline": [ "error", { multiline: "consistent", singleline: "consistent" } ], "@stylistic/jsx-curly-spacing": [ "error", { attributes: { when: "never", allowMultiline: true }, children: { when: "never", allowMultiline: true } } ], "@stylistic/jsx-equals-spacing": ["error", "never"], "@stylistic/jsx-first-prop-new-line": ["error", "multiline-multiprop"], "@stylistic/jsx-indent": [ "error", 2, { checkAttributes: false, indentLogicalExpressions: true } ], "@stylistic/jsx-indent-props": ["error", 2], "@stylistic/jsx-pascal-case": ["error", { allowAllCaps: false }], "@stylistic/jsx-props-no-multi-spaces": "error", "@stylistic/jsx-tag-spacing": [ "error", { closingSlash: "never", beforeSelfClosing: "always", afterOpening: "never", beforeClosing: "never" } ], "@stylistic/jsx-wrap-multilines": [ "error", { declaration: "parens-new-line", assignment: "parens-new-line", return: "parens-new-line", arrow: "ignore", condition: "ignore", logical: "ignore", prop: "ignore" } ] } }; const semiConfig = { name: "neostandard/semi", rules: { "@stylistic/semi": ["error", "always"], "@stylistic/no-extra-semi": "error" } }; const jsImportRules = { name: "neostandard/import-x", files: ["**/*.js", "**/*.mjs", "**/*.cjs", "**/*.jsx"], plugins: { "import-x": plugins["import-x"] }, rules: { "import-x/export": "error", "import-x/first": "error", "import-x/no-absolute-path": ["error", { esmodule: true, commonjs: true, amd: false }], "import-x/no-duplicates": "error", "import-x/no-named-default": "error", "import-x/no-webpack-loader-syntax": "error", // Custom rules "import-x/no-mutable-exports": "error", "import-x/newline-after-import": ["error", { count: 1 }], "import-x/no-self-import": "error" } }; const isNonEmpty = (value) => Object.keys(value).length > 0; const resolveIgnoresFromGitignore = () => { const configFile = findFlatConfigFileSync(); if (!configFile) { return []; } const result = []; try { const content = node_fs.readFileSync(node_path.join(node_path.dirname(configFile), ".gitignore"), "utf8"); for (let line of content.split("\n")) { line = line.trim(); if (line && !line.startsWith("#")) { result.push(gitignoreToMinimatch.gitignoreToMinimatch(line)); } } } catch { } return result; }; function findFlatConfigFileSync() { return findUp.findUpSync([ "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", "eslint.config.mts", "eslint.config.cts" ]); } function typescriptify(configs, options) { const { files, ignores, name, project, tsconfigRootDir, typeChecking = false } = options; const deactivatedRules = {}; const replacementRules = {}; const tsPlugin = tseslint.plugin; for (const config of configs) { for (const ruleId of Object.keys(tsRedundant)) { if (config.rules?.[ruleId]) { deactivatedRules[ruleId] = "off"; } } for (const [ruleId, ruleDefinition] of Object.entries(tsPlugin.rules)) { const currentRule = config.rules?.[ruleId]; if (currentRule === void 0) { continue; } if (!ruleDefinition.meta?.docs || typeof ruleDefinition.meta.docs !== "object") { continue; } const docs = ruleDefinition.meta.docs; if (!docs.extendsBaseRule) { continue; } if (docs.requiresTypeChecking && !typeChecking) { continue; } deactivatedRules[ruleId] = "off"; if (ruleId !== "dot-notation") { replacementRules[`@typescript-eslint/${ruleId}`] = currentRule; } } } return { ...name ? { name } : {}, ...files ? { files } : {}, ...ignores ? { ignores } : {}, languageOptions: { parser: tseslint.parser, parserOptions: { project: project === void 0 ? typeChecking : project, ...tsconfigRootDir ? { tsconfigRootDir } : {} } }, plugins: { "@typescript-eslint": tseslint.plugin }, rules: { ...deactivatedRules, ...replacementRules } }; } const tsRedundant = { "getter-return": "off", "constructor-super": "off", "no-const-assign": "off", "no-dupe-args": "off", "no-dupe-class-members": "off", "no-dupe-keys": "off", "no-func-assign": "off", "no-import-assign": "off", "no-new-native-nonconstructor": "off", "no-obj-calls": "off", "no-redeclare": "off", "no-setter-return": "off", "no-this-before-super": "off", "no-undef": "off", "no-unreachable": "off", "no-unsafe-negation": "off" }; const getJsConfigs = (opts) => { const jsxConfigs = opts.noJsx ? [] : [jsx, ...opts.noStyle ? [] : [jsxStyles]]; const styleConfigs = opts.noStyle ? [] : [style, modernizationStyles, ...opts.semi ? [semiConfig] : []]; return [...promiseConfigs, base, modernization, ...jsxConfigs, ...styleConfigs, jsImportRules]; }; const neostandard = (options) => { const { env, files: rawFiles, filesTs, globals: rawGlobals, ignores, noJsx = false, noStyle = false, ts = false, semi = false } = options || {}; if (filesTs && !ts) { throw new Error('"filesTs" is only usable with the "ts" option'); } const resolvedGlobals = Array.isArray(rawGlobals) ? Object.fromEntries(rawGlobals.map((global) => [global, true])) : { ...rawGlobals }; for (const key of env || []) { if (!globals[key]) { throw new Error(`Invalid env definition: ${env}`); } const envGlobals = globals[key]; for (const [name, value] of Object.entries(envGlobals)) { resolvedGlobals[name] = resolvedGlobals[name] || value; } } const files = [...rawFiles || [], ...noJsx ? [] : ["**/*.jsx"]]; const jsConfigs = getJsConfigs({ noJsx, noStyle, semi }); return [ ...ignores ? [{ ignores }] : [], ...files.length ? [ { name: "neostandard/additional-files", files } ] : [], ...isNonEmpty(resolvedGlobals) ? [ { name: "neostandard/globals", languageOptions: { globals: resolvedGlobals } } ] : [], ...jsConfigs, ...ts ? [ typescriptify(jsConfigs, { files: ["**/*.ts", ...noJsx ? [] : ["**/*.tsx"], ...filesTs || []], ignores, name: "neostandard/ts" }) ] : [] ]; }; const rule = { meta: { type: "suggestion", docs: { description: "Require defineOptions with name attribute in script setup Vue components", recommended: true }, schema: [] }, create(context) { let hasDefineOptionsWithName = false; return { // We'll assume every Vue file with script setup needs a defineOptions name // Check for defineOptions function calls 'CallExpression[callee.name="defineOptions"]'(node) { if (node.arguments?.length && node.arguments[0]?.type === "ObjectExpression") { const properties = node.arguments[0].properties || []; for (const prop of properties) { if (prop.type === "Property" && prop.key?.type === "Identifier" && prop.key?.name === "name") { hasDefineOptionsWithName = true; break; } } } }, // At the end of the file, report if we don't have defineOptions with name "Program:exit"(node) { const filename = context.physicalFilename; const isVueFile = filename.endsWith(".vue"); if (isVueFile && !hasDefineOptionsWithName) { const sourceCode = context.sourceCode.getText(); const hasScriptSetup = /<script\s+setup/.test(sourceCode); if (hasScriptSetup) { context.report({ node, message: 'Component name is required. Add defineOptions({ name: "YourComponentName" }) to your <script setup>' }); } } } }; } }; var customRules = { "vue-require-name-in-setup": rule }; const typescript = [ ...plugins["typescript-eslint"].configs.recommended, { name: "kouts/typescript", rules: { // Prefer T[] instead of Array<T> "@typescript-eslint/array-type": ["error", { default: "array" }], // Prefer type over interface for objects "@typescript-eslint/consistent-type-definitions": ["error", "type"], // Fix type imports "@typescript-eslint/consistent-type-imports": [ "error", { prefer: "type-imports", fixStyle: "inline-type-imports" } ], // Allow _ for unused variables "@typescript-eslint/no-unused-vars": [ "error", { args: "none", caughtErrors: "none", ignoreRestSiblings: true, vars: "all", argsIgnorePattern: "^_" } ], // Disallow non-null assertions using the ! postfix operator "@typescript-eslint/no-non-null-assertion": "error", // Disallow enums "no-restricted-syntax": [ "error", { selector: "TSEnumDeclaration", message: `Enums introduce unexpected runtime behavior, break TypeScript's structural typing, and add unnecessary complexity. Use union types or 'as const' objects instead.` } ] } } ]; const vitest = [ { name: "kouts/vitest", files: ["test/**", "tests/**", "**/*.test.{js,ts}*", "**/*.spec.{js,ts}"], plugins: { vitest: pluginVitest }, rules: { ...pluginVitest.configs.recommended.rules, "vitest/expect-expect": [ "error", { assertFunctionNames: ["expect", "assert", "expectTypeOf"] } ] }, languageOptions: { globals: { ...pluginVitest.environments.env.globals } } } ]; const vue = (options) => { const opts = { version: 3, ts: true, ...options }; const vueConfig = opts.version === 3 ? pluginVue.configs["flat/recommended"] : pluginVue.configs["flat/vue2-recommended"]; const languageOptions = opts.ts ? { languageOptions: { parserOptions: { parser: plugins["typescript-eslint"].parser } } } : {}; const config = [ ...vueConfig, { name: "kouts/vue", files: ["*.vue", "**/*.vue"], ...languageOptions, rules: { // Custom rules "kouts/vue-require-name-in-setup": "error", // Overrides for vue/(vue3-)recommended preset "vue/max-attributes-per-line": "off", "vue/singleline-html-element-content-newline": "off", // Strengthen vue/(vue3-)recommended preset for autofix // https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/configs/recommended.js "vue/attributes-order": "error", "vue/block-order": "error", "vue/no-lone-template": "error", "vue/no-multiple-slot-args": "error", "vue/no-v-html": "error", "vue/order-in-components": "error", "vue/this-in-template": "error", "vue/require-prop-types": "error", // Enforce PascalCase for Vue components "vue/component-name-in-template-casing": ["error", "PascalCase", { registeredComponentsOnly: false, ignores: [] }], // Do not allow inline styles "vue/no-static-inline-styles": ["error", { allowBinding: false }], // Require explicit emits "vue/require-explicit-emits": "error", // Require component to have a name property "vue/require-name-property": "error", // Require components that don't have any content to self-close "vue/html-self-closing": [ "error", { html: { void: "always", normal: "never", component: "always" }, svg: "always", math: "always" } ], // Enforce dot notation whenever possible in `<template>` "vue/dot-notation": ["error"] } } ]; return config; }; const customRulesPlugin = { name: "kouts", rules: customRules }; const config = (options) => { const opts = { noJsx: true, noStyle: true, semi: false, ts: true, vue: true, vueVersion: 3, vitest: true, ...options }; const linterConfig = [ // Custom rules plugin { name: "kouts/custom-rules", plugins: { kouts: customRulesPlugin } }, { name: "kouts/ignores", ignores: [ "**/node_modules/**", "{tmp,temp}/**", "**/*.min.js", "vendor/**", "dist/**", "public/**", ...resolveIgnoresFromGitignore() ] }, // JavaScript { name: "kouts/javascript", ...js.configs.recommended }, // TypeScript ...opts.ts ? typescript : [], // Neostandard ...neostandard(opts), { name: "kouts/neostandard-overrides", rules: { // Enforce blank lines between the given 2 kinds of statements "padding-line-between-statements": [ "error", { blankLine: "always", prev: "*", next: "return" }, { blankLine: "always", prev: ["const", "let", "var"], next: "*" }, { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"] }, { blankLine: "always", prev: "directive", next: "*" }, { blankLine: "any", prev: "directive", next: "directive" } ], // Console and debugger settings depending whether we're on production or not "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", // Enforce dot notation "dot-notation": ["error"] } }, // Sort imports { name: "kouts/sort-imports", plugins: { "simple-import-sort": pluginSimpleImportSort }, rules: { "simple-import-sort/imports": [ "error", // Remove all blank lines between imports { groups: [["^\\u0000", "^node:", "^@?\\w", "^", "^\\."]] } ], "simple-import-sort/exports": "error" } }, // HTML { name: "kouts/html", files: ["**/*.html"], plugins: { html: pluginHtml } }, // Vue ...opts.vue ? vue({ version: opts.vueVersion, ts: opts.ts }) : [], // Vitest ...opts.vitest ? vitest : [], // Prettier { name: "kouts/prettier", ...pluginPrettier } ]; return linterConfig; }; exports.config = config; exports.plugins = plugins;