diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..69e44ca --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Normalize line endings to LF on checkout across platforms so the oxfmt +# snapshot tests produce identical output on Windows runners (default +# autocrlf=true would otherwise check out fixtures as CRLF). +* text=auto eol=lf diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml new file mode 100644 index 0000000..49bcabb --- /dev/null +++ b/.github/workflows/ci-module.yml @@ -0,0 +1,13 @@ +name: ci + +on: + push: + branches: + - master + - next + pull_request: + workflow_dispatch: + +jobs: + test: + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cba0217 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +**/node_modules +**/package-lock.json +pnpm-lock.yaml + +coverage + +**/.DS_Store +**/._* + +**/*.pem + +**/.vs +**/.vscode +**/.idea diff --git a/API.md b/API.md new file mode 100644 index 0000000..9f2995e --- /dev/null +++ b/API.md @@ -0,0 +1,141 @@ +## Configurations + +### `@hapi/oxc-plugin/oxlint` + +The oxlint configuration containing hapi style guide rules and config. To use in your project, add +[`@hapi/oxc-plugin`](https://github.com/hapijs/oxc-plugin) to your `package.json`, then in your `oxlint.config.ts` add: + +```ts +import { defineConfig } from 'oxlint'; +import DefaultOxlintConfig from '@hapi/oxc-plugin/oxlint'; + +export default defineConfig({ + extends: [DefaultOxlintConfig], + env: { + ...DefaultOxlintConfig.env, + }, +}); +``` + +### `@hapi/oxc-plugin/oxfmt` + +The [oxfmt](https://oxc.rs/docs/guide/usage/formatter/oxfmt.html) configuration containing hapi style guide formatting rules. To use in your project, add +[`@hapi/oxc-plugin`](https://github.com/hapijs/oxc-plugin) to your `package.json`, then in your `oxfmt.config.ts` add: + +```ts +import { defineConfig } from 'oxfmt'; +import DefaultOxfmtConfig from '@hapi/oxc-plugin/oxfmt'; + +export default defineConfig({ + ...DefaultOxfmtConfig, +}); +``` + +## Rules + +This plugin includes the following Oxlint rules: + +### capitalize-modules + +Enforces capitalization of imported module variables. + +#### `global-scope-only` + +If the string `'global-scope-only'` is passed as the first option to this rule, +then it will only be enforced on assignments in the module's top level scope. + +### for-loop + +Enforces `for` loop iterator variable rules and restricts loop nesting depth. + +This rule enforces the following: + +- Restrict iterator variable names. `for` loop iterator variables should be named `i`. Nested loops should use the variables `j`, `k`, and so on. +- Restrict loop nesting. You can restrict the maximum nesting of `for` loops. By default, this limit is three. +- Prevent postfix increment and decrement operators. The hapi style guide does not allow postfix increment and decrement operators in `for` loop updates. The prefix version of these operators should be used instead. +- Single variable declaration in initialization section. A single `var i = 0;` is allowed in the initialization section. This only applies to variable declarations, not assignments to existing variables. This means that `for (i = 0, j = 0)` is allowed if `i` and `j` are existing variables. Variable declarations involving destructuring are not allowed. + +#### Rule options + +This rule can be configured by providing a single options object. The object supports the following keys. + +##### `maxDepth` + +A number representing the maximum allowed nesting of `for` loops. Defaults to three. + +##### `startIterator` + +The first variable iterator name to use. This defaults to `'i'`. + +### no-var + +Enforces the usage of var declarations only in try-catch scope. + +### no-arrowception + +Prevents arrow functions that implicitly create additional arrow functions. + +This rule prevents the pattern () => () => () => ...;. + +Functions can still be returned by arrow functions whose bodies use curly braces and an explicit return. + +This rule does not accept any configuration options. + +### consistent-this + +Enforces consistent naming when capturing the current execution context in a variable. + +The rule takes a list of permitted alias names. In the recommended configuration the only permitted alias is +`self`, so `const self = this;` is allowed while `const that = this;` is reported. It also reports a permitted +alias that is assigned a value other than `this` (for example `const self = 5;`). + +Checks are performed at assignment sites. An alias that is declared but never assigned `this` (for example a +bare `let self;`) is not reported. + +### one-var + +Requires each variable to be declared in its own statement. Combined declarations such as `let a, b;` or +`const x = 1, y = 2;` are reported and should be split into separate statements. + +This rule is configured with the string `'never'` in the recommended configuration. + +## Vitest plugin + +This plugin also exposes a [Vitest](https://vitest.dev) plugin at `@hapi/oxc-plugin/vitest`. It runs +the oxlint and oxfmt checks as a dedicated test project, so linting and formatting violations surface +as failing tests in your existing test run. + +The Vitest plugin requires `vitest` >= 4 in your project. It is an optional part of this package — the +`oxlint` and `oxfmt` configurations work without `vitest` installed — so `vitest` is not declared as a +peer dependency. + +To use it, add [`@hapi/oxc-plugin`](https://github.com/hapijs/oxc-plugin) to your `package.json`, then +in your `vitest.config.js` add: + +```js +import { defineConfig } from 'vitest/config'; +import oxc from '@hapi/oxc-plugin/vitest'; + +export default defineConfig({ + plugins: [oxc()], +}); +``` + +When run, the plugin checks for an existing oxlint or oxfmt configuration in the working directory. If +none is found, it falls back to the configurations shipped by this plugin. + +### Options + +The plugin accepts a single options object with the following keys. + +#### `oxlint` + +A boolean controlling whether the oxlint check runs. Defaults to `true`. + +#### `oxfmt` + +A boolean controlling whether the oxfmt check runs. Defaults to `true`. + +#### `cwd` + +The working directory the checks run against. Defaults to `process.cwd()`. diff --git a/LICENSE.md b/LICENSE.md new file mode 100755 index 0000000..060bbf0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,11 @@ +Copyright (c) 2019-2020, Sideway Inc, and project contributors +Copyright (c) 2015-2019 Continuation Labs and Colin J. Ihrig +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +- The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS OFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..657417e --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ + + +# @hapi/oxc-plugin + +#### Oxlint & Oxfmt plugin containing hapi style guide rules and config. + +**oxc-plugin** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together. + +### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support + +## Useful resources + +- [Documentation and API](https://hapi.dev/family/oxc-plugin/) +- [Version status](https://hapi.dev/resources/status/#oxc-plugin) (builds, dependencies, node versions, licenses, eol) +- [Changelog](https://hapi.dev/family/oxc-plugin/changelog/) +- [Project policies](https://hapi.dev/policies/) +- [Free and commercial support options](https://hapi.dev/support/) diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..5ee924c --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'oxfmt'; + +import DefaultOxfmtConfig from './src/configs/oxfmt.config.js'; + +export default defineConfig({ + ...DefaultOxfmtConfig, + ignorePatterns: [...DefaultOxfmtConfig.ignorePatterns, 'test/configs/fixtures/**'], +}); diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..18affe6 --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,15 @@ +import HapiRecommended from '@hapi/oxc-plugin/oxlint'; +import { defineConfig } from 'oxlint'; + +export default defineConfig({ + extends: [HapiRecommended], + env: { + ...HapiRecommended.env, + }, + globals: { + __HAPI_OXC_OXLINT__: 'readonly', + __HAPI_OXC_OXFMT__: 'readonly', + __HAPI_OXC_CWD__: 'readonly', + }, + ignorePatterns: ['test/configs/fixtures/**'], +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..aafbba3 --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "@hapi/oxc-plugin", + "version": "1.0.0", + "description": "Oxlint & Oxfmt plugin containing hapi style guide rules and config", + "keywords": [ + "hapi", + "lint", + "oxfmt", + "oxlint" + ], + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "git+https://github.com/hapijs/oxc-plugin.git" + }, + "files": [ + "src" + ], + "type": "module", + "main": "src/index.js", + "types": "src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.d.ts", + "import": "./src/index.js" + }, + "./oxlint": { + "types": "./src/configs/recommended.d.ts", + "import": "./src/configs/recommended.js" + }, + "./oxfmt": { + "types": "./src/configs/oxfmt.config.d.ts", + "import": "./src/configs/oxfmt.config.js" + }, + "./vitest": { + "types": "./src/vitest.d.ts", + "import": "./src/vitest.js" + } + }, + "scripts": { + "test": "vitest run", + "lint": "oxlint && oxfmt --check", + "lint:fix": "oxlint --fix && oxfmt --write" + }, + "devDependencies": { + "@hapi/oxc-plugin": "file:.", + "@oxlint/plugins": "1.68.0", + "@vitest/coverage-v8": "^4.1.8", + "oxfmt": "^0.53.0", + "oxlint": "^1.68.0", + "vitest": "^4.1.8" + }, + "peerDependencies": { + "oxfmt": ">=0.53.0", + "oxlint": ">=1.68.0" + }, + "engines": { + "node": ">=22" + } +} diff --git a/src/configs/oxfmt.config.d.ts b/src/configs/oxfmt.config.d.ts new file mode 100644 index 0000000..212458e --- /dev/null +++ b/src/configs/oxfmt.config.d.ts @@ -0,0 +1,5 @@ +import type { OxfmtConfig } from 'oxfmt'; + +declare const config: OxfmtConfig; + +export default config; diff --git a/src/configs/oxfmt.config.js b/src/configs/oxfmt.config.js new file mode 100644 index 0000000..5c169a4 --- /dev/null +++ b/src/configs/oxfmt.config.js @@ -0,0 +1,39 @@ +import { defineConfig } from 'oxfmt'; + +export default defineConfig({ + // Non-source files that should not be reformatted in consumer repos. oxfmt + // otherwise rewrites LICENSE.md and CI workflow YAML on first adoption. + ignorePatterns: ['LICENSE.md', '.github/**'], + singleQuote: true, + tabWidth: 4, + printWidth: 120, + jsdoc: {}, + sortImports: { + ignoreCase: true, + newlinesBetween: false, + sortSideEffects: true, + groups: [ + 'side_effect', + { newlinesBetween: true }, + 'value-builtin', + { newlinesBetween: true }, + 'external', + { newlinesBetween: true }, + 'internal', + { newlinesBetween: true }, + 'value-index', + 'sibling', + 'parent', + { newlinesBetween: true }, + 'type-builtin', + 'type-external', + 'type-internal', + 'type-index', + 'type-sibling', + 'type-parent', + 'type-import', + 'unknown', + ], + }, + sortPackageJson: true, +}); diff --git a/src/configs/recommended.d.ts b/src/configs/recommended.d.ts new file mode 100644 index 0000000..0c2aa87 --- /dev/null +++ b/src/configs/recommended.d.ts @@ -0,0 +1,5 @@ +import type { OxlintConfig } from 'oxlint'; + +declare const config: OxlintConfig; + +export default config; diff --git a/src/configs/recommended.js b/src/configs/recommended.js new file mode 100644 index 0000000..94ec8ac --- /dev/null +++ b/src/configs/recommended.js @@ -0,0 +1,77 @@ +export default { + jsPlugins: ['@hapi/oxc-plugin'], + plugins: ['node', 'unicorn'], + env: { + builtin: true, + node: true, + }, + rules: { + // Custom rules + '@hapi/capitalize-modules': ['warn', 'global-scope-only'], + '@hapi/for-loop': ['warn', { maxDepth: 3, startIterator: 'i' }], + '@hapi/no-var': 'error', + '@hapi/no-arrowception': 'error', + '@hapi/consistent-this': ['error', 'self'], + '@hapi/one-var': ['error', 'never'], + + 'no-constant-condition': 'error', + 'no-undef': ['error', { typeof: false }], + 'no-shadow': ['warn', { allow: ['err', 'done'] }], + 'no-unused-vars': [ + 'warn', + { vars: 'all', caughtErrors: 'all', varsIgnorePattern: '^internals$', args: 'none' }, + ], + 'no-eq-null': 'warn', + 'no-extend-native': 'warn', + 'no-redeclare': 'warn', + 'no-loop-func': 'warn', + yoda: ['warn', 'never'], + 'sort-vars': 'warn', + 'no-array-constructor': 'error', + eqeqeq: 'error', + curly: ['error', 'all'], + 'no-eval': 'error', + 'no-else-return': 'error', + 'no-return-assign': 'error', + 'no-new-wrappers': 'error', + 'no-ex-assign': 'error', + 'prefer-const': ['error', { destructuring: 'all' }], + // Off for ESM: packages use exported `function` declarations (`export function foo() {}`), + // not the CJS-era `const foo = function () {}` expression form. + 'func-style': 'off', + 'no-unsafe-finally': 'error', + 'no-useless-computed-key': 'error', + 'require-await': 'error', + 'constructor-super': 'error', + 'no-caller': 'error', + 'no-const-assign': 'error', + 'no-dupe-class-members': 'error', + 'no-class-assign': 'warn', + 'no-this-before-super': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'no-useless-call': 'error', + 'no-dupe-keys': 'error', + 'prefer-arrow-callback': 'error', + 'object-shorthand': ['error', 'properties'], + 'node/handle-callback-err': ['error', '^(e|err|error)$'], + 'unicorn/no-new-buffer': 'error', + 'typescript/dot-notation': 'warn', + 'eslint/no-object-constructor': 'error', + 'eslint/no-new-native-nonconstructor': 'error', + 'typescript/consistent-return': 'off', + 'eslint/vars-on-top': 'off', + 'eslint/new-cap': 'off', + 'eslint/no-console': 'off', + 'eslint/no-empty': 'off', + 'eslint/no-global-assign': 'off', + 'unicorn/no-process-exit': 'off', + 'eslint/no-unused-expressions': 'off', + 'eslint/no-regex-spaces': 'off', + 'eslint/no-lonely-if': 'off', + 'eslint/no-sparse-arrays': 'error', + // Off: hapi style deliberately over-escapes characters in regex character classes + // and literals for readability (e.g. `[\!\:\|]`). oxlint enables this by default. + 'no-useless-escape': 'off', + }, +}; diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..42fcec2 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,5 @@ +import type { Plugin } from '@oxlint/plugins'; + +declare const plugin: Plugin; + +export default plugin; diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..c3e2ec0 --- /dev/null +++ b/src/index.js @@ -0,0 +1,20 @@ +import CapitalizeModules from './rules/capitalize-modules.js'; +import ConsistentThis from './rules/consistent-this.js'; +import ForLoop from './rules/for-loop.js'; +import NoArrowception from './rules/no-arrowception.js'; +import NoVar from './rules/no-var.js'; +import OneVar from './rules/one-var.js'; + +export default { + meta: { + name: '@hapi', + }, + rules: { + 'capitalize-modules': CapitalizeModules, + 'for-loop': ForLoop, + 'no-var': NoVar, + 'no-arrowception': NoArrowception, + 'consistent-this': ConsistentThis, + 'one-var': OneVar, + }, +}; diff --git a/src/rules/capitalize-modules.js b/src/rules/capitalize-modules.js new file mode 100644 index 0000000..7a2f809 --- /dev/null +++ b/src/rules/capitalize-modules.js @@ -0,0 +1,53 @@ +export default { + meta: { + type: 'suggestion', + docs: { + description: 'enforce the capitalization of imported module variables', + category: 'Stylistic Issues', + recommended: true, + }, + schema: [ + { + enum: ['global-scope-only'], + }, + ], + messages: { + notCapitalized: 'Imported module variable name not capitalized.', + }, + }, + createOnce(context) { + const check = (node) => { + const globalScopeOnly = context.options[0] === 'global-scope-only'; + + if (globalScopeOnly && context.sourceCode.getScope(node).type !== 'module') { + return; + } + + const name = node.local ? node.local.name : node.id.name; + if (name[0] !== name[0].toUpperCase()) { + context.report({ + node: node.local || node.id, + messageId: 'notCapitalized', + data: { name }, + }); + } + }; + + const checkVariable = (node) => { + if ( + node.init && + node.init.type === 'AwaitExpression' && + node.init.argument.type === 'ImportExpression' && + node.id.type === 'Identifier' + ) { + check(node); + } + }; + + return { + ImportDefaultSpecifier: check, + ImportNamespaceSpecifier: check, + VariableDeclarator: checkVariable, + }; + }, +}; diff --git a/src/rules/consistent-this.js b/src/rules/consistent-this.js new file mode 100644 index 0000000..8a2d4c5 --- /dev/null +++ b/src/rules/consistent-this.js @@ -0,0 +1,58 @@ +export default { + meta: { + type: 'suggestion', + docs: { + description: 'enforce consistent naming when capturing the current execution context', + category: 'Stylistic Issues', + recommended: true, + }, + schema: { + type: 'array', + items: { + type: 'string', + minLength: 1, + }, + uniqueItems: true, + }, + messages: { + aliasNotAssignedToThis: "Designated alias '{{name}}' is not assigned to 'this'.", + unexpectedAlias: "Unexpected alias '{{name}}' for 'this'.", + }, + }, + createOnce(context) { + // Checks that a designated alias is only assigned `this`, and that `this` + // is only ever assigned to a designated alias. The scope-level check for an + // alias declared but never assigned `this` (e.g. `let self;`) is omitted — + // assignment sites cover the hapi pattern `const self = this`. + + const checkAssignment = function (node, name, value) { + const isThis = value.type === 'ThisExpression'; + const isAlias = context.options.includes(name); + + if (isAlias && (!isThis || (node.operator && node.operator !== '='))) { + context.report({ node, messageId: 'aliasNotAssignedToThis', data: { name } }); + return; + } + + if (!isAlias && isThis) { + context.report({ node, messageId: 'unexpectedAlias', data: { name } }); + } + }; + + return { + VariableDeclarator(node) { + const id = node.id; + const isDestructuring = id.type === 'ArrayPattern' || id.type === 'ObjectPattern'; + + if (node.init !== null && !isDestructuring) { + checkAssignment(node, id.name, node.init); + } + }, + AssignmentExpression(node) { + if (node.left.type === 'Identifier') { + checkAssignment(node, node.left.name, node.right); + } + }, + }; + }, +}; diff --git a/src/rules/for-loop.js b/src/rules/for-loop.js new file mode 100644 index 0000000..2c62054 --- /dev/null +++ b/src/rules/for-loop.js @@ -0,0 +1,102 @@ +export default { + meta: { + type: 'suggestion', + docs: { + description: 'enforce for loop syntax', + category: 'Stylistic Issues', + recommended: true, + }, + schema: [ + { + type: 'object', + properties: { + maxDepth: { + type: 'integer', + }, + startIterator: { + type: 'string', + }, + }, + additionalProperties: false, + }, + ], + messages: { + depthExceeded: 'Too many nested for loops.', + singleInit: 'Only one variable can be initialized per loop.', + singleVar: 'Left hand side of initializer must be a single variable.', + badIter: "Expected iterator '{{designatedIter}}', but got '{{iteratorVar}}'.", + usePrefixOp: 'Update to iterator should use prefix operator.', + }, + }, + createOnce(context) { + let stack = []; + + const check = function (node) { + const options = context.options[0] || {}; + const maxDepth = options.maxDepth || 3; + const startIterator = options.startIterator || 'i'; + + const getIteratorVariable = function (offset) { + return String.fromCharCode(startIterator.charCodeAt(0) + offset); + }; + + stack.push(node); + + // Make sure that for loops are not nested excessively + + if (stack.length > maxDepth) { + context.report({ node, messageId: 'depthExceeded' }); + } + + const init = node.init; + if (init !== null && init.type === 'VariableDeclaration') { + // Verify that there is 1 initialized variable at most + + if (init.declarations.length > 1) { + context.report({ node, messageId: 'singleInit' }); + } + + const declaration = init.declarations[0]; + + // Verify that this is a normal variable declaration, not destructuring + + if (declaration.id.type !== 'Identifier') { + context.report({ node, messageId: 'singleVar' }); + } else { + const iteratorVar = declaration.id.name; + const designatedIter = getIteratorVariable(stack.length - 1); + + // Verify that the iterator variable has the expected value + + if (iteratorVar !== designatedIter) { + context.report({ + node, + messageId: 'badIter', + data: { designatedIter, iteratorVar }, + }); + } + } + } + + const update = node.update; + + // Verify that postfix increment/decrement are not used + + if (update && update.type === 'UpdateExpression' && !update.prefix) { + context.report({ node, messageId: 'usePrefixOp' }); + } + }; + + const popStack = function () { + stack.pop(); + }; + + return { + before() { + stack = []; + }, + ForStatement: check, + 'ForStatement:exit': popStack, + }; + }, +}; diff --git a/src/rules/no-arrowception.js b/src/rules/no-arrowception.js new file mode 100644 index 0000000..611c515 --- /dev/null +++ b/src/rules/no-arrowception.js @@ -0,0 +1,27 @@ +export default { + meta: { + type: 'problem', + docs: { + description: 'prevent arrow functions that implicitly create arrow functions', + category: 'ECMAScript 6', + recommended: true, + }, + schema: [], + messages: { + implicitCreate: 'Arrow function implicitly creates arrow function.', + }, + }, + createOnce(context) { + const check = function (node) { + const fnBody = node.body; + + if (fnBody.type === 'ArrowFunctionExpression') { + context.report({ node, messageId: 'implicitCreate' }); + } + }; + + return { + ArrowFunctionExpression: check, + }; + }, +}; diff --git a/src/rules/no-var.js b/src/rules/no-var.js new file mode 100644 index 0000000..317e9e1 --- /dev/null +++ b/src/rules/no-var.js @@ -0,0 +1,71 @@ +// Based on https://github.com/eslint/eslint/blob/master/lib/rules/no-var.js + +const internals = { + scopeTypes: new Set([ + 'Program', + 'BlockStatement', + 'SwitchStatement', + 'ForStatement', + 'ForInStatement', + 'ForOfStatement', + ]), +}; + +export default { + meta: { + type: 'suggestion', + docs: { + description: 'require `let` or `const` instead of `var` when outside of try...catch', + category: 'ECMAScript 6', + recommended: true, + }, + schema: [], + messages: { + unexpectedVar: 'Unexpected var, use let or const instead.', + }, + }, + createOnce(context) { + const check = (node) => { + const sourceCode = context.sourceCode; + + if ( + node.parent.parent && + (node.parent.parent.type === 'TryStatement' || node.parent.parent.type === 'CatchClause') + ) { + const variables = sourceCode.getDeclaredVariables(node, context); + const scopeNode = internals.getScopeNode(node); + if (variables.some(internals.isUsedFromOutsideOf(scopeNode))) { + return; + } + } + + context.report({ node, messageId: 'unexpectedVar' }); + }; + + return { + 'VariableDeclaration:exit'(node) { + if (node.kind === 'var') { + check(node); + } + }, + }; + }, +}; + +internals.getScopeNode = function (node) { + if (internals.scopeTypes.has(node.type)) { + return node; + } + + return internals.getScopeNode(node.parent); +}; + +internals.isUsedFromOutsideOf = function (scopeNode) { + const isOutsideOfScope = (reference) => { + const scope = scopeNode.range; + const id = reference.identifier.range; + return id[0] < scope[0] || id[1] > scope[1]; + }; + + return (variable) => variable.references.some(isOutsideOfScope); +}; diff --git a/src/rules/one-var.js b/src/rules/one-var.js new file mode 100644 index 0000000..310a2bb --- /dev/null +++ b/src/rules/one-var.js @@ -0,0 +1,31 @@ +export default { + meta: { + type: 'suggestion', + docs: { + description: 'require variables to be declared in separate statements', + category: 'Stylistic Issues', + recommended: true, + }, + schema: [ + { + enum: ['never'], + }, + ], + messages: { + split: "Split '{{type}}' declarations into multiple statements.", + }, + }, + createOnce(context) { + const check = function (node) { + // `never`: each variable must be declared in its own statement. + + if (node.declarations.length > 1) { + context.report({ node, messageId: 'split', data: { type: node.kind } }); + } + }; + + return { + VariableDeclaration: check, + }; + }, +}; diff --git a/src/vitest.d.ts b/src/vitest.d.ts new file mode 100644 index 0000000..c868063 --- /dev/null +++ b/src/vitest.d.ts @@ -0,0 +1,27 @@ +import type { Plugin } from 'vitest/config'; + +export interface Options { + /** + * Run the oxlint checks. + * + * @default true + */ + oxlint?: boolean; + /** + * Run the oxfmt checks. + * + * @default true + */ + oxfmt?: boolean; + /** + * Working directory the checks run against. + * + * @default process.cwd() + */ + cwd?: string; +} + +/** Vitest plugin that runs the hapi oxlint and oxfmt checks as a test project. */ +declare function plugin(options?: Options): Plugin; + +export default plugin; diff --git a/src/vitest.js b/src/vitest.js new file mode 100644 index 0000000..253182f --- /dev/null +++ b/src/vitest.js @@ -0,0 +1,22 @@ +import * as Path from 'node:path'; + +export default function (options = {}) { + const { oxlint = true, oxfmt = true, cwd } = options; + + return { + name: '@hapi/oxc-plugin:vitest', + async configureVitest({ injectTestProjects }) { + await injectTestProjects({ + define: { + __HAPI_OXC_OXLINT__: oxlint, + __HAPI_OXC_OXFMT__: oxfmt, + __HAPI_OXC_CWD__: JSON.stringify(cwd ?? null), + }, + test: { + name: '@hapi/oxc-plugin', + include: [Path.join(import.meta.dirname, 'vitest.test.js')], + }, + }); + }, + }; +} diff --git a/src/vitest.test.js b/src/vitest.test.js new file mode 100644 index 0000000..ef0eb4b --- /dev/null +++ b/src/vitest.test.js @@ -0,0 +1,64 @@ +/* global __HAPI_OXC_OXLINT__, __HAPI_OXC_OXFMT__, __HAPI_OXC_CWD__ */ + +import { execFile } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { delimiter, join, resolve } from 'node:path'; +import { promisify } from 'node:util'; + +import { describe, it } from 'vitest'; + +const internals = {}; + +internals.execFile = promisify(execFile); + +internals.cwd = __HAPI_OXC_CWD__ ?? process.cwd(); + +internals.oxlintConfigs = ['oxlint.config.ts', 'oxlint.config.js', 'oxlint.config.json', '.oxlintrc.json']; +internals.oxfmtConfigs = ['oxfmt.config.ts', 'oxfmt.config.js', 'oxfmt.config.json']; + +internals.hasConfig = function (files) { + for (const file of files) { + if (existsSync(resolve(internals.cwd, file))) { + return true; + } + } + + return false; +}; + +internals.exec = function (command, args) { + const env = { ...process.env }; + env.PATH = `${resolve(internals.cwd, 'node_modules/.bin')}${delimiter}${env.PATH}`; + + return internals.execFile(command, args, { encoding: 'utf8', cwd: internals.cwd, env }); +}; + +describe('@hapi/oxc-plugin', () => { + it.skipIf(!__HAPI_OXC_OXLINT__)('oxlint', async () => { + const args = []; + if (!internals.hasConfig(internals.oxlintConfigs)) { + args.push('-c', join(import.meta.dirname, 'configs', 'recommended.js')); + } + + try { + await internals.exec('oxlint', args); + } catch (err) { + throw new Error(`oxlint check failed:\n${err.stdout + err.stderr}`); + } + }); + + it.skipIf(!__HAPI_OXC_OXFMT__)('oxfmt', async () => { + const args = ['--check']; + if (!internals.hasConfig(internals.oxfmtConfigs)) { + args.push('-c', join(import.meta.dirname, 'configs', 'oxfmt.config.js')); + } + + args.push('.'); + + try { + await internals.exec('oxfmt', args); + } catch (err) { + throw new Error(`oxfmt check failed:\n${err.stdout + err.stderr}`); + } + }); +}); diff --git a/test/configs/__snapshots__/recommended.js.snap b/test/configs/__snapshots__/recommended.js.snap new file mode 100644 index 0000000..1c2c0ec --- /dev/null +++ b/test/configs/__snapshots__/recommended.js.snap @@ -0,0 +1,106 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`recommended config > oxfmt > formats arrow-parens.js correctly 1`] = ` +"/* eslint-disable no-unused-vars */ +const foo = (bar) => { + return bar + 1; +}; + +const baz = (quux) => { + return quux + 1; +}; +" +`; + +exports[`recommended config > oxfmt > formats arrow-spacing.js correctly 1`] = ` +"/* eslint-disable no-unused-vars */ +const foo = (bar) => { + return bar + 1; +}; + +const baz = (quux) => { + return quux + 1; +}; + +const fn = (arg) => { + return arg + 1; +}; +" +`; + +exports[`recommended config > oxfmt > formats indent.js correctly 1`] = ` +"export const foo = function (value) { + return value + 1; +}; + +export const bar = function (value) { + return value + 1; +}; +" +`; + +exports[`recommended config > oxfmt > formats indent-switch-case.js correctly 1`] = ` +"const foo = 'foo'; +let result = 0; + +switch (foo) { + case 'foo': + result = 1; + break; + + case 'bar': + result = 2; + break; + case 'baz': + result = 3; + break; +} + +result.toString(); +" +`; + +exports[`recommended config > oxfmt > formats no-extra-semi.js correctly 1`] = ` +"/* eslint-disable padding-line-between-statements */ + +export const foo = function () { + try { + } catch {} +}; +" +`; + +exports[`recommended config > oxfmt > formats semi.js correctly 1`] = ` +"export const foo = function () { + return 42; +}; + +export const bar = function () { + return 85; +}; +" +`; + +exports[`recommended config > oxfmt > formats space-before-blocks.js correctly 1`] = ` +"/* eslint-disable no-unused-vars */ + +const foo = function () {}; + +const bar = function () {}; + +const baz = function () {}; +" +`; + +exports[`recommended config > oxfmt > formats space-before-function-paren.js correctly 1`] = ` +"/* eslint-disable no-unused-vars */ + +const foo = function () {}; + +const bar = function () {}; + +const baz = function baz() {}; + +const quux = function quux() {}; +" +`; diff --git a/test/configs/fixtures/arrow-parens.js b/test/configs/fixtures/arrow-parens.js new file mode 100644 index 0000000..d3264ea --- /dev/null +++ b/test/configs/fixtures/arrow-parens.js @@ -0,0 +1,10 @@ +/* eslint-disable no-unused-vars */ +const foo = bar => { + + return bar + 1; +}; + +const baz = (quux) => { + + return quux + 1; +}; diff --git a/test/configs/fixtures/arrow-spacing.js b/test/configs/fixtures/arrow-spacing.js new file mode 100644 index 0000000..e6f4959 --- /dev/null +++ b/test/configs/fixtures/arrow-spacing.js @@ -0,0 +1,15 @@ +/* eslint-disable no-unused-vars */ +const foo = (bar)=> { + + return bar + 1; +}; + +const baz = (quux) =>{ + + return quux + 1; +}; + +const fn = (arg) => { + + return arg + 1; +}; diff --git a/test/configs/fixtures/brace-style.js b/test/configs/fixtures/brace-style.js new file mode 100644 index 0000000..0047ac3 --- /dev/null +++ b/test/configs/fixtures/brace-style.js @@ -0,0 +1,17 @@ +const foo = true; +let bar = 0; + +if (foo) { + bar = 1; +} else { + bar = 2; +} + +if (foo) { + bar = 3; +} +else { + bar = 4; +} + +bar.toString(); diff --git a/test/configs/fixtures/camelcase.js b/test/configs/fixtures/camelcase.js new file mode 100644 index 0000000..656e800 --- /dev/null +++ b/test/configs/fixtures/camelcase.js @@ -0,0 +1,4 @@ +const foo_bar = '123'; +const barBaz = '456'; + +foo_bar + barBaz; diff --git a/test/configs/fixtures/handle-callback-err.js b/test/configs/fixtures/handle-callback-err.js new file mode 100644 index 0000000..25dbde8 --- /dev/null +++ b/test/configs/fixtures/handle-callback-err.js @@ -0,0 +1,28 @@ +/* eslint-disable no-unused-vars */ + +export const foo = function (value) { + + const top = function (err) { + + const inner = function (e) { + + return value; + }; + }; + + top(); +}; + + +export const bar = function (value) { + + const top = function (abc) { + + const inner = function (xyz) { + + return value; + }; + }; + + top(); +}; diff --git a/test/configs/fixtures/hapi-capitalize-modules.js b/test/configs/fixtures/hapi-capitalize-modules.js new file mode 100644 index 0000000..b5d5d2c --- /dev/null +++ b/test/configs/fixtures/hapi-capitalize-modules.js @@ -0,0 +1,12 @@ +/* eslint-disable no-unused-vars */ + +import Fs from 'fs'; +import net from 'net'; + +const fn = async function () { + + const Assert = await import('assert'); + const dgram = await import('dgram'); +}; + +fn(); diff --git a/test/configs/fixtures/hapi-for-you.js b/test/configs/fixtures/hapi-for-you.js new file mode 100644 index 0000000..04bd567 --- /dev/null +++ b/test/configs/fixtures/hapi-for-you.js @@ -0,0 +1,7 @@ +const arr = []; + +for (let i = 0; i < arr.length; ++i) { + for (let k = 0; k < arr.length; k++) { + + } +} diff --git a/test/configs/fixtures/indent-switch-case.js b/test/configs/fixtures/indent-switch-case.js new file mode 100644 index 0000000..8eba854 --- /dev/null +++ b/test/configs/fixtures/indent-switch-case.js @@ -0,0 +1,17 @@ +const foo = 'foo'; +let result = 0; + +switch (foo) { + case 'foo': + result = 1; + break; + +case 'bar': + result = 2; + break; + case 'baz': + result = 3; + break; +} + +result.toString(); diff --git a/test/configs/fixtures/indent.js b/test/configs/fixtures/indent.js new file mode 100644 index 0000000..409da0d --- /dev/null +++ b/test/configs/fixtures/indent.js @@ -0,0 +1,10 @@ +export const foo = function (value) { + + return value + 1; +}; + + +export const bar = function (value) { + + return value + 1; +}; diff --git a/test/configs/fixtures/key-spacing.js b/test/configs/fixtures/key-spacing.js new file mode 100755 index 0000000..f1f6d8d --- /dev/null +++ b/test/configs/fixtures/key-spacing.js @@ -0,0 +1,5 @@ +/* eslint-disable no-unused-vars */ +const a = { + b: 'c', + c : 'd' +}; diff --git a/test/configs/fixtures/no-arrowception.js b/test/configs/fixtures/no-arrowception.js new file mode 100644 index 0000000..d1367f7 --- /dev/null +++ b/test/configs/fixtures/no-arrowception.js @@ -0,0 +1,3 @@ +/* eslint-disable no-unused-vars */ +const foo = () => () => 85; +const bar = () => 85; diff --git a/test/configs/fixtures/no-constant-condition.js b/test/configs/fixtures/no-constant-condition.js new file mode 100644 index 0000000..f26e65f --- /dev/null +++ b/test/configs/fixtures/no-constant-condition.js @@ -0,0 +1,3 @@ +if ((foo) => 1) { + // Do nothing +} diff --git a/test/configs/fixtures/no-dupe-keys.js b/test/configs/fixtures/no-dupe-keys.js new file mode 100644 index 0000000..bff6f65 --- /dev/null +++ b/test/configs/fixtures/no-dupe-keys.js @@ -0,0 +1,6 @@ +/* eslint-disable no-unused-vars */ + +const obj = { + a: 1, + a: 2 +}; diff --git a/test/configs/fixtures/no-extra-semi.js b/test/configs/fixtures/no-extra-semi.js new file mode 100644 index 0000000..c10026b --- /dev/null +++ b/test/configs/fixtures/no-extra-semi.js @@ -0,0 +1,12 @@ +/* eslint-disable padding-line-between-statements */ + +export const foo = function () { + + try { + + } + catch { + + }; + +}; diff --git a/test/configs/fixtures/no-shadow.js b/test/configs/fixtures/no-shadow.js new file mode 100644 index 0000000..0564481 --- /dev/null +++ b/test/configs/fixtures/no-shadow.js @@ -0,0 +1,34 @@ +/* eslint-disable no-unused-vars, handle-callback-err */ + + +// Declare internals + +const internals = {}; + + +export const foo = function (value) { + + const top = function (err) { + + const inner = function (err) { + + return value; + }; + }; + + top(); +}; + + +export const bar = function (value) { + + const top = function (res) { + + const inner = function (res) { + + return value; + }; + }; + + top(); +}; diff --git a/test/configs/fixtures/no-undef.js b/test/configs/fixtures/no-undef.js new file mode 100644 index 0000000..3cc4a4f --- /dev/null +++ b/test/configs/fixtures/no-undef.js @@ -0,0 +1,9 @@ +/* eslint-disable no-unused-vars */ + +try { + const foo = typeof bar; + const baz = bar; +} +catch (ignoreErr) { + +} diff --git a/test/configs/fixtures/no-unsafe-finally.js b/test/configs/fixtures/no-unsafe-finally.js new file mode 100644 index 0000000..361633c --- /dev/null +++ b/test/configs/fixtures/no-unsafe-finally.js @@ -0,0 +1,13 @@ +export const foo = function () { + + try { + return 1; + } + catch { + + return 2; + } + finally { + return 3; + } +}; diff --git a/test/configs/fixtures/no-unused-vars.js b/test/configs/fixtures/no-unused-vars.js new file mode 100644 index 0000000..7918756 --- /dev/null +++ b/test/configs/fixtures/no-unused-vars.js @@ -0,0 +1,7 @@ +const internals = {}; +const internals2 = {}; +const bar = function (foo) { + +}; + +bar.toString(); diff --git a/test/configs/fixtures/no-useless-computed-key.js b/test/configs/fixtures/no-useless-computed-key.js new file mode 100644 index 0000000..0006eb8 --- /dev/null +++ b/test/configs/fixtures/no-useless-computed-key.js @@ -0,0 +1,5 @@ +export const a = { ['0']: 0 }; +export const b = { ['0+1,234']: 0 }; +export const c = { [0]: 0 }; +export const d = { ['x']: 0 }; +export const e = { ['x']() {} }; diff --git a/test/configs/fixtures/no-var.js b/test/configs/fixtures/no-var.js new file mode 100644 index 0000000..5487d03 --- /dev/null +++ b/test/configs/fixtures/no-var.js @@ -0,0 +1,7 @@ +/* eslint-disable no-unused-vars */ + +var foo = 1; +let bar = 2; +const baz = 3; + +bar = 4; diff --git a/test/configs/fixtures/node-env.js b/test/configs/fixtures/node-env.js new file mode 100644 index 0000000..4166d9d --- /dev/null +++ b/test/configs/fixtures/node-env.js @@ -0,0 +1,3 @@ +import Fs from "fs"; + +export default Fs; diff --git a/test/configs/fixtures/object-shorthand.js b/test/configs/fixtures/object-shorthand.js new file mode 100644 index 0000000..208ccd8 --- /dev/null +++ b/test/configs/fixtures/object-shorthand.js @@ -0,0 +1,12 @@ +/* eslint-disable no-unused-vars */ + +const a = 1; +const b = 2; +const c = function () {}; +const d = function () {}; +const obj = { + a, + b: b, + c: function () {}, + d() {} +}; diff --git a/test/configs/fixtures/one-var.js b/test/configs/fixtures/one-var.js new file mode 100644 index 0000000..1edf18e --- /dev/null +++ b/test/configs/fixtures/one-var.js @@ -0,0 +1,7 @@ +/* eslint-disable no-unused-vars, prefer-const */ + +const foo = 1; +let bar; +let baz, quux; + +bar = 1; diff --git a/test/configs/fixtures/prefer-arrow-callback.js b/test/configs/fixtures/prefer-arrow-callback.js new file mode 100644 index 0000000..d58f936 --- /dev/null +++ b/test/configs/fixtures/prefer-arrow-callback.js @@ -0,0 +1,23 @@ +/* eslint-disable handle-callback-err */ + +const foo = (arg, callback) => { + + return callback(null, arg + 1); +}; + +const bar = function (err, value) { + +}; + +const baz = (err, value) => { + +}; + +foo(1, bar); +foo(2, baz); +foo(3, (err, value) => { + +}); +foo(4, function (err, value) { + +}); diff --git a/test/configs/fixtures/prefer-const.js b/test/configs/fixtures/prefer-const.js new file mode 100644 index 0000000..71e7925 --- /dev/null +++ b/test/configs/fixtures/prefer-const.js @@ -0,0 +1,10 @@ +/* eslint-disable no-unused-vars */ + +let foo = 1; +let bar = 2; +const baz = 3; + +bar++; + +let { a, b } = { a: 1, b: 2 }; +a++; diff --git a/test/configs/fixtures/private-class-field.js b/test/configs/fixtures/private-class-field.js new file mode 100644 index 0000000..7040f0e --- /dev/null +++ b/test/configs/fixtures/private-class-field.js @@ -0,0 +1,5 @@ +/* eslint-disable no-unused-vars */ + +class Test { + #a = 1; +} diff --git a/test/configs/fixtures/semi.js b/test/configs/fixtures/semi.js new file mode 100644 index 0000000..55b5512 --- /dev/null +++ b/test/configs/fixtures/semi.js @@ -0,0 +1,9 @@ +export const foo = function () { + + return 42 +}; + +export const bar = function () { + + return 85; +}; diff --git a/test/configs/fixtures/space-before-blocks.js b/test/configs/fixtures/space-before-blocks.js new file mode 100755 index 0000000..2344960 --- /dev/null +++ b/test/configs/fixtures/space-before-blocks.js @@ -0,0 +1,11 @@ +/* eslint-disable no-unused-vars */ + +const foo = function (){ + +}; + +const bar = function () { + +}; + +const baz = function (){}; diff --git a/test/configs/fixtures/space-before-function-paren.js b/test/configs/fixtures/space-before-function-paren.js new file mode 100644 index 0000000..77cffb2 --- /dev/null +++ b/test/configs/fixtures/space-before-function-paren.js @@ -0,0 +1,17 @@ +/* eslint-disable no-unused-vars */ + +const foo = function () { + +}; + +const bar = function() { + +}; + +const baz = function baz() { + +}; + +const quux = function quux () { + +}; diff --git a/test/configs/oxfmt.test.config.ts b/test/configs/oxfmt.test.config.ts new file mode 100644 index 0000000..bb1221f --- /dev/null +++ b/test/configs/oxfmt.test.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'oxfmt'; + +import DefaultOxFmtConfig from '../../src/configs/oxfmt.config.js'; + +export default defineConfig({ + ...DefaultOxFmtConfig, +}); diff --git a/test/configs/oxlint.test.config.ts b/test/configs/oxlint.test.config.ts new file mode 100644 index 0000000..5b5da6f --- /dev/null +++ b/test/configs/oxlint.test.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'oxlint'; + +import HapiRecommended from '../../src/configs/recommended.js'; + +export default defineConfig({ + extends: [HapiRecommended], + env: { + ...HapiRecommended.env, + }, +}); diff --git a/test/configs/recommended.js b/test/configs/recommended.js new file mode 100644 index 0000000..67ea026 --- /dev/null +++ b/test/configs/recommended.js @@ -0,0 +1,410 @@ +import { execSync } from 'child_process'; +import Fs from 'fs'; +import Path from 'path'; + +import { describe, it } from 'vitest'; + +describe.concurrent('recommended config', () => { + describe('oxlint', () => { + const configPath = Path.join(import.meta.dirname, './oxlint.test.config.ts'); + + const checkFile = function (filePath) { + const command = `oxlint -c ${configPath} --format json --no-ignore --disable-nested-config ${filePath}`; + + let output; + try { + output = execSync(command, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }); + } catch (err) { + output = err.stdout; + if (!output) { + console.error(err.stderr); + throw err; + } + } + + const result = JSON.parse(output); + + const { diagnostics } = result; + + const messages = diagnostics.map((d) => ({ + ruleId: d.code, + severity: d.severity === 'error' ? 2 : 1, + message: d.message, + line: d.labels[0].span.line, + column: d.labels[0].span.column, + })); + + return messages; + }; + + const lintFile = function (file) { + const messages = checkFile(Path.join(import.meta.dirname, file)); + + return [ + { + messages, + errorCount: messages.filter((m) => m.severity === 2).length, + warningCount: messages.filter((m) => m.severity === 1).length, + }, + ]; + }; + + it('enforces @hapi/for-loop', async ({ expect }) => { + const output = await lintFile('fixtures/hapi-for-you.js'); + const results = output[0]; + + expect(results.errorCount).toBe(0); + expect(results.warningCount).toBe(2); + + let msg = results.messages[0]; + + expect(msg.ruleId).toBe('@hapi(for-loop)'); + expect(msg.severity).toBe(1); + expect(msg.message).toBe("Expected iterator 'j', but got 'k'."); + expect(msg.line).toBe(4); + expect(msg.column).toBe(5); + + msg = results.messages[1]; + + expect(msg.ruleId).toBe('@hapi(for-loop)'); + expect(msg.severity).toBe(1); + expect(msg.message).toBe('Update to iterator should use prefix operator.'); + expect(msg.line).toBe(4); + expect(msg.column).toBe(5); + }); + + it('enforces @hapi/capitalize-modules', async ({ expect }) => { + const output = await lintFile('fixtures/hapi-capitalize-modules.js'); + const results = output[0]; + + expect(results.errorCount).toBe(0); + expect(results.warningCount).toBe(1); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('@hapi(capitalize-modules)'); + expect(msg.severity).toBe(1); + expect(msg.message).toBe('Imported module variable name not capitalized.'); + expect(msg.line).toBe(4); + expect(msg.column).toBe(8); + }); + + it('enforces @hapi/no-arrowception', async ({ expect }) => { + const output = await lintFile('fixtures/no-arrowception.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('@hapi(no-arrowception)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('Arrow function implicitly creates arrow function.'); + expect(msg.line).toBe(2); + expect(msg.column).toBe(13); + }); + + it('enforces no-shadow rule', async ({ expect }) => { + const output = await lintFile('fixtures/no-shadow.js'); + const results = output[0]; + + expect(results.errorCount).toBe(0); + expect(results.warningCount).toBe(1); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(no-shadow)'); + expect(msg.severity).toBe(1); + expect(msg.message).toBe("'res' is already declared in the upper scope."); + expect(msg.line).toBe(27); + expect(msg.column).toBe(33); + }); + + it('enforces one-var rule', async ({ expect }) => { + const output = await lintFile('fixtures/one-var.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('@hapi(one-var)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe("Split 'let' declarations into multiple statements."); + expect(msg.line).toBe(5); + expect(msg.column).toBe(1); + }); + + it('enforces no-undef rule', async ({ expect }) => { + const output = await lintFile('fixtures/no-undef.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(no-undef)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe("'bar' is not defined."); + expect(msg.line).toBe(5); + expect(msg.column).toBe(17); + }); + + it('enforces no-unused-vars', async ({ expect }) => { + const output = await lintFile('fixtures/no-unused-vars.js'); + const results = output[0]; + + expect(results.errorCount).toBe(0); + expect(results.warningCount).toBe(1); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(no-unused-vars)'); + expect(msg.severity).toBe(1); + expect(msg.message).toMatch(/Variable 'internals2' is declared but never used\./); + expect(msg.line).toBe(2); + expect(msg.column).toBe(7); + }); + + it('enforces prefer-const', async ({ expect }) => { + const output = await lintFile('fixtures/prefer-const.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(prefer-const)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('`foo` is never reassigned.'); + expect(msg.line).toBe(3); + expect(msg.column).toBe(5); + }); + + it('enforces @hapi/no-var', async ({ expect }) => { + const output = await lintFile('fixtures/no-var.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('@hapi(no-var)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('Unexpected var, use let or const instead.'); + expect(msg.line).toBe(3); + expect(msg.column).toBe(1); + }); + + it('enforces object-shorthand', async ({ expect }) => { + const output = await lintFile('fixtures/object-shorthand.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(object-shorthand)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('Expected property shorthand.'); + expect(msg.line).toBe(9); + expect(msg.column).toBe(5); + }); + + it('enforces prefer-arrow-callback', async ({ expect }) => { + const output = await lintFile('fixtures/prefer-arrow-callback.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(prefer-arrow-callback)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('Unexpected function expression.'); + expect(msg.line).toBe(21); + expect(msg.column).toBe(8); + }); + + it('enforces no-constant-condition rule', async ({ expect }) => { + const output = await lintFile('fixtures/no-constant-condition.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(no-constant-condition)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('Unexpected constant condition'); + expect(msg.line).toBe(1); + expect(msg.column).toBe(5); + }); + + it('enforces no-unsafe-finally rule', async ({ expect }) => { + const output = await lintFile('fixtures/no-unsafe-finally.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(no-unsafe-finally)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('Unsafe `finally` block.'); + expect(msg.line).toBe(11); + expect(msg.column).toBe(9); + }); + + it('enforces no-useless-computed-key rule', async ({ expect }) => { + const output = await lintFile('fixtures/no-useless-computed-key.js'); + const results = output[0]; + + expect(results.errorCount).toBe(5); + expect(results.warningCount).toBe(0); + + let msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(no-useless-computed-key)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe("Unnecessarily computed property `'0'` found."); + expect(msg.line).toBe(1); + expect(msg.column).toBe(21); + + msg = results.messages[1]; + + expect(msg.ruleId).toBe('eslint(no-useless-computed-key)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe("Unnecessarily computed property `'0+1,234'` found."); + expect(msg.line).toBe(2); + expect(msg.column).toBe(21); + + msg = results.messages[2]; + + expect(msg.ruleId).toBe('eslint(no-useless-computed-key)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('Unnecessarily computed property `0` found.'); + expect(msg.line).toBe(3); + expect(msg.column).toBe(21); + + msg = results.messages[3]; + + expect(msg.ruleId).toBe('eslint(no-useless-computed-key)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe("Unnecessarily computed property `'x'` found."); + expect(msg.line).toBe(4); + expect(msg.column).toBe(21); + + msg = results.messages[4]; + + expect(msg.ruleId).toBe('eslint(no-useless-computed-key)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe("Unnecessarily computed property `'x'` found."); + expect(msg.line).toBe(5); + expect(msg.column).toBe(21); + }); + + it('enforces handle-callback-err rule', async ({ expect }) => { + const output = await lintFile('fixtures/handle-callback-err.js'); + const results = output[0]; + + expect(results.errorCount).toBe(2); + expect(results.warningCount).toBe(0); + + let msg = results.messages[0]; + + expect(msg.ruleId).toBe('node(handle-callback-err)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('Expected error to be handled.'); + expect(msg.line).toBe(5); + expect(msg.column).toBe(27); + + msg = results.messages[1]; + + expect(msg.ruleId).toBe('node(handle-callback-err)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe('Expected error to be handled.'); + expect(msg.line).toBe(7); + expect(msg.column).toBe(33); + }); + + it('enforces no-dupe-keys rule', async ({ expect }) => { + const output = await lintFile('fixtures/no-dupe-keys.js'); + const results = output[0]; + + expect(results.errorCount).toBe(1); + expect(results.warningCount).toBe(0); + + const msg = results.messages[0]; + + expect(msg.ruleId).toBe('eslint(no-dupe-keys)'); + expect(msg.severity).toBe(2); + expect(msg.message).toBe("Duplicate key 'a'"); + expect(msg.line).toBe(4); + expect(msg.column).toBe(5); + }); + + it('uses the node environment', async ({ expect }) => { + const output = await lintFile('fixtures/node-env.js'); + const results = output[0]; + + expect(results.errorCount).toBe(0); + expect(results.warningCount).toBe(0); + expect(results.messages).toEqual([]); + }); + + it('does not enforce the camelcase lint rule', async ({ expect }) => { + const output = await lintFile('fixtures/camelcase.js'); + const results = output[0]; + + expect(results.errorCount).toBe(0); + expect(results.warningCount).toBe(0); + expect(results.messages).toEqual([]); + }); + }); + + describe('oxfmt', () => { + const configPath = Path.join(import.meta.dirname, './oxfmt.test.config.ts'); + const fixturesDir = Path.join(import.meta.dirname, './fixtures'); + + const formatFile = (file) => { + const filePath = Path.join(fixturesDir, file); + const command = `oxfmt -c ${configPath} --stdin-filepath ${filePath}`; + + try { + const output = execSync(command, { + encoding: 'utf8', + input: Fs.readFileSync(filePath, 'utf8'), + stdio: ['pipe', 'pipe', 'ignore'], + }); + return output; + } catch (err) { + console.error(err.stderr); + throw err; + } + }; + + it.for([ + 'indent.js', + 'indent-switch-case.js', + 'semi.js', + 'no-extra-semi.js', + 'space-before-function-paren.js', + 'arrow-parens.js', + 'arrow-spacing.js', + 'space-before-blocks.js', + ])('formats %s correctly', (fileName, { expect }) => { + const output = formatFile(fileName); + expect(output).toMatchSnapshot(); + }); + }); +}); diff --git a/test/index.js b/test/index.js new file mode 100755 index 0000000..0745b45 --- /dev/null +++ b/test/index.js @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; + +import Plugin from '..'; + +describe('ESLint Plugin', () => { + it('exposes all expected rules', () => { + expect(Plugin.rules).toBeDefined(); + expect(Plugin.rules).toBeTypeOf('object'); + + const rules = Object.keys(Plugin.rules); + + expect(rules.length).toBe(6); + expect(rules.includes('capitalize-modules')).toBe(true); + expect(rules.includes('for-loop')).toBe(true); + expect(rules.includes('no-var')).toBe(true); + expect(rules.includes('no-arrowception')).toBe(true); + expect(rules.includes('consistent-this')).toBe(true); + expect(rules.includes('one-var')).toBe(true); + }); +}); diff --git a/test/rules/capitalize-modules.js b/test/rules/capitalize-modules.js new file mode 100644 index 0000000..2127a5e --- /dev/null +++ b/test/rules/capitalize-modules.js @@ -0,0 +1,83 @@ +import { RuleTester } from 'oxlint/plugins-dev'; +import { describe, it } from 'vitest'; + +import HapiRecommended from '../../src/configs/recommended.js'; +import Rule from '../../src/rules/capitalize-modules'; + +const ruleTester = new RuleTester(HapiRecommended); + +describe('capitalize-modules rule', () => { + it('reports warning when module is not capitalized', () => { + const sample = [ + 'import hapi from "hapi";', + 'import * as hapi from "hapi";', + 'async function x() { const hapi = await import("hapi"); }', + ]; + + ruleTester.run('test', Rule, { + valid: [], + invalid: sample.map((code) => { + return { + code, + errors: [{ message: 'Imported module variable name not capitalized.' }], + }; + }), + }); + }); + + it('does not report anything if module variable is capitalized', () => { + const sample = ['import Hapi from "hapi";', 'import * as Hapi from "hapi";', 'import { hapi } from "hapi";']; + + ruleTester.run('test', Rule, { + valid: sample.map((code) => { + return { code }; + }), + invalid: [], + }); + }); + + it('only warns on globals when global-scope-only is set', () => { + const valid = [ + 'import Hapi from "hapi";', + 'import * as Hapi from "hapi";', + 'async function x() { const hapi = await import("hapi"); }', + ]; + + const invalid = ['import hapi from "hapi";', 'import * as hapi from "hapi";']; + + ruleTester.run('test', Rule, { + valid: valid.map((code) => { + return { + code, + options: ['global-scope-only'], + }; + }), + invalid: invalid.map((code) => { + return { + code, + options: ['global-scope-only'], + errors: [{ message: 'Imported module variable name not capitalized.' }], + }; + }), + }); + }); + + it('does not report anything for non-module variables', () => { + const sample = [ + 'let foo, bar, baz;', + 'const foo = fn()', + 'const foo = "string";', + 'const foo = this.bar()', + 'foo[bar] = 5;', + 'this.foo = null;', + ' [foo, bar] = [1, 2];', + ]; + + ruleTester.run('test', Rule, { + valid: sample.map((code) => { + return { code }; + }), + invalid: [], + }); + }); +}); diff --git a/test/rules/consistent-this.js b/test/rules/consistent-this.js new file mode 100644 index 0000000..1b18a15 --- /dev/null +++ b/test/rules/consistent-this.js @@ -0,0 +1,59 @@ +import { RuleTester } from 'oxlint/plugins-dev'; +import { describe, it } from 'vitest'; + +import HapiRecommended from '../../src/configs/recommended.js'; +import Rule from '../../src/rules/consistent-this'; + +const ruleTester = new RuleTester(HapiRecommended); + +describe('consistent-this rule', () => { + it('reports a non-this value assigned to the designated alias', () => { + ruleTester.run('test', Rule, { + valid: [], + invalid: [ + { + code: 'const self = 5;', + options: ['self'], + errors: [{ message: "Designated alias 'self' is not assigned to 'this'." }], + }, + { + code: 'let self; self = 5;', + options: ['self'], + errors: [{ message: "Designated alias 'self' is not assigned to 'this'." }], + }, + { + code: 'let self; self += this;', + options: ['self'], + errors: [{ message: "Designated alias 'self' is not assigned to 'this'." }], + }, + ], + }); + }); + + it('reports this captured under an unexpected alias', () => { + ruleTester.run('test', Rule, { + valid: [], + invalid: [ + { + code: 'const that = this;', + options: ['self'], + errors: [{ message: "Unexpected alias 'that' for 'this'." }], + }, + ], + }); + }); + + it('allows this captured as the designated alias and unrelated assignments', () => { + ruleTester.run('test', Rule, { + valid: [ + { code: 'const self = this;', options: ['self'] }, + { code: 'let self; self = this;', options: ['self'] }, + { code: 'const value = 5;', options: ['self'] }, + { code: 'const node = this.node;', options: ['self'] }, + { code: 'const { x } = this;', options: ['self'] }, + { code: 'this.context = this;', options: ['self'] }, + ], + invalid: [], + }); + }); +}); diff --git a/test/rules/for-loop.js b/test/rules/for-loop.js new file mode 100644 index 0000000..fd40bc0 --- /dev/null +++ b/test/rules/for-loop.js @@ -0,0 +1,139 @@ +import { RuleTester } from 'oxlint/plugins-dev'; +import { describe, it } from 'vitest'; + +import HapiRecommended from '../../src/configs/recommended.js'; +import Rule from '../../src/rules/for-loop'; + +const ruleTester = new RuleTester(HapiRecommended); + +describe('for-loop rule', () => { + it('enforces iterator variable naming', () => { + const valids = [ + { + code: 'for (let i = 0; i < a.length; ++i) { for (let j = 0; j < b.length; ++j) {} }', + }, + { + code: 'for (let j = 0; j < a.length; ++j) { for (let k = 0; k < b.length; ++k) {} }', + options: [{ startIterator: 'j' }], + }, + { + code: 'for (let i = 0; i < a.length; ++i) {}; for (let i = 0; i < a.length; ++i) {}', + }, + { + code: 'for (;;) {}', + }, + ]; + + const invalids = [ + { + code: 'for (let j = 0; j < a.length; ++j) {}', + errors: [{ message: "Expected iterator 'i', but got 'j'." }], + }, + { + code: 'for (let i = 0; i < a.length; ++i) {}', + options: [{ startIterator: 'j' }], + errors: [{ message: "Expected iterator 'j', but got 'i'." }], + }, + ]; + + ruleTester.run('test', Rule, { + valid: valids, + invalid: invalids, + }); + }); + + it('enforces a maximum of one variable initialized per loop', () => { + const valids = [ + { + code: 'for (let i = 0; i < a.length; ++i) {}', + }, + { + code: 'for (i = 0, j = 1; i < a.length; ++i) {}', + }, + { + code: 'for (; i < a.length; ++i) {}', + }, + ]; + + const invalids = [ + { + code: 'for (let i = 0, j; i < a.length; ++i) {}', + errors: [{ message: 'Only one variable can be initialized per loop.' }], + }, + { + code: 'for (let [i] = [0]; i < a.length; ++i) {}', + errors: [{ message: 'Left hand side of initializer must be a single variable.' }], + }, + ]; + + ruleTester.run('test', Rule, { + valid: valids, + invalid: invalids, + }); + }); + + it('enforces the maximum number of nested for loops', () => { + const valids = [ + { + code: 'for (let i = 0; i < a.length; ++i) {}', + }, + { + code: 'for (let i = 0; i < a.length; ++i) { for (let j = 0; j < b.length; ++j) { for (let k = 0; k < c.length; ++k) { for (let l = 0; l < d.length; ++l) {} } } }', + options: [{ maxDepth: 4 }], + }, + ]; + + const invalids = [ + { + code: 'for (let i = 0; i < a.length; ++i) { for (let j = 0; j < b.length; ++j) { for (let k = 0; k < c.length; ++k) { for (let l = 0; l < d.length; ++l) {} } } }', + errors: [{ message: 'Too many nested for loops.' }], + }, + { + code: 'for (let i = 0; i < a.length; ++i) { for (let j = 0; j < b.length; ++j) {} }', + options: [{ maxDepth: 1 }], + errors: [{ message: 'Too many nested for loops.' }], + }, + ]; + + ruleTester.run('test', Rule, { + valid: valids, + invalid: invalids, + }); + }); + + it('prevents post-increment and post-decrement', () => { + const valids = [ + { + code: 'for (let i = 0; i < a.length; ++i) {}', + }, + { + code: 'for (let i = 0; i < a.length; --i) {}', + }, + { + code: 'for (let i = 0; i < a.length; i += 1) {}', + }, + { + code: 'for (let i = 0; i < a.length; i = i + 1) {}', + }, + { + code: 'for (let i = 0; i < a.length;) {}', + }, + ]; + + const invalids = [ + { + code: 'for (let i = 0; i < a.length; i++) {}', + errors: [{ message: 'Update to iterator should use prefix operator.' }], + }, + { + code: 'for (let i = 0; i < a.length; i--) {}', + errors: [{ message: 'Update to iterator should use prefix operator.' }], + }, + ]; + + ruleTester.run('test', Rule, { + valid: valids, + invalid: invalids, + }); + }); +}); diff --git a/test/rules/no-arrowception.js b/test/rules/no-arrowception.js new file mode 100644 index 0000000..2ac4ee0 --- /dev/null +++ b/test/rules/no-arrowception.js @@ -0,0 +1,35 @@ +import { RuleTester } from 'oxlint/plugins-dev'; +import { describe, it } from 'vitest'; + +import HapiRecommended from '../../src/configs/recommended.js'; +import Rule from '../../src/rules/no-arrowception'; + +const ruleTester = new RuleTester(HapiRecommended); + +describe('no-arrowception rule', () => { + it('reports error when an arrow function implicitly creates another arrow function', () => { + const valids = [ + 'const foo = () => 85;', + 'const foo = () => { return 42; }', + 'const foo = () => ({});', + 'const foo = () => ({\nbar: 1});', + 'const foo = () => [];', + 'const foo = () => [\n1,\n2];', + 'const foo = () => { return () => 85; };', + ].map((code) => { + return { code }; + }); + + const invalids = ['const foo = () => () => 85;'].map((code) => { + return { + code, + errors: [{ message: 'Arrow function implicitly creates arrow function.' }], + }; + }); + + ruleTester.run('test', Rule, { + valid: valids, + invalid: invalids, + }); + }); +}); diff --git a/test/rules/no-var.js b/test/rules/no-var.js new file mode 100644 index 0000000..0c98891 --- /dev/null +++ b/test/rules/no-var.js @@ -0,0 +1,47 @@ +import { RuleTester } from 'oxlint/plugins-dev'; +import { describe, it } from 'vitest'; + +import HapiRecommended from '../../src/configs/recommended.js'; +import Rule from '../../src/rules/no-var'; + +const ruleTester = new RuleTester(HapiRecommended); + +describe('no-var rule', () => { + it('reports warning when vars used outside of try...catch scope', () => { + const sample = [ + 'function test() { var a = 1; }', + 'function test() { try { var bf = 2; console.log(bf); } catch (err) {} }', + 'function test() { try {} catch (err) { var cf = 3; console.log(cf); } }', + 'function test() { try { var bf = 2; if (bf) { console.log(bf); } } catch (err) {} }', + 'function test() { try { if (true) { var bf = 2; } console.log(bf); } catch (err) {} }', + 'var a = 1; try {} catch (err) {}', + ]; + + ruleTester.run('test', Rule, { + valid: [], + invalid: sample.map((code) => { + return { + code, + errors: [{ message: 'Unexpected var, use let or const instead.' }], + }; + }), + }); + }); + + it('ignores vars used inside try...catch scope and referenced from outside', () => { + const sample = [ + 'const a = 1;', + 'function test() { try { var bf = 2; } catch (err) {} console.log(bf); }', + 'function test() { try {} catch (err) { var cf = 3; } console.log(cf); }', + 'function test() { a = 1; try { var a = 2; } catch (err) {} }', + 'try { var a = 1; } catch (err) {} console.log(a);', + ]; + + ruleTester.run('test', Rule, { + valid: sample.map((code) => { + return { code }; + }), + invalid: [], + }); + }); +}); diff --git a/test/rules/one-var.js b/test/rules/one-var.js new file mode 100644 index 0000000..18040d9 --- /dev/null +++ b/test/rules/one-var.js @@ -0,0 +1,35 @@ +import { RuleTester } from 'oxlint/plugins-dev'; +import { describe, it } from 'vitest'; + +import HapiRecommended from '../../src/configs/recommended.js'; +import Rule from '../../src/rules/one-var'; + +const ruleTester = new RuleTester(HapiRecommended); + +describe('one-var rule', () => { + it('reports declarations that combine multiple variables', () => { + ruleTester.run('test', Rule, { + valid: [], + invalid: [ + { code: 'let a, b;', errors: [{ message: "Split 'let' declarations into multiple statements." }] }, + { + code: 'const a = 1, b = 2;', + errors: [{ message: "Split 'const' declarations into multiple statements." }], + }, + { code: 'var a, b, c;', errors: [{ message: "Split 'var' declarations into multiple statements." }] }, + ], + }); + }); + + it('allows one variable per declaration', () => { + ruleTester.run('test', Rule, { + valid: [ + { code: 'const a = 1;' }, + { code: 'let b;' }, + { code: 'let c; let d;' }, + { code: 'const e = 1; const f = 2;' }, + ], + invalid: [], + }); + }); +}); diff --git a/test/vitest.js b/test/vitest.js new file mode 100644 index 0000000..4d621cf --- /dev/null +++ b/test/vitest.js @@ -0,0 +1,59 @@ +import Path from 'node:path'; + +import { describe, it } from 'vitest'; + +import OxcVitest from '../src/vitest.js'; + +const internals = {}; + +internals.inject = async function (plugin) { + const injected = []; + const context = { + injectTestProjects(config) { + injected.push(config); + return Promise.resolve([]); + }, + }; + + await plugin.configureVitest(context); + return injected; +}; + +describe('vitest plugin', () => { + it('returns a valid plugin', ({ expect }) => { + const plugin = OxcVitest(); + + expect(plugin.name).toBe('@hapi/oxc-plugin:vitest'); + expect(plugin.configureVitest).toBeTypeOf('function'); + }); + + it('injects a test project with default options', async ({ expect }) => { + const plugin = OxcVitest(); + const injected = await internals.inject(plugin); + + expect(injected).toHaveLength(1); + expect(injected[0].test.name).toBe('@hapi/oxc-plugin'); + expect(injected[0].test.include).toHaveLength(1); + expect(injected[0].test.include[0]).toMatch(/vitest\.test\.js$/); + expect(injected[0].define.__HAPI_OXC_OXLINT__).toBe(true); + expect(injected[0].define.__HAPI_OXC_OXFMT__).toBe(true); + expect(injected[0].define.__HAPI_OXC_CWD__).toBe('null'); + }); + + it('passes custom options to the injected project', async ({ expect }) => { + const plugin = OxcVitest({ oxlint: false, oxfmt: false, cwd: '/custom/path' }); + const injected = await internals.inject(plugin); + + expect(injected[0].define.__HAPI_OXC_OXLINT__).toBe(false); + expect(injected[0].define.__HAPI_OXC_OXFMT__).toBe(false); + expect(injected[0].define.__HAPI_OXC_CWD__).toBe('"/custom/path"'); + }); + + it('includes the test file from the src directory', async ({ expect }) => { + const plugin = OxcVitest(); + const injected = await internals.inject(plugin); + + const expectedPath = Path.join(import.meta.dirname, '..', 'src', 'vitest.test.js'); + expect(injected[0].test.include[0]).toBe(expectedPath); + }); +}); diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..2f3cde7 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,26 @@ +import { defineConfig } from 'vitest/config'; + +import Oxc from './src/vitest.js'; + +export default defineConfig({ + plugins: [Oxc()], + test: { + environment: 'node', + include: ['test/**/*.js'], + exclude: ['test/configs/fixtures/**', 'test/configs/common.js'], + coverage: { + enabled: true, + provider: 'v8', + all: true, + thresholds: { + functions: 100, + lines: 100, + branches: 100, + statements: 100, + }, + reportsDirectory: './coverage', + reporter: ['text', 'lcov'], + exclude: ['test/**', 'dist/**', '**/*.d.ts'], + }, + }, +});