diff --git a/.claude/agents/vitest-test-writer.md b/.claude/agents/vitest-test-writer.md index 3a5abd6bd..7889de5ed 100644 --- a/.claude/agents/vitest-test-writer.md +++ b/.claude/agents/vitest-test-writer.md @@ -20,22 +20,29 @@ You write comprehensive, high-quality tests that follow the established patterns ## Testing Patterns You Must Follow ### Use runInlineTests Utility + For integration tests, always use the `runInlineTests` utility to create and run test scenarios. This utility allows you to define inline test files and validate their output. ### Snapshot Validation with toMatchInlineSnapshot + Always validate output using `toMatchInlineSnapshot()`. The snapshot is automatically generated on the first run. This is the preferred method because it: + - Captures the exact expected output - Makes changes visible in code review - Catches regressions precisely ### Avoid toContain + Do NOT use `toContain()` for output validation. This method fails to catch: + - Extra unexpected output - Repeated output that shouldn't occur - Subtle formatting differences ### Handle Dynamic Content + When output contains dynamic content (timestamps, absolute paths, durations, etc.): + 1. First check `test-utils` for existing utilities that normalize this content 2. If no utility exists, manually process with `stdout.replace(regexp, 'normalized-value')` 3. Common patterns to normalize: @@ -44,7 +51,9 @@ When output contains dynamic content (timestamps, absolute paths, durations, etc - Process IDs or temporary file paths ### Validate Test Results with testTree or errorTree + To ensure all tests actually passed (not just that they ran), use `testTree` or `errorTree` helpers. Pass the result to `toMatchInlineSnapshot()` to verify: + - The correct number of tests ran - Tests are organized in the expected suites - No unexpected failures or skipped tests @@ -52,6 +61,7 @@ To ensure all tests actually passed (not just that they ran), use `testTree` or ## Writing Unit Tests For unit tests in `test/unit/`: + 1. Import the function directly from its source package 2. Test pure functionality without process spawning 3. Cover edge cases, error conditions, and typical usage @@ -60,6 +70,7 @@ For unit tests in `test/unit/`: ## Writing Integration Tests For integration tests in `test/e2e/`: + 1. Use `runInlineTests` to define test scenarios 2. Create realistic test file content 3. Validate both stderr and the test results structure @@ -86,6 +97,7 @@ For integration tests in `test/e2e/`: ## Output Format When writing tests, provide: + 1. The complete test file with all imports 2. Explanations of what each test verifies 3. Notes on any dynamic content normalization applied diff --git a/.claude/skills/typo-checker/SKILL.md b/.claude/skills/typo-checker/SKILL.md index 026939e46..88ef1f89d 100644 --- a/.claude/skills/typo-checker/SKILL.md +++ b/.claude/skills/typo-checker/SKILL.md @@ -16,11 +16,11 @@ Scan the codebase with `typos-cli`, classify findings, fix real typos, and maint `typos-cli` must be installed. If not available, install via one of: -| Method | Command | -|---|---| -| cargo | `cargo install typos-cli` | -| brew | `brew install typos-cli` | -| pipx | `pipx install typos` | +| Method | Command | +| ------ | -------------------------------------------------------- | +| cargo | `cargo install typos-cli` | +| brew | `brew install typos-cli` | +| pipx | `pipx install typos` | | Binary | Download from https://github.com/crate-ci/typos/releases | ## Workflow @@ -52,6 +52,7 @@ For every finding, decide: - **False positive** — add to `_typos.toml` Common false positive patterns: + - Short variable names that happen to be words (`ba`, `fo`, `nd`) - Domain abbreviations (`als` for AsyncLocalStorage, `PnP` for Plug'n'Play) - File extensions in regexes (`.styl`, `.pcss`) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index aa069c529..cca20b499 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,16 +9,20 @@ Resolves #issue-number ### Please don't delete this checklist! Before submitting the PR, please make sure you do the following: + - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. If the feature is substantial or introduces breaking changes without a discussion, PR might be closed. - [ ] Ideally, include a test that fails without this PR but passes with it. - [ ] Please, don't make changes to `pnpm-lock.yaml` unless you introduce a new test example. - [ ] Please check [Allow edits by maintainers](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) to make review process faster. Note that this option is not available for repositories that are owned by Github organizations. ### Tests + - [ ] Run the tests with `pnpm test:ci`. ### Documentation + - [ ] If you introduce new functionality, document it. You can run documentation with `pnpm run docs` command. ### Changesets + - [ ] Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with `feat:`, `fix:`, `perf:`, `docs:`, or `chore:`. diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 0fed23671..87294e000 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,44 +1,42 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended", "schedule:weekly", "group:allNonMajor"], - "labels": ["dependencies"], - "rangeStrategy": "bump", + $schema: 'https://docs.renovatebot.com/renovate-schema.json', + extends: ['config:recommended', 'schedule:weekly', 'group:allNonMajor'], + labels: ['dependencies'], + rangeStrategy: 'bump', // Align with pnpm's `minimumReleaseAge` default (1440 minutes) so Renovate does // not propose versions that pnpm's supply-chain check would reject as too new. - "minimumReleaseAge": "1 day", - "packageRules": [ + minimumReleaseAge: '1 day', + packageRules: [ { - "groupName": "Eslint packages", - "matchPackageNames": ["/eslint/"] + groupName: 'Oxc packages', + matchPackageNames: ['oxlint', 'oxfmt'], }, { - "matchDepTypes": ["peerDependencies"], - "enabled": false + matchDepTypes: ['peerDependencies'], + enabled: false, }, { - "matchDepTypes": ["action"], - "matchPackageNames": ["!actions/{/,}**", "!github/{/,}**"], - "pinDigests": true - } + matchDepTypes: ['action'], + matchPackageNames: ['!actions/{/,}**', '!github/{/,}**'], + pinDigests: true, + }, ], - "ignoreDeps": [ + ignoreDeps: [ // manually bumping - "node", - "vite", + 'node', + 'vite', // we patch these packages - "@types/chai", - "@sinonjs/fake-timers", - "cac", + '@types/chai', + '@sinonjs/fake-timers', + 'cac', // Transitive dependency that we patch - "acorn", + 'acorn', // Keep using codemirror 5 - "codemirror", - "react-18", - "react-is-18", + 'codemirror', + 'react-18', + 'react-is-18', // webdriverio removed provenance: https://github.com/webdriverio/webdriverio/issues/14887 - "webdriverio" + 'webdriverio', ], - "ignorePaths": [ - "**/node_modules/**" - ] + ignorePaths: ['**/node_modules/**'], } diff --git a/.gitignore b/.gitignore index 19d26869a..4b87bf4ea 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ bench/test/*/*/ **/bench.json **/browser/browser.json docs/public/user-avatars -.eslintcache docs/.vitepress/cache/ !test/e2e/fixtures/dotted-files/**/.cache test/**/__screenshots__/**/* diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 000000000..51706d150 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,47 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "sortPackageJson": false, + "sortImports": { + "groups": [ + "type-import", + ["type-parent", "type-sibling", "type-index", "type-internal"], + "value-builtin", + "value-external", + "value-internal", + ["value-parent", "value-sibling", "value-index"], + "side_effect", + "unknown" + ], + "newlinesBetween": false + }, + "overrides": [ + { + // code blocks in the docs are written by hand to look good + "files": ["**/*.md"], + "options": { "embeddedLanguageFormatting": "off" } + } + ], + "ignorePatterns": [ + "**/fixtures", + "**/*.timestamp-*", + "pnpm-lock.yaml", + // generated by the build + "packages/*/LICENSE.md", + "docs/guide/cli-generated.md", + "docs/.vitepress/contributor-names.json", + "packages/browser/src/client/public/esm-client-injector.js", + "test/unit/src/wasm/wasm-bindgen*", + // test inputs that depend on the exact source text + "test/unit/src/self", + "test/unit/test/mocking/already-hoisted.test.ts", + "test/unit/test/tab-effect.spec.mjs", + "test/unit/test/mocking/external.test.ts", + "test/workspaces/results.json", + "test/workspaces-browser/results.json", + "test/e2e/deps/error", + "test/e2e/deps/malformed-source-map" + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..0d654a689 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,332 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn", "oxc", "import", "node", "vue", "jsdoc"], + "env": { + "browser": true, + "node": true + }, + "options": { + "reportUnusedDisableDirectives": "error" + }, + "ignorePatterns": [ + "**/fixtures", + "**/*.d.ts", + "**/*.timestamp-*", + "packages/browser/src/client/public/esm-client-injector.js", + "test/unit/src/wasm/wasm-bindgen*", + "test/unit/src/self", + "test/unit/test/mocking/already-hoisted.test.ts", + "test/*/deps" + ], + "categories": { + "correctness": "error" + }, + "rules": { + "accessor-pairs": "error", + "array-callback-return": "error", + "block-scoped-var": "error", + "curly": ["error", "all"], + "default-case-last": "error", + "dot-notation": "error", + "eqeqeq": ["error", "smart"], + "new-cap": ["error", { "capIsNew": false }], + "no-alert": "error", + "no-array-constructor": "error", + "no-async-promise-executor": "error", + "no-caller": "error", + "no-case-declarations": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": ["error", "always"], + "no-console": ["error", { "allow": ["warn", "error"] }], + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-class-members": "error", + "no-duplicate-case": "error", + "no-empty": ["error", { "allowEmptyCatch": true }], + "no-eval": "error", + "no-ex-assign": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-boolean-cast": "error", + "no-fallthrough": "error", + "no-global-assign": "error", + "no-implied-eval": "error", + "no-irregular-whitespace": "error", + "no-iterator": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loss-of-precision": "error", + "no-misleading-character-class": "error", + "no-multi-str": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-proto": "error", + "no-prototype-builtins": "error", + "no-redeclare": "error", + "no-regex-spaces": "error", + "no-restricted-globals": [ + "error", + { "name": "global", "message": "Use `globalThis` instead." }, + { "name": "self", "message": "Use `globalThis` instead." } + ], + "no-restricted-imports": ["error", { "paths": ["path"] }], + "no-restricted-properties": [ + "error", + { + "property": "__proto__", + "message": "Use `Object.getPrototypeOf` or `Object.setPrototypeOf` instead." + }, + { "property": "__defineGetter__", "message": "Use `Object.defineProperty` instead." }, + { "property": "__defineSetter__", "message": "Use `Object.defineProperty` instead." }, + { + "property": "__lookupGetter__", + "message": "Use `Object.getOwnPropertyDescriptor` instead." + }, + { + "property": "__lookupSetter__", + "message": "Use `Object.getOwnPropertyDescriptor` instead." + } + ], + "no-self-assign": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-template-curly-in-string": "error", + "no-throw-literal": "error", + "no-unexpected-multiline": "error", + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": ["error", { "defaultAssignment": false }], + "no-unreachable-loop": "error", + "no-unsafe-finally": "error", + "no-unused-expressions": [ + "error", + { "allowShortCircuit": true, "allowTaggedTemplates": true, "allowTernary": true } + ], + "no-unused-vars": [ + "error", + { + "args": "after-used", + "argsIgnorePattern": "^_", + "ignoreRestSiblings": true, + "vars": "all", + "varsIgnorePattern": "^_" + } + ], + "no-use-before-define": ["error", { "classes": false, "functions": false, "variables": true }], + "no-useless-call": "error", + "no-useless-catch": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-var": "error", + "object-shorthand": ["error", "always", { "avoidQuotes": true }], + "one-var": ["error", { "initialized": "never" }], + "prefer-arrow-callback": ["error", { "allowUnboundThis": true }], + "prefer-const": ["error", { "destructuring": "all", "ignoreReadBeforeAssign": true }], + "prefer-exponentiation-operator": "error", + "prefer-object-has-own": "error", + "prefer-promise-reject-errors": "error", + "prefer-regex-literals": ["error", { "disallowRedundantWrapping": true }], + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "symbol-description": "error", + "unicode-bom": "error", + "use-isnan": ["error", { "enforceForIndexOf": true, "enforceForSwitchCase": true }], + "valid-typeof": ["error", { "requireStringLiterals": true }], + "vars-on-top": "error", + "yoda": "error", + + "typescript/ban-ts-comment": ["error", { "ts-expect-error": "allow-with-description" }], + "typescript/consistent-type-definitions": ["error", "interface"], + "typescript/consistent-type-imports": [ + "error", + { + "prefer": "type-imports", + "fixStyle": "separate-type-imports", + "disallowTypeAnnotations": false + } + ], + "typescript/method-signature-style": ["error", "property"], + "typescript/no-duplicate-enum-values": "error", + "typescript/no-empty-object-type": ["error", { "allowInterfaces": "always" }], + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-import-type-side-effects": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-nullish-coalescing": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-require-imports": "error", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-as-const": "error", + "typescript/prefer-literal-enum-member": "error", + "typescript/prefer-namespace-keyword": "error", + + "unicorn/consistent-empty-array-spread": "error", + "unicorn/error-message": "error", + "unicorn/escape-case": "error", + "unicorn/new-for-builtins": "error", + "unicorn/no-instanceof-builtins": ["error", { "strategy": "loose" }], + "unicorn/no-new-array": "error", + "unicorn/no-new-buffer": "error", + "unicorn/prefer-array-some": "error", + "unicorn/prefer-date-now": "error", + "unicorn/prefer-dom-node-text-content": "error", + "unicorn/prefer-includes": "error", + "unicorn/prefer-node-protocol": "error", + "unicorn/prefer-number-properties": ["error", { "checkInfinity": false, "checkNaN": false }], + "unicorn/prefer-regexp-test": "error", + "unicorn/prefer-string-starts-ends-with": "error", + "unicorn/prefer-type-error": "error", + "unicorn/throw-new-error": "error", + + "import/consistent-type-specifier-style": ["error", "prefer-top-level"], + "import/first": "error", + "import/no-duplicates": "error", + "import/no-mutable-exports": "error", + "import/no-named-default": "error", + + "node/handle-callback-err": ["error", "^(err|error)$"], + "node/no-exports-assign": "error", + "node/no-new-require": "error", + "node/no-path-concat": "error", + "node/no-top-level-await": "error", + + "vue/component-definition-name-casing": "warn", + "vue/no-arrow-functions-in-watch": "error", + "vue/no-async-in-computed-properties": "error", + "vue/no-computed-properties-in-data": "error", + "vue/no-deprecated-data-object-declaration": "error", + "vue/no-deprecated-delete-set": "error", + "vue/no-deprecated-destroyed-lifecycle": "error", + "vue/no-deprecated-events-api": "error", + "vue/no-deprecated-model-definition": "error", + "vue/no-deprecated-props-default-this": "error", + "vue/no-deprecated-vue-config-keycodes": "error", + "vue/no-dupe-keys": "error", + "vue/no-export-in-script-setup": "error", + "vue/no-expose-after-await": "error", + "vue/no-lifecycle-after-await": "error", + "vue/no-multiple-slot-args": "warn", + "vue/no-required-prop-with-default": "warn", + "vue/no-reserved-component-names": "error", + "vue/no-reserved-keys": "error", + "vue/no-reserved-props": "error", + "vue/no-shared-component-data": "error", + "vue/no-side-effects-in-computed-properties": "error", + "vue/no-watch-after-await": "error", + "vue/prefer-import-from-vue": "error", + "vue/prop-name-casing": ["error", "camelCase"], + "vue/require-prop-type-constructor": "error", + "vue/require-render-return": "error", + "vue/require-slots-as-functions": "error", + "vue/return-in-computed-property": "error", + "vue/return-in-emits-validator": "error", + "vue/valid-define-emits": "error", + "vue/valid-define-options": "error", + "vue/valid-define-props": "error", + "vue/valid-next-tick": "error", + + "jsdoc/check-access": "warn", + "jsdoc/check-property-names": "warn", + "jsdoc/empty-tags": "warn", + "jsdoc/implements-on-classes": "warn", + "jsdoc/no-defaults": "warn", + "jsdoc/require-param-name": "warn", + "jsdoc/require-property": "warn", + "jsdoc/require-property-description": "warn", + "jsdoc/require-property-name": "warn", + "jsdoc/require-returns-description": "warn", + + // part of the correctness category, but not wanted here + "no-constant-condition": "off", + "no-empty-pattern": "off", + "import/default": "off", + "import/namespace": "off", + "jsdoc/check-tag-names": "off", + "jsdoc/require-yields": "off", + "typescript/no-useless-empty-export": "off", + "unicorn/no-empty-file": "off", + "unicorn/no-thenable": "off", + "unicorn/no-useless-spread": "off", + // the formatter lowercases hex digits + "unicorn/number-literal-case": "off" + }, + "overrides": [ + { + "files": ["packages/**"], + "rules": { + "no-restricted-imports": ["error", { "paths": ["vitest", "path", "vitest/node"] }] + } + }, + { + // these packages declare vitest as a peer dependency + "files": ["packages/{coverage-*,ui,browser,web-worker,browser-*}/**"], + "rules": { + "no-restricted-imports": ["error", { "paths": ["path"] }] + } + }, + { + "files": ["packages/browser/src/client/orchestrator.ts"], + "rules": { + "no-restricted-imports": ["error", { "paths": ["vitest/internal/browser", "vitest/node"] }] + } + }, + { + // ivya must stay in a single rollup chunk, see the browser rollup config + "files": ["packages/browser/**"], + "excludeFiles": [ + "packages/browser/src/vendor-types.ts", + "packages/browser/src/client/tester/aria.ts", + "packages/browser/src/client/tester/locators.ts", + "packages/browser/src/client/tester/expect/**", + "packages/browser/src/client/utils.ts" + ], + "rules": { + "no-restricted-imports": ["error", { "paths": ["ivya", "ivya/utils", "ivya/aria"] }] + } + }, + { + "files": ["docs/**", "packages/web-worker/**", "test/unit/**"], + "rules": { + "no-restricted-globals": "off" + } + }, + { + "files": ["docs/**"], + "rules": { + "prefer-arrow-callback": "off", + "typescript/method-signature-style": "off", + "no-self-compare": "off", + "no-throw-literal": "off", + "no-unused-vars": "off", + "import/no-duplicates": "off", + "import/no-mutable-exports": "off" + } + }, + { + "files": ["test/**", "scripts/**", "docs/**", "examples/**", "**/*.config.*"], + "rules": { + "node/no-top-level-await": "off" + } + }, + { + "files": ["**/scripts/**", "**/cli.*", "**/cli/**"], + "rules": { + "no-console": "off" + } + }, + { + "files": ["**/*.cjs"], + "rules": { + "typescript/no-require-imports": "off" + } + } + ] +} diff --git a/.tazerc.json b/.tazerc.json index fd46acea9..cfff77b6c 100644 --- a/.tazerc.json +++ b/.tazerc.json @@ -1,8 +1,5 @@ { - "exclude": [ - "vue", - "pretty-format" - ], + "exclude": ["vue", "pretty-format"], "packageMode": { "vue": "minor", "codemirror": "minor", diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f63d683be..a3311b458 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,3 @@ { - "recommendations": [ - "vitest.explorer", - "dbaeumer.vscode-eslint" - ] + "recommendations": ["vitest.explorer", "oxc.oxc-vscode"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 2ec80fa65..5f15fd816 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,44 +1,16 @@ { - // Disable the default formatter, use eslint instead - "prettier.enable": false, - "editor.formatOnSave": false, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true, - // Auto fix "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit", + "source.fixAll.oxc": "explicit", "source.organizeImports": "never" }, - // Silent the stylistic rules in you IDE, but still auto fix them - // "eslint.rules.customizations": [ - // { "rule": "style/*", "severity": "off" }, - // { "rule": "*-indent", "severity": "off" }, - // { "rule": "*-spacing", "severity": "off" }, - // { "rule": "*-spaces", "severity": "off" }, - // { "rule": "*-order", "severity": "off" }, - // { "rule": "*-dangle", "severity": "off" }, - // { "rule": "*-newline", "severity": "off" }, - // { "rule": "*quotes", "severity": "off" }, - // { "rule": "*semi", "severity": "off" } - // ], - "vitest.ignoreWorkspace": true, "vitest.configSearchPatternInclude": "test/{unit,e2e,config,browser,reporters}/{vitest,vite}.{config.ts,config.unit.mts}", "testing.automaticallyOpenTestResults": "neverOpen", - // Enable eslint for all supported languages - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "typescriptreact", - "vue", - "html", - "markdown", - "json", - "jsonc", - "yaml" - ], // Use the project's typescript version "js/ts.tsdk.path": "node_modules/typescript/lib" } diff --git a/AGENTS.md b/AGENTS.md index 10e0aa404..b750330a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,20 +24,23 @@ Vitest is a next-generation testing framework powered by Vite. This is a monorep ## Setup and Development ### Initial Setup + 1. Run `pnpm install` to install dependencies 2. Run `pnpm build` to build all packages 3. Install Playwright browsers when working with browser features: `npx playwright install --with-deps` ### Key Scripts + - `pnpm build` - Build all packages - `pnpm dev` - Watch mode for development -- `pnpm lint` - Run ESLint -- `pnpm lint:fix` - Fix linting issues automatically +- `pnpm lint` - Run oxlint and check formatting with oxfmt +- `pnpm lint:fix` - Fix lint issues and format with oxfmt - `pnpm typecheck` - Run TypeScript type checking ## Testing ### Running Tests + - **All tests**: `CI=true pnpm test:ci` - **Examples**: `CI=true pnpm test:examples` - **Specific test suite**: `CI=true cd test/ && pnpm test ` @@ -70,6 +73,7 @@ Tests execute built output: test suites resolve `vitest` through workspace symli - `pnpm dev` (watch mode) rebuilds JS only; the `.d.ts` bundling configs are skipped in watch mode. After changing public types, run a full build before checking anything against `dist/*.d.ts`. ### Testing Utilities + - **`runInlineTests`** from `test/test-utils/index.ts` - You must use this for complex file system setups (>1 file) - **`runVitest`** from `test/test-utils/index.ts` - You can use this to run Vitest programmatically - **No mocking policy** - You must never mock anything in tests @@ -85,12 +89,13 @@ Behavior you must know: - Never mutate committed fixture files from a test; e2e tests run in parallel. Tests that only need an editable directory must use `runInlineTests`. Tests that genuinely need git-tracked files (for example `--changed`) must be added to the `serialTests` list in `test/e2e/vitest.config.ts`. - In watch-mode tests, mutate files only with `createFile`/`editFile` from `test/test-utils`: they restore content and mtime after the test, so the next test's watcher sees no phantom change. Call them inside a test, not in hooks (cleanup registers via `onTestFinished`). Always pass a small explicit `root`; `runVitest({ watch: true, root })` waits for the watcher to be ready before resolving. -- ESLint's test rules are disabled in this repo, so a stray `.only` passes lint. Check for and remove it yourself. +- The vitest lint plugin is not enabled in this repo, so a stray `.only` passes lint. Check for and remove it yourself. - CI runs the unit, e2e, coverage, and browser suites on Windows, plus an e2e leg on macOS. Vitest reports paths with forward slashes, so normalize `\` to `/` before comparing against `import.meta.filename`, `process.execArgv`, or other raw OS paths. Never use unix-only commands like `rm -rf` or `cp -r` in package.json scripts; use a node script or rimraf. ## Project Structure ### Core Packages (`packages/`) + - `vitest` - Main testing framework, including the test runner core (all imported packages, except `@vitest/mocker`, from this repository are inlined into its bundle, they are not imported at runtime) - `browser` - Browser testing support - `browser-playwright` / `browser-preview` - Browser mode providers @@ -105,6 +110,7 @@ Behavior you must know: - `web-worker` - Web Worker simulation for Node.js ### Test Organization (`test/`) + - `test/unit` - Core functionality tests - `test/e2e` - End-to-end tests run through `runVitest`/`runInlineTests` - `test/browser` - Browser-specific tests @@ -112,6 +118,7 @@ Behavior you must know: - Various test suites organized by feature ### Important Directories + - `docs/` - Documentation (Vite-powered) - `examples/` - Example projects and integrations - `scripts/` - Build and development scripts @@ -121,20 +128,25 @@ Behavior you must know: ## Code Style and Conventions ### Formatting and Linting -- **Always run** `pnpm lint:fix` after making changes + +- Linting is done by [oxlint](https://oxc.rs/docs/guide/usage/linter) (`.oxlintrc.json`). Formatting, including import order, is done only by [oxfmt](https://oxc.rs/docs/guide/usage/formatter) (`.oxfmtrc.json`); there are no stylistic lint rules +- **Always run** `pnpm lint:fix` after making changes; it runs `oxlint --fix` and then `oxfmt` +- `pnpm lint` runs `oxlint` and `oxfmt --check`; both must pass in CI - Fix non-auto-fixable errors manually -- Run lint as `CI=true pnpm lint` from a terminal inside an editor or agent harness: the config disables some rules when it detects an editor environment, and those rules still fail in CI +- Suppress a rule with `// oxlint-disable-next-line `, never with `eslint-disable`; an unused directive is a lint error +- Test fixtures (`**/fixtures`) are neither linted nor formatted; `*.d.ts` files and markdown code blocks are formatted but not linted Rules that `lint:fix` cannot fix: -- Never `import ... from 'path'`; it is an ESLint error everywhere. Prefer `pathe` (the dominant convention; it normalizes paths to posix), though `node:path` is allowed in Node-only code. +- Never `import ... from 'path'`; it is a lint error everywhere. Prefer `pathe` (the dominant convention; it normalizes paths to posix), though `node:path` is allowed in Node-only code. - Source in `packages/*/src` must not import from `vitest` or `vitest/node`, even type-only. Exception: packages that declare vitest as a peer dependency (`coverage-*`, `ui`, `browser`, `browser-*`, `web-worker`). -- `console.log` in package source is an ESLint error; only `console.warn` and `console.error` are allowed. Remove debug logging, and give intentional console output an explicit eslint-disable comment. +- `console.log` in package source is a lint error; only `console.warn` and `console.error` are allowed. Remove debug logging, and give intentional console output an explicit `oxlint-disable-next-line no-console` comment. - Use `globalThis`, never `global` or `self` (allowed only in `docs/`, `packages/web-worker/`, and `test/unit/`). -- No top-level `await` in `packages/*/src` (allowed in `test/`, `scripts/`, and config files); no `const enum`; no `export =`. -- In `packages/browser`, do not import from `ivya` outside the files that already do; ESLint enforces this so ivya stays in a single rollup chunk. Reuse the existing entry points. +- No top-level `await` in `packages/*/src` (allowed in `test/`, `scripts/`, `docs/`, `examples/`, and config files). `const enum` and `export =` are also banned, but the linter does not check them. +- In `packages/browser`, do not import from `ivya` outside the files that already do; oxlint enforces this so ivya stays in a single rollup chunk. Reuse the existing entry points. ### TypeScript + - Strict TypeScript configuration - Use `pnpm typecheck` to verify types - Configuration files: `tsconfig.base.json`, `tsconfig.build.json`, `tsconfig.check.json` @@ -142,6 +154,7 @@ Rules that `lint:fix` cannot fix: - Root typecheck does not cover the UI client Vue code; when changing `packages/ui/client`, also run `pnpm -C packages/ui typecheck:client` ### Code Quality + - ESM-first approach - Follow existing patterns in the codebase - Use utilities from `@vitest/utils/*` when available. Never import from `@vitest/utils` main entry point directly. @@ -160,6 +173,7 @@ Rules that `lint:fix` cannot fix: ## Common Workflows ### Adding New Features + 1. Identify the appropriate package in `packages/` 2. Follow existing code patterns 3. Add tests using testing utilities @@ -167,10 +181,12 @@ Rules that `lint:fix` cannot fix: 5. Add tests with relevant test suites ### Debugging + - Use VS Code: `⇧⌘B` (Shift+Cmd+B) or `Ctrl+Shift+B` for dev tasks - Check `scripts/` directory for specialized development tools ### Documentation + - Docs live in `docs/` (VitePress); read `docs/AGENTS.md` before working on them - After ANY change to CLI options or their descriptions in `packages/vitest/src/node/cli/cli-config.ts`, run `pnpm -C docs run cli-table` and commit the regenerated `docs/guide/cli-generated.md`; never edit that file by hand @@ -191,15 +207,18 @@ Other blocking CI jobs: ## Dependencies and Tools ### Key Dependencies + - **Vite** - Build tool and dev server - **Rollup** - Bundler -- **ESLint** - Linting +- **oxlint** - Linting +- **oxfmt** - Formatting - **TypeScript** - Type checking - **Playwright** - Browser testing - **Chai/Expect** - Assertions - **Tinybench** - Benchmarking ### Adding and Updating Dependencies + - New runtime deps for `packages/*` usually go into `devDependencies`: Rollup marks only `dependencies` as external and bundles everything else. Use `dependencies` only for `@types/*` packages, deps that cannot be bundled (binaries), or deps whose own types appear in Vitest's public types (see "Notes on Dependencies" in CONTRIBUTING.md). - Add deps with `pnpm add ` inside the target package: `catalogMode: prefer` writes `catalog:` into package.json and adds the version to the default catalog in `pnpm-workspace.yaml` automatically. To bump a shared dep, edit its catalog entry, never per-package ranges. - The `overrides` in `pnpm-workspace.yaml` force one version of `vite`, `rollup`, `@types/node`, `acorn`, and `mlly` across the workspace; editing a range in an individual package.json changes what gets published, not what installs locally. @@ -209,10 +228,12 @@ Other blocking CI jobs: - pnpm enforces a 24h `minimumReleaseAge`: installing a version published less than a day ago either resolves to an older version or appends the pick to `minimumReleaseAgeExclude` in `pnpm-workspace.yaml`. Both outcomes are expected; commit the yaml change instead of reverting it. ## Browser Testing + - Providers: Playwright (`@vitest/browser-playwright`) and preview (`@vitest/browser-preview`); the WebDriverIO provider is maintained outside this monorepo - Component testing supported (Vue, React, Svelte via official `vitest-browser-*` packages, other frameworks via Testing Library) ## Performance Considerations + - This is a performance-critical testing framework - Pay attention to import costs and bundle size - Use lazy loading where appropriate @@ -221,12 +242,14 @@ Other blocking CI jobs: ## Troubleshooting ### Common Issues + - Ensure pnpm is used (not npm/yarn) - Build before running tests - Check Node.js version compatibility - Playwright browsers must be installed for browser tests ### Getting Help + - Check existing issues and documentation - Review CONTRIBUTING.md for detailed guidelines - Follow patterns in existing code diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8bcd08965..0144acf0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,8 @@ To develop and test `vitest` package: 1. Run `pnpm install` in `vitest`'s root folder 2. Run `pnpm run build` to build all monorepo packages - - after this, you can use `pnpm run dev` to rebuild packages as you modify code + +- after this, you can use `pnpm run dev` to rebuild packages as you modify code 3. Run - `pnpm run test` to run core tests diff --git a/_typos.toml b/_typos.toml index c386660c6..ea0661edc 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,9 +1,5 @@ [files] -extend-exclude = [ - "*.js.map", - "*.svg", - "test/e2e/test/fixtures/reporters/html/**", -] +extend-exclude = ["*.js.map", "*.svg", "test/e2e/test/fixtures/reporters/html/**"] [default.extend-words] # AsyncLocalStorage abbreviation diff --git a/docs/.vitepress/blog.data.ts b/docs/.vitepress/blog.data.ts index 8aa4dc4a7..d9799a454 100644 --- a/docs/.vitepress/blog.data.ts +++ b/docs/.vitepress/blog.data.ts @@ -17,8 +17,7 @@ export default createContentLoader('blog/*.md', { transform(raw): Post[] { return raw .map(({ url, frontmatter }) => ({ - title: frontmatter.head.find((e: any) => e[1].property === 'og:title')[1] - .content, + title: frontmatter.head.find((e: any) => e[1].property === 'og:title')[1].content, url, date: formatDate(frontmatter.date), })) diff --git a/docs/.vitepress/components/Advanced.vue b/docs/.vitepress/components/Advanced.vue index de842162c..8bd33cc5c 100644 --- a/docs/.vitepress/components/Advanced.vue +++ b/docs/.vitepress/components/Advanced.vue @@ -1,5 +1,9 @@ diff --git a/docs/.vitepress/components/BlogIndex.vue b/docs/.vitepress/components/BlogIndex.vue index b702f1617..95244ffb6 100644 --- a/docs/.vitepress/components/BlogIndex.vue +++ b/docs/.vitepress/components/BlogIndex.vue @@ -10,9 +10,7 @@ function getDateTime(time: number) {
  • - +

    {{ post.title }}

    diff --git a/docs/.vitepress/components/CRoot.vue b/docs/.vitepress/components/CRoot.vue index b01d65081..464f4c5f6 100644 --- a/docs/.vitepress/components/CRoot.vue +++ b/docs/.vitepress/components/CRoot.vue @@ -3,7 +3,11 @@ import { Icon } from '@iconify/vue' diff --git a/docs/.vitepress/components/CopyPrompt.vue b/docs/.vitepress/components/CopyPrompt.vue index bf253ab5e..7cc1b2091 100644 --- a/docs/.vitepress/components/CopyPrompt.vue +++ b/docs/.vitepress/components/CopyPrompt.vue @@ -7,14 +7,13 @@ const props = defineProps<{ }>() const state = ref<'idle' | 'copied' | 'error'>('idle') -const label = computed(() => state.value === 'copied' ? 'Copied' : 'Copy prompt') +const label = computed(() => (state.value === 'copied' ? 'Copied' : 'Copy prompt')) async function copyPrompt() { try { await navigator.clipboard.writeText(props.prompt) state.value = 'copied' - } - catch { + } catch { state.value = 'error' } } @@ -26,6 +25,12 @@ async function copyPrompt() { {{ label }} - {{ state === 'copied' ? 'Prompt copied to clipboard.' : state === 'error' ? 'Could not copy prompt.' : '' }} + {{ + state === 'copied' + ? 'Prompt copied to clipboard.' + : state === 'error' + ? 'Could not copy prompt.' + : '' + }} diff --git a/docs/.vitepress/components/Deprecated.vue b/docs/.vitepress/components/Deprecated.vue index 9ae996276..b5d12e2e4 100644 --- a/docs/.vitepress/components/Deprecated.vue +++ b/docs/.vitepress/components/Deprecated.vue @@ -1,5 +1,3 @@ diff --git a/docs/.vitepress/components/Experimental.vue b/docs/.vitepress/components/Experimental.vue index b35721c98..40292f00f 100644 --- a/docs/.vitepress/components/Experimental.vue +++ b/docs/.vitepress/components/Experimental.vue @@ -1,5 +1,9 @@ diff --git a/docs/.vitepress/components/FeaturesList.vue b/docs/.vitepress/components/FeaturesList.vue index 9ab874de8..a16b98da7 100644 --- a/docs/.vitepress/components/FeaturesList.vue +++ b/docs/.vitepress/components/FeaturesList.vue @@ -3,31 +3,68 @@ import ListItem from './ListItem.vue'