Config File Parsing¶
Jest Runner reads jest.config.*, vitest.config.* / vite.config.*, playwright.config.*,
rstest.config.*, cypress.config.* and deno.json(c) to learn where the tests are and where coverage
reports go. It does so without executing the file: the file is parsed into a Babel AST and a small
evaluator (src/testDetection/configParsers/parseUtils.ts and src/utils/AstUtils.ts) turns the subset
that configs normally use into a plain object. Each framework then has a thin parser that picks the fields it
needs.
Why not just require() the config¶
A jest.config.ts needs a TypeScript loader; a vitest.config.ts imports from vitest/config and often
from the project's own code; both may read process.env or call functions. Executing that inside the
extension host would mean loading the project's dependencies into VS Code, with the project's Node version
semantics, for every package of a monorepo. The static approach is safe, fast and has no side effects; its
price is that dynamic configs are not understood, for which jestrunner.disableFrameworkConfig exists.
The generic evaluator¶
parseConfigObject(content, configPath?)¶
Parses content with @babel/parser (typescript and jsx plugins) and returns the exported config as a
JavaScript object, or undefined. Steps:
- Collect bindings (
collectBindings): every top-levelconst/let/varandexport constdeclaration is evaluated withastToValueand stored by name. Destructuring patterns (const { a, b = 1 } = obj) are supported.__dirnameand__filenameare bound to the literal strings'__dirname'and'__filename', which the framework parsers later resolve to the config's directory. - Resolve
require()(resolveRequireValue): a declarator initialized withrequire('./relative')is replaced by themodule.exportsof that file, resolved with the usual extensions (.js,.ts,.cjs,.mjs,index.*) and parsed the same way, recursively, with a visited set against cycles. Only relative paths; packages are not resolved. - Find the export:
export default <expr>,module.exports = <expr>orexports = <expr>. The expression is passed toresolveConfigNode. - Unwrap (
resolveConfigNode): parentheses,ascasts and TypeScript non-null assertions are stripped. Then: - an object or array literal is the config,
- an identifier is looked up in the bindings,
- a call expression (
defineConfig(x),mergeConfig(a, b),createJestConfig(...), anything) is unwrapped to its first argument that resolves, so wrappers of any name work; an arrow function argument (defineConfig(() => ({...}))) is unwrapped to its returned object, - a function expression is unwrapped to the object it returns.
- Evaluate with
astToValue(node, bindings).
astToValue(node, bindings)¶
A recursive evaluator for the literal subset of JavaScript:
| Node | Result |
|---|---|
string, number, boolean, null, undefined |
the value |
| identifier | the binding's value, undefined if unknown |
| array literal | array of evaluated elements |
| object literal | object of evaluated properties; ...spread of a resolvable object is merged |
| template literal | the string, if every ${expr} resolves |
a + b |
concatenation when both sides are strings |
member expression obj.prop |
the property of the evaluated object; x.name of an unknown x yields 'x' (for it(Foo.prototype.bar.name) style names) |
| call expression | the object arguments merged into one object (so mergeConfig(base, override) yields the merged config); other calls undefined |
anything else (conditions, process.env, new, regular expression literals) |
undefined |
A property that evaluates to undefined is simply missing from the result, so a partially dynamic config
still yields its static parts.
The same evaluator is used by the test file parser to resolve it.each tables and variable test names
(see Test File Parsing).
The framework parsers¶
All live in src/testDetection/configParsers/ and return TestPatterns objects (patterns, isRegex,
rootDir, roots, ignorePatterns, excludePatterns, dir), one per project.
jestParser.ts¶
getTestMatchFromJestConfig(path):.jsonfiles (and thejestkey ofpackage.json) are parsed as JSON, everything else withparseConfigObject.projectsentries are followed: strings are resolved relative to the config and parsed recursively (so'<rootDir>/packages/*'style globs are handed to the file system as-is and only literal paths resolve), objects are parsed inline.- Fields:
rootDir(__dirname→ config directory, otherwise resolved against it),roots,testMatch(globs),testRegex(string or array, marks the resultisRegex),testPathIgnorePatterns. A config with onlyrootsor ignore patterns yields an entry with empty patterns, which the caller fills with the defaults. parseCoverageDirectory(path, framework):coverageDirectoryfor Jest,coverage.reportsDirectory(also undertest.) for Vitest and Rstest, resolved againstrootDir/rootand the config directory.
vitestParser.ts¶
getVitestConfig(path): like Jest, withtest.include,test.exclude,test.dir,rootandtest.projects/projects(strings or objects, recursive).viteConfigHasTestAttribute(path): decides whether avite.config.*is a Vitest config at all. It is when the parsed object hastestorprojects; when the file cannot be parsed, the text is searched fortest =as a last resort.
playwrightParser.ts¶
getPlaywrightConfig(path):testDir(defaulttests),testMatch(string or string array; a regular expression literal cannot be evaluated statically, so the default patterns apply then),testIgnore. The result'sdiris the base for matching.getPlaywrightTestDir(path): thetestDiralone, used by the conflict check in test detection.
rstestParser.ts¶
getRstestConfig(path): root (__dirname understood), include, exclude as an array or as
{ patterns: [...] }, projects as paths or inline objects.
denoParser.ts¶
getDenoConfig(path): deno.json is parsed as JSON; deno.jsonc after stripping // and /* */
comments outside strings. Reads test.include and test.exclude; the config's directory is the root.
cypressParser.ts¶
getCypressSpecPattern(path): specPattern at the top level or under e2e, used only to exclude Cypress
specs from the Jest and Vitest trees.
configParsing.ts¶
Not a parser but the lookup helpers used by detection:
getConfigPath(dir, framework): the first config file name of the framework that exists indir, with the two special cases (vite.config.*must have atestsection,package.jsonmust have ajestkey).binaryExists(dir, name):node_modules/.bin/<name>,<name>.cmdornode_modules/<name>/package.json.resolveAndValidateCustomConfig(settingKey, file): resolvesjestrunner.configPath,jestrunner.vitestConfigPathorjestrunner.playwrightConfigPath(string or glob mapping) for a file against its workspace folder and returns it only if the file exists.getDefaultTestPatterns():jestrunner.defaultTestPatternsor the built-in defaults.
Examples of what resolves¶
import { defineConfig } from 'vitest/config';
const unitTests = ['src/**/*.test.ts'];
const base = { root: __dirname, test: { exclude: ['**/e2e/**'] } };
export default defineConfig({
...base,
test: { ...base.test, include: [...unitTests, `tests/${'integration'}/**/*.ts`] },
});
// jest.config.js
const { testMatch } = require('./jest.base.js'); // module.exports = { testMatch: [...] }
module.exports = { rootDir: __dirname, testMatch, projects: ['<rootDir>/packages/api'] };
import { nxPreset } from '@nx/jest/preset'; // package import: ignored
export default {
...nxPreset, // unknown spread: skipped
testMatch: process.env.CI ? ciMatch : devMatch, // conditional: undefined
coverageDirectory: '../../coverage/apps/web', // literal: kept
};
In the last case the Test Explorer still shows the tests found by the default patterns, and coverage is read
from the right directory; only a custom testMatch would be lost.
Error handling¶
Every parser catches its own errors, logs them to the Jest Runner channel and returns undefined, which
callers treat as "no config": the file name lookup continues with the next candidate and the default patterns
apply. A syntax error in a config therefore never breaks detection; it only shows up in the log.