diff --git a/README.md b/README.md index 51994add..a9b784fe 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,39 @@ module.exports = { } ``` +### Detect rules from `oxlint.json` + +If you are using flat configuration (eslint >= 9.0), you can use the following config: + +```js +// eslint.config.js +import { buildFromOxlintConfigFile } from 'eslint-plugin-oxlint'; +export default [ + ..., // other plugins + ...buildFromOxlintConfigFile('./oxlint.json'), +]; +``` + +Or build it by an `oxlint.json`-like object: + +```js +// eslint.config.js +import { buildFromOxlintConfig } from 'eslint-plugin-oxlint'; +export default [ + ..., // other plugins + ...buildFromOxlintConfig({ + categories: { + correctness: 'warn' + }, + rules: { + eqeqeq: 'warn' + } + }), +]; +``` + +`buildFromOxlintConfigFile` is not supported for legacy configuration (eslint < 9.0). + ### Run it before eslint And then you can add the following script to your `package.json`: diff --git a/package.json b/package.json index 4b55784b..fe034309 100644 --- a/package.json +++ b/package.json @@ -82,5 +82,8 @@ }, "volta": { "node": "20.14.0" + }, + "dependencies": { + "jsonc-parser": "^3.3.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5859e252..f225d418 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + jsonc-parser: + specifier: ^3.3.1 + version: 3.3.1 devDependencies: '@eslint/js': specifier: ^9.13.0 diff --git a/scripts/constants.ts b/scripts/constants.ts index 21a6b970..cf6e93e9 100644 --- a/scripts/constants.ts +++ b/scripts/constants.ts @@ -13,6 +13,8 @@ export const ignoreScope = new Set(['oxc', 'deepscan', 'security']); // only used for the scopes where the directory structure doesn't reflect the eslint scope // such as `typescript` vs `@typescript-eslint` or others. Eslint as a scope is an exception, // as eslint doesn't have a scope. +// There is a duplicate in src/build-from-oxlint-config.ts, for clean builds we manage it in 2 files. +// In the future we can generate maybe this constant into src folder export const scopeMaps = { eslint: '', typescript: '@typescript-eslint', diff --git a/src/__snapshots__/build-from-oxlint-config.spec.ts.snap b/src/__snapshots__/build-from-oxlint-config.spec.ts.snap new file mode 100644 index 00000000..d4e0c97f --- /dev/null +++ b/src/__snapshots__/build-from-oxlint-config.spec.ts.snap @@ -0,0 +1,187 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`buildFromOxlintConfig > custom plugins, custom categories > customPluginCustomCategories 1`] = ` +[ + { + "name": "oxlint/from-oxlint-config", + "rules": { + "constructor-super": "off", + "getter-return": "off", + "import/export": "off", + "import/no-deprecated": "off", + "import/no-unused-modules": "off", + "no-undef": "off", + "no-unreachable": "off", + }, + }, +] +`; + +exports[`buildFromOxlintConfig > custom plugins, default categories > customPluginDefaultCategories 1`] = ` +[ + { + "name": "oxlint/from-oxlint-config", + "rules": { + "for-direction": "off", + "no-async-promise-executor": "off", + "no-caller": "off", + "no-class-assign": "off", + "no-compare-neg-zero": "off", + "no-cond-assign": "off", + "no-const-assign": "off", + "no-constant-binary-expression": "off", + "no-constant-condition": "off", + "no-control-regex": "off", + "no-debugger": "off", + "no-delete-var": "off", + "no-dupe-class-members": "off", + "no-dupe-else-if": "off", + "no-dupe-keys": "off", + "no-duplicate-case": "off", + "no-empty-character-class": "off", + "no-empty-pattern": "off", + "no-empty-static-block": "off", + "no-ex-assign": "off", + "no-extra-boolean-cast": "off", + "no-func-assign": "off", + "no-global-assign": "off", + "no-import-assign": "off", + "no-invalid-regexp": "off", + "no-irregular-whitespace": "off", + "no-loss-of-precision": "off", + "no-new-native-nonconstructor": "off", + "no-nonoctal-decimal-escape": "off", + "no-obj-calls": "off", + "no-self-assign": "off", + "no-setter-return": "off", + "no-shadow-restricted-names": "off", + "no-sparse-arrays": "off", + "no-this-before-super": "off", + "no-unsafe-finally": "off", + "no-unsafe-negation": "off", + "no-unsafe-optional-chaining": "off", + "no-unused-labels": "off", + "no-unused-private-class-members": "off", + "no-unused-vars": "off", + "no-useless-catch": "off", + "no-useless-escape": "off", + "no-useless-rename": "off", + "no-with": "off", + "require-yield": "off", + "unicorn/no-await-in-promise-methods": "off", + "unicorn/no-document-cookie": "off", + "unicorn/no-empty-file": "off", + "unicorn/no-invalid-remove-event-listener": "off", + "unicorn/no-new-array": "off", + "unicorn/no-single-promise-in-promise-methods": "off", + "unicorn/no-thenable": "off", + "unicorn/no-unnecessary-await": "off", + "unicorn/no-useless-fallback-in-spread": "off", + "unicorn/no-useless-length-check": "off", + "unicorn/no-useless-spread": "off", + "unicorn/prefer-set-size": "off", + "unicorn/prefer-string-starts-ends-with": "off", + "use-isnan": "off", + "valid-typeof": "off", + }, + }, +] +`; + +exports[`buildFromOxlintConfig > default plugins (react, unicorn, typescript), default categories > defaultPluginDefaultCategories 1`] = ` +[ + { + "name": "oxlint/from-oxlint-config", + "rules": { + "@typescript-eslint/no-dupe-class-members": "off", + "@typescript-eslint/no-duplicate-enum-values": "off", + "@typescript-eslint/no-extra-non-null-assertion": "off", + "@typescript-eslint/no-loss-of-precision": "off", + "@typescript-eslint/no-misused-new": "off", + "@typescript-eslint/no-non-null-asserted-optional-chain": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-unsafe-declaration-merging": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-useless-empty-export": "off", + "@typescript-eslint/no-wrapper-object-types": "off", + "@typescript-eslint/prefer-as-const": "off", + "@typescript-eslint/triple-slash-reference": "off", + "for-direction": "off", + "no-async-promise-executor": "off", + "no-caller": "off", + "no-class-assign": "off", + "no-compare-neg-zero": "off", + "no-cond-assign": "off", + "no-const-assign": "off", + "no-constant-binary-expression": "off", + "no-constant-condition": "off", + "no-control-regex": "off", + "no-debugger": "off", + "no-delete-var": "off", + "no-dupe-class-members": "off", + "no-dupe-else-if": "off", + "no-dupe-keys": "off", + "no-duplicate-case": "off", + "no-empty-character-class": "off", + "no-empty-pattern": "off", + "no-empty-static-block": "off", + "no-ex-assign": "off", + "no-extra-boolean-cast": "off", + "no-func-assign": "off", + "no-global-assign": "off", + "no-import-assign": "off", + "no-invalid-regexp": "off", + "no-irregular-whitespace": "off", + "no-loss-of-precision": "off", + "no-new-native-nonconstructor": "off", + "no-nonoctal-decimal-escape": "off", + "no-obj-calls": "off", + "no-self-assign": "off", + "no-setter-return": "off", + "no-shadow-restricted-names": "off", + "no-sparse-arrays": "off", + "no-this-before-super": "off", + "no-unsafe-finally": "off", + "no-unsafe-negation": "off", + "no-unsafe-optional-chaining": "off", + "no-unused-labels": "off", + "no-unused-private-class-members": "off", + "no-unused-vars": "off", + "no-useless-catch": "off", + "no-useless-escape": "off", + "no-useless-rename": "off", + "no-with": "off", + "react/iframe-missing-sandbox": "off", + "react/jsx-key": "off", + "react/jsx-no-duplicate-props": "off", + "react/jsx-no-target-blank": "off", + "react/jsx-no-undef": "off", + "react/jsx-props-no-spread-multi": "off", + "react/no-children-prop": "off", + "react/no-danger-with-children": "off", + "react/no-direct-mutation-state": "off", + "react/no-find-dom-node": "off", + "react/no-is-mounted": "off", + "react/no-render-return-value": "off", + "react/no-string-refs": "off", + "react/void-dom-elements-no-children": "off", + "require-yield": "off", + "unicorn/no-await-in-promise-methods": "off", + "unicorn/no-document-cookie": "off", + "unicorn/no-empty-file": "off", + "unicorn/no-invalid-remove-event-listener": "off", + "unicorn/no-new-array": "off", + "unicorn/no-single-promise-in-promise-methods": "off", + "unicorn/no-thenable": "off", + "unicorn/no-unnecessary-await": "off", + "unicorn/no-useless-fallback-in-spread": "off", + "unicorn/no-useless-length-check": "off", + "unicorn/no-useless-spread": "off", + "unicorn/prefer-set-size": "off", + "unicorn/prefer-string-starts-ends-with": "off", + "use-isnan": "off", + "valid-typeof": "off", + }, + }, +] +`; diff --git a/src/build-from-oxlint-config.spec.ts b/src/build-from-oxlint-config.spec.ts new file mode 100644 index 00000000..6e02db48 --- /dev/null +++ b/src/build-from-oxlint-config.spec.ts @@ -0,0 +1,342 @@ +import { describe, expect, it } from 'vitest'; +import { + buildFromOxlintConfig, + buildFromOxlintConfigFile, +} from './build-from-oxlint-config.js'; +import fs from 'node:fs'; +import { execSync } from 'node:child_process'; +import type { Linter } from 'eslint'; +import { typescriptRulesExtendEslintRules } from '../scripts/constants.js'; + +describe('buildFromOxlintConfig', () => { + describe('rule values', () => { + it('detect active rules inside "rules" scope', () => { + ['error', ['error'], 'warn', ['warn'], 1, [1], 2, [2]].forEach( + (ruleSetting) => { + const rules = buildFromOxlintConfig({ + rules: { + eqeqeq: ruleSetting, + }, + }); + + expect(rules.length).toBe(1); + expect(rules[0].rules).not.toBeUndefined(); + expect('eqeqeq' in rules[0].rules!).toBe(true); + expect(rules[0].rules!.eqeqeq).toBe('off'); + } + ); + }); + + it('skip deactive rules inside "rules" scope', () => { + ['off', ['off'], 0, [0]].forEach((ruleSetting) => { + const rules = buildFromOxlintConfig({ + rules: { + eqeqeq: ruleSetting, + }, + }); + + expect(rules.length).toBe(1); + expect(rules[0].rules).not.toBeUndefined(); + expect('eqeqeq' in rules[0].rules!).toBe(false); + }); + }); + + it('skip invalid rules inside "rules" scope', () => { + ['on', ['on'], 3, [3]].forEach((ruleSetting) => { + const rules = buildFromOxlintConfig({ + rules: { + eqeqeq: ruleSetting, + }, + }); + + expect(rules.length).toBe(1); + expect(rules[0].rules).not.toBeUndefined(); + expect('eqeqeq' in rules[0].rules!).toBe(false); + }); + }); + }); + + it('skip deactivate categories', () => { + expect( + buildFromOxlintConfig({ + categories: { + // correctness is the only category on by default + correctness: 'off', + }, + }) + ).toStrictEqual([ + { + name: 'oxlint/from-oxlint-config', + rules: {}, + }, + ]); + }); + + it('default plugins (react, unicorn, typescript), default categories', () => { + // snapshot because it can change with the next release + expect(buildFromOxlintConfig({})).toMatchSnapshot( + 'defaultPluginDefaultCategories' + ); + }); + + it('custom plugins, default categories', () => { + // snapshot because it can change with the next release + expect( + buildFromOxlintConfig({ + plugins: ['unicorn'], + }) + ).toMatchSnapshot('customPluginDefaultCategories'); + }); + + it('custom plugins, custom categories', () => { + // snapshot because it can change with the next release + expect( + buildFromOxlintConfig({ + plugins: ['import'], + categories: { + nursery: 'warn', + correctness: 'off', + }, + }) + ).toMatchSnapshot('customPluginCustomCategories'); + }); + + it('skip deactivate rules, for custom enable category', () => { + const rules = buildFromOxlintConfig({ + plugins: ['import'], + categories: { + nursery: 'warn', + correctness: 'off', + }, + rules: { + 'import/no-unused-modules': 'off', + }, + }); + expect('import/no-unused-modules' in rules).toBe(false); + }); +}); + +const createConfigFileAndBuildFromIt = ( + filename: string, + content: string +): Linter.Config>[] => { + fs.writeFileSync(filename, content); + + const rules = buildFromOxlintConfigFile(filename); + + fs.unlinkSync(filename); + + return rules; +}; + +describe('buildFromOxlintConfigFile', () => { + it('successfully parse oxlint json config', () => { + const rules = createConfigFileAndBuildFromIt( + 'success-config.json', + `{ + "rules": { + // hello world + "no-await-loop": "error", + }, + }` + ); + + expect(rules.length).toBe(1); + expect(rules[0].rules).not.toBeUndefined(); + expect('no-await-loop' in rules[0].rules!).toBe(true); + }); + + it('fails to find oxlint config', () => { + const rules = buildFromOxlintConfigFile('not-found.json'); + + expect(rules).toStrictEqual([ + { + name: 'oxlint/from-oxlint-config', + }, + ]); + }); + + it('fails to parse invalid json', () => { + const rules = createConfigFileAndBuildFromIt( + 'invalid-json.json', + '["this", is an invalid json format]' + ); + + expect(rules).toStrictEqual([ + { + name: 'oxlint/from-oxlint-config', + }, + ]); + }); + + it('fails to parse invalid oxlint config', () => { + const rules = createConfigFileAndBuildFromIt( + 'invalid-config.json', + JSON.stringify(['this is valid json but not an object']) + ); + + expect(rules).toStrictEqual([ + { + name: 'oxlint/from-oxlint-config', + }, + ]); + }); +}); + +const executeOxlintWithConfiguration = ( + filename: string, + config: { + [key: string]: unknown; + plugins?: string[]; + categories?: Record; + rules?: Record; + } +) => { + fs.writeFileSync(filename, JSON.stringify(config)); + let oxlintOutput: string; + + const cliArguments = [ + `--config=${filename}`, + '--disable-oxc-plugin', + '--silent', + ]; + + // --disabled--plugin can be removed after oxc-project/oxc#6896 + if (config.plugins !== undefined) { + ['typescript', 'unicorn', 'react'].forEach((plugin) => { + if (!config.plugins!.includes(plugin)) { + cliArguments.push(`--disable-${plugin}-plugin`); + } + }); + } + + try { + oxlintOutput = execSync(`npx oxlint ${cliArguments.join(' ')}`, { + encoding: 'utf-8', + stdio: 'pipe', + }); + } catch { + oxlintOutput = ''; + } + + fs.unlinkSync(filename); + + const result = /with\s(\d+)\srules/.exec(oxlintOutput); + + if (result === null) { + return undefined; + } + + return parseInt(result[1], 10) ?? undefined; +}; + +describe('integration test with oxlint', () => { + [ + // default + {}, + // no plugins + { plugins: [] }, + // simple plugin override + { plugins: ['typescript'] }, + // custom rule off + { + rules: { eqeqeq: 'off' }, + }, + // combination plugin + rule + { plugins: ['vite'], rules: { eqeqeq: 'off' } }, + + // categories change + { categories: { correctness: 'off', nusery: 'warn' } }, + // combination plugin + categires + rules + { + plugins: ['vite'], + categories: { correctness: 'off', style: 'warn' }, + rules: { eqeqeq: 'off' }, + }, + // all categories enabled + { + categories: { + correctness: 'warn', + nursery: 'off', // enable after oxc-project/oxc#7073 + pedantic: 'warn', + perf: 'warn', + restriction: 'warn', + style: 'warn', + suspicious: 'warn', + }, + }, + // all plugins enabled + { + plugins: [ + 'typescript', + 'unicorn', + 'react', + 'react-perf', + 'nextjs', + 'import', + 'jsdoc', + 'jsx-a11y', + 'n', + 'promise', + 'jest', + 'vitest', + 'tree_shaking', + ], + }, + // everything on + { + plugins: [ + 'typescript', + 'unicorn', + 'react', + 'react-perf', + 'nextjs', + 'import', + 'jsdoc', + 'jsx-a11y', + 'n', + 'promise', + 'jest', + 'vitest', + 'tree_shaking', + ], + categories: { + correctness: 'warn', + nursery: 'off', // enable after oxc-project/oxc#7073 + pedantic: 'warn', + perf: 'warn', + restriction: 'warn', + style: 'warn', + suspicious: 'warn', + }, + }, + ].forEach((config, index) => { + const fileContent = JSON.stringify(config); + + it(`should output same rule count for: ${fileContent}`, () => { + const oxlintRulesCount = executeOxlintWithConfiguration( + `integration-test-${index}-oxlint.json`, + config + ); + + const eslintRules = buildFromOxlintConfig(config); + + expect(eslintRules.length).toBe(1); + expect(eslintRules[0].rules).not.toBeUndefined(); + + let expectedCount = oxlintRulesCount ?? 0; + + // special mapping for ts alias rules + if ( + config.plugins === undefined || + config.plugins.includes('typescript') + ) { + expectedCount += typescriptRulesExtendEslintRules.filter( + (aliasRule) => aliasRule in eslintRules[0].rules! + ).length; + } + + expect(Object.keys(eslintRules[0].rules!).length).toBe(expectedCount); + }); + }); +}); diff --git a/src/build-from-oxlint-config.ts b/src/build-from-oxlint-config.ts new file mode 100644 index 00000000..d5524556 --- /dev/null +++ b/src/build-from-oxlint-config.ts @@ -0,0 +1,239 @@ +import fs from 'node:fs'; +import configByCategory from './configs-by-category.js'; +import type { Linter } from 'eslint'; +import JSONCParser from 'jsonc-parser'; + +// these are the mappings from the scope in the rules.rs to the eslint scope +// only used for the scopes where the directory structure doesn't reflect the eslint scope +// such as `typescript` vs `@typescript-eslint` or others. Eslint as a scope is an exception, +// as eslint doesn't have a scope. +// There is a duplicate in scripts/constants.js, for clean builds we manage it in 2 files. +// In the future we can generate maybe this constant into src folder +const scopeMaps = { + eslint: '', + typescript: '@typescript-eslint', +}; + +type OxlintConfigPlugins = string[]; + +type OxlintConfigCategories = Record; + +type OxlintConfigRules = Record; + +type OxlintConfig = { + [key: string]: unknown; + plugins?: OxlintConfigPlugins; + categories?: OxlintConfigCategories; + rules?: OxlintConfigRules; +}; + +// default plugins, see +const defaultPlugins: OxlintConfigPlugins = ['react', 'unicorn', 'typescript']; + +// default categories, see +const defaultCategories: OxlintConfigCategories = { correctness: 'warn' }; + +/** + * tries to read the oxlint config file and returning its JSON content. + * if the file is not found or could not be parsed, undefined is returned. + * And an error message will be emitted to `console.error` + */ +const getConfigContent = ( + oxlintConfigFile: string +): OxlintConfig | undefined => { + try { + const content = fs.readFileSync(oxlintConfigFile, 'utf8'); + + try { + const configContent = JSONCParser.parse(content); + + if (typeof configContent !== 'object' || Array.isArray(configContent)) { + throw new TypeError('not an valid config file'); + } + + return configContent; + } catch { + console.error( + `eslint-plugin-oxlint: could not parse oxlint config file: ${oxlintConfigFile}` + ); + return undefined; + } + } catch { + console.error( + `eslint-plugin-oxlint: could not find oxlint config file: ${oxlintConfigFile}` + ); + return undefined; + } +}; + +/** + * appends all rules which are enabled by a plugin and falls into a specific category + */ +const handleCategoriesScope = ( + plugins: OxlintConfigPlugins, + categories: OxlintConfigCategories, + rules: Record +): void => { + for (const category in categories) { + const configName = `flat/${category}`; + + // category is not enabled or not in found categories + if (categories[category] === 'off' || !(configName in configByCategory)) { + continue; + } + + // @ts-ignore -- come on TS, we are checking if the configName exists in the configByCategory + const possibleRules = configByCategory[configName].rules; + + // iterate to each rule to check if the rule can be appended, because the plugin is activated + Object.keys(possibleRules).forEach((rule) => { + plugins.forEach((plugin) => { + // @ts-ignore -- come on TS, we are checking if the plugin exists in the configByscopeMapsCategory + const pluginPrefix = plugin in scopeMaps ? scopeMaps[plugin] : plugin; + + // the rule has no prefix, so it is a eslint one + if (pluginPrefix === '' && !rule.includes('/')) { + rules[rule] = 'off'; + // other rules with a prefix like @typescript-eslint/ + } else if (rule.startsWith(`${pluginPrefix}/`)) { + rules[rule] = 'off'; + } + }); + }); + } +}; + +/** + * checks if the oxlint rule is activated/deactivated and append/remove it. + */ +const handleRulesScope = ( + oxlintRules: OxlintConfigRules, + rules: Record +): void => { + for (const rule in oxlintRules) { + // is this rules not turned off + if (isActiveValue(oxlintRules[rule])) { + rules[rule] = 'off'; + } else if (rule in rules && isDeactivateValue(oxlintRules[rule])) { + // rules extended by categories or plugins can be disabled manually + delete rules[rule]; + } + } +}; + +/** + * check if the value is "off", 0, ["off", ...], or [0, ...] + */ +const isDeactivateValue = (value: unknown): boolean => { + const isOff = (value: unknown) => value === 'off' || value === 0; + + return isOff(value) || (Array.isArray(value) && isOff(value[0])); +}; + +/** + * check if the value is "error", "warn", 1, 2, ["error", ...], ["warn", ...], [1, ...], or [2, ...] + */ +const isActiveValue = (value: unknown): boolean => { + const isOn = (value: unknown) => + value === 'error' || value === 'warn' || value === 1 || value === 2; + + return isOn(value) || (Array.isArray(value) && isOn(value[0])); +}; + +/** + * tries to return the "plugins" section from the config. + * it returns `undefined` when not found or invalid. + */ +const readPluginsFromConfig = ( + config: OxlintConfig +): OxlintConfigPlugins | undefined => { + return 'plugins' in config && Array.isArray(config.plugins) + ? (config.plugins as OxlintConfigPlugins) + : undefined; +}; + +/** + * tries to return the "categories" section from the config. + * it returns `undefined` when not found or invalid. + */ +const readCategoriesFromConfig = ( + config: OxlintConfig +): OxlintConfigCategories | undefined => { + return 'categories' in config && + typeof config.categories === 'object' && + config.categories !== null + ? (config.categories as OxlintConfigCategories) + : undefined; +}; + +/** + * tries to return the "rules" section from the config. + * it returns `undefined` when not found or invalid. + */ +const readRulesFromConfig = ( + config: OxlintConfig +): OxlintConfigRules | undefined => { + return 'rules' in config && + typeof config.rules === 'object' && + config.rules !== null + ? (config.rules as OxlintConfigRules) + : undefined; +}; + +/** + * builds turned off rules, which are supported by oxlint. + * It accepts an object similar to the oxlint.json file. + */ +export const buildFromOxlintConfig = ( + config: OxlintConfig +): Linter.Config>[] => { + const rules: Record = {}; + const plugins = readPluginsFromConfig(config) ?? defaultPlugins; + + // it is not a plugin but it is activated by default + plugins.push('eslint'); + + handleCategoriesScope( + plugins, + readCategoriesFromConfig(config) ?? defaultCategories, + rules + ); + + const configRules = readRulesFromConfig(config); + + if (configRules !== undefined) { + handleRulesScope(configRules, rules); + } + + return [ + { + name: 'oxlint/from-oxlint-config', + rules, + }, + ]; +}; + +/** + * builds turned off rules, which are supported by oxlint. + * It accepts an filepath to the oxlint.json file. + * + * It the oxlint.json file could not be found or parsed, + * no rules will be deactivated and an error to `console.error` will be emitted + */ +export const buildFromOxlintConfigFile = ( + oxlintConfigFile: string +): Linter.Config>[] => { + const config = getConfigContent(oxlintConfigFile); + + // we could not parse form the file, do not build with default values + // we can not be sure if the setup is right + if (config === undefined) { + return [ + { + name: 'oxlint/from-oxlint-config', + }, + ]; + } + + return buildFromOxlintConfig(config); +}; diff --git a/src/index.ts b/src/index.ts index 99d78efe..84351bbd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,11 @@ import * as ruleMapsByCategory from './rules-by-category.js'; import configByScope from './configs-by-scope.js'; import configByCategory from './configs-by-category.js'; +export { + buildFromOxlintConfig, + buildFromOxlintConfigFile, +} from './build-from-oxlint-config.js'; + type UnionToIntersection = (U extends any ? (x: U) => void : never) extends ( x: infer I ) => void diff --git a/vite.config.ts b/vite.config.ts index 43fef223..82b1e18e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,7 @@ import dts from 'vite-plugin-dts'; export default defineConfig({ test: { coverage: { - exclude: ['lib'], + include: ['src', 'scripts'], }, }, build: {