Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d30cf2d
feat: support native loading of TS config files
aryaemami59 Jan 29, 2025
05752e9
Workaround unflagged `--experimental-strip-types`
aryaemami59 Jan 30, 2025
26c2555
ci: only test native type transformation on ubuntu
aryaemami59 Mar 7, 2025
340daee
ci: only pass `--experimental-transform-types` for Node.js 22
aryaemami59 Mar 7, 2025
501f2a1
fix: update error message for `unstable_native_nodejs_ts_config` flag
aryaemami59 Mar 7, 2025
c70ee7d
Add `describe` block
aryaemami59 Mar 12, 2025
0600a99
ci: add `NODE_OPTIONS` to CI workflow matrix
aryaemami59 Mar 13, 2025
c9f8dfc
ci: remove Node.js 23.x from CI workflow matrix
aryaemami59 Mar 13, 2025
4ce4ea8
refactor: simplify TypeScript support check in `config-loader.js`
aryaemami59 Mar 13, 2025
ac56991
refactor: move comment in `cli-engine.js`
aryaemami59 Mar 15, 2025
ca00c65
refactor: move comment in `eslint.js`
aryaemami59 Mar 15, 2025
71a6c74
refactor: improve JSDoc return type description for `dynamicImport`
aryaemami59 Mar 15, 2025
b1644b5
chore: fix `dynamicImport` function
aryaemami59 Mar 15, 2025
5427038
refactor: rename `dynamicImport` to `dynamicImportConfig` for clarity
aryaemami59 Mar 15, 2025
eabfb9d
refactor: re-use `dynamicImportConfig`
aryaemami59 Mar 15, 2025
167f9b1
chore: add unit-tests for CJS and ESM
aryaemami59 Mar 16, 2025
41c8078
Fix indentation
aryaemami59 Mar 16, 2025
ec65a0a
Add specific error message for older Node.js versions
aryaemami59 Mar 16, 2025
c111184
Fix unit-tests
aryaemami59 Mar 16, 2025
f6bea88
docs: add section on native TypeScript support
aryaemami59 Mar 16, 2025
3d69372
chore: update `@types/node` to version 22.13.14
aryaemami59 Mar 16, 2025
5a747de
Fix formatting
aryaemami59 Mar 24, 2025
8f08903
Add unit-test for when `process.features.typescript` is set to `false`
aryaemami59 Mar 27, 2025
fbcf61b
Fix `configuration-files.md`
aryaemami59 Mar 27, 2025
dbac643
Move `Native TypeScript Support` section in `configuration-files.md`
aryaemami59 Mar 27, 2025
c6add69
Use the `in` operator
aryaemami59 Mar 27, 2025
7e5f91f
Fix error message for older versions of Node.js
aryaemami59 Mar 27, 2025
3359c3c
Add comment about `NODE_OPTIONS` in `ci.yml`
aryaemami59 Mar 27, 2025
82c2029
Switch from `Rule` to `RuleDefinition`
aryaemami59 Mar 27, 2025
35944a1
Re-use existing flags in unit-tests
aryaemami59 Mar 27, 2025
d4c4cb3
Fix false positives in unit tests
aryaemami59 Mar 27, 2025
6fb98bf
Add more unit-tests for `--experimental-strip-types` alone
aryaemami59 Mar 27, 2025
b3898f2
Fix unit-tests
aryaemami59 Mar 28, 2025
f65f47f
Fix minor JSDoc-related issues
aryaemami59 Mar 28, 2025
f8e1ebf
Remove unnecessary `afterEach` hook in ESLint tests
aryaemami59 Mar 28, 2025
3f5c55c
Remove unnecessary conditional `describe` call
aryaemami59 Mar 28, 2025
b0f6da7
Add `beforeEach` hook
aryaemami59 Mar 28, 2025
5ed1c55
Add `eslintConfigFiles` and `nativeTSConfigFileFlags` helpers
aryaemami59 Mar 29, 2025
518d9db
Remove unnecessary `stub.restore()` calls
aryaemami59 Mar 29, 2025
76118fa
Fix minor JSDoc issue in `eslint.js`
aryaemami59 Mar 29, 2025
f44bd29
Rename `loadTypeScriptConfigFile` to `loadTypeScriptConfigFileWithJiti`
aryaemami59 Mar 31, 2025
23cf96f
Fix the types in `cli-engine.js`
aryaemami59 Apr 2, 2025
ee9b4c9
Change `Promise<void>` to `Promise<never>` in `eslint-helpers.js`
aryaemami59 Apr 2, 2025
46b0b6b
Revert all changes inside `cli-engine.js`
aryaemami59 Apr 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,19 @@ jobs:
matrix:
os: [ubuntu-latest]
node: [23.x, 22.x, 21.x, 20.x, 18.x, "18.18.0"]
NODE_OPTIONS: [""]
include:
- os: windows-latest
node: "lts/*"
- os: macOS-latest
node: "lts/*"
- os: ubuntu-latest
node: 22.x

# `--experimental-strip-types` is enabled by default in Node.js 23.x.
# This additional environment is necessary only to test `--experimental-transform-types`,
# as it is not enabled by default in any Node.js version yet.
NODE_OPTIONS: "--experimental-transform-types"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -85,6 +93,8 @@ jobs:
- name: Install Packages
run: npm install
- name: Test
env:
NODE_OPTIONS: ${{ matrix.NODE_OPTIONS }}
run: node Makefile mocha
- name: Fuzz Test
run: node Makefile fuzz
Expand Down
10 changes: 10 additions & 0 deletions docs/src/use/configure/configuration-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,16 @@ You can then create a configuration file with a `.ts`, `.mts`, or `.cts` extensi
ESLint does not perform type checking on your configuration file and does not apply any settings from `tsconfig.json`.
:::

### Native TypeScript Support

If you're using **Node.js >= 22.6.0**, you can load TypeScript configuration files natively without requiring [`jiti`](https://github.com/unjs/jiti). This is possible thanks to the [**`--experimental-strip-types`**](https://nodejs.org/docs/latest-v22.x/api/cli.html#--experimental-strip-types) flag.

Since this feature is still experimental, you must also enable the `unstable_native_nodejs_ts_config` flag.

```bash
npx --node-options='--experimental-strip-types' eslint --flag unstable_native_nodejs_ts_config
```

### Configuration File Precedence

If you have multiple ESLint configuration files, ESLint prioritizes JavaScript files over TypeScript files. The order of precedence is as follows:
Expand Down
142 changes: 108 additions & 34 deletions lib/config/config-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ const { FlatConfigArray } = require("./flat-config-array");
//-----------------------------------------------------------------------------

/**
* @typedef {import("../shared/types").FlatConfigObject} FlatConfigObject
* @typedef {import("../shared/types").FlatConfigArray} FlatConfigArray
* @import { ConfigData, ConfigData as FlatConfigObject } from "../shared/types.js";
*/

/**
* @typedef {Object} ConfigLoaderOptions
* @property {string|false|undefined} configFile The path to the config file to use.
* @property {string} cwd The current working directory.
* @property {boolean} ignoreEnabled Indicates if ignore patterns should be honored.
* @property {FlatConfigArray} [baseConfig] The base config to use.
* @property {Array<FlatConfigObject>} [defaultConfigs] The default configs to use.
* @property {Array<string>} [ignorePatterns] The ignore patterns to use.
* @property {FlatConfigObject|Array<FlatConfigObject>} overrideConfig The override config to use.
* @property {FlatConfigObject|Array<FlatConfigObject>} [overrideConfig] The override config to use.
* @property {boolean} [hasUnstableNativeNodeJsTSConfigFlag] The flag to indicate whether the `unstable_native_nodejs_ts_config` flag is enabled.
*/

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -108,12 +111,87 @@ function isRunningInDeno() {
return !!globalThis.Deno;
}

/**
* Checks if native TypeScript support is
* enabled in the current Node.js process.
*
* This function determines if the
* {@linkcode NodeJS.ProcessFeatures.typescript | typescript}
* feature is present in the
* {@linkcode process.features} object
* and if its value is either "strip" or "transform".
* @returns {boolean} `true` if native TypeScript support is enabled, otherwise `false`.
* @since 9.24.0
*/
function isNativeTypeScriptSupportEnabled() {
return (
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- it's still an experimental feature.
["strip", "transform"].includes(process.features.typescript)
);
}

/**
* Load the TypeScript configuration file.
* @param {string} filePath The absolute file path to load.
* @param {URL} fileURL The file URL to load.
* @param {number} mtime The last modified timestamp of the file.
* @returns {Promise<any>} The configuration loaded from the file.
* @since 9.24.0
*/
async function loadTypeScriptConfigFileWithJiti(filePath, fileURL, mtime) {
// eslint-disable-next-line no-use-before-define -- `ConfigLoader.loadJiti` can be overwritten for testing
const { createJiti } = await ConfigLoader.loadJiti().catch(() => {
throw new Error(
"The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.",
);
});

// `createJiti` was added in jiti v2.
if (typeof createJiti !== "function") {
throw new Error(
"You are using an outdated version of the 'jiti' library. Please update to the latest version of 'jiti' to ensure compatibility and access to the latest features.",
);
}

/*
* Disabling `moduleCache` allows us to reload a
* config file when the last modified timestamp changes.
*/

const jiti = createJiti(__filename, {
moduleCache: false,
interopDefault: false,
});
const config = await jiti.import(fileURL.href);

importedConfigFileModificationTime.set(filePath, mtime);

return config?.default ?? config;
}

/**
* Dynamically imports a module from the given file path.
* @param {string} filePath The absolute file path of the module to import.
* @param {URL} fileURL The file URL to load.
* @param {number} mtime The last modified timestamp of the file.
* @returns {Promise<any>} - A {@linkcode Promise | promise} that resolves to the imported ESLint config.
* @since 9.24.0
*/
async function dynamicImportConfig(filePath, fileURL, mtime) {
const module = await import(fileURL.href);

importedConfigFileModificationTime.set(filePath, mtime);

return module.default;
}

/**
* Load the config array from the given filename.
* @param {string} filePath The filename to load from.
* @param {boolean} hasUnstableNativeNodeJsTSConfigFlag The flag to indicate whether the `unstable_native_nodejs_ts_config` flag is enabled.
* @returns {Promise<any>} The config loaded from the config file.
*/
async function loadConfigFile(filePath) {
async function loadConfigFile(filePath, hasUnstableNativeNodeJsTSConfigFlag) {
debug(`Loading config from ${filePath}`);

const fileURL = pathToFileURL(filePath);
Expand Down Expand Up @@ -161,44 +239,36 @@ async function loadConfigFile(filePath) {
*
* When Node.js supports native TypeScript imports, we can remove this check.
*/
if (isTS && !isDeno && !isBun) {
// eslint-disable-next-line no-use-before-define -- `ConfigLoader.loadJiti` can be overwritten for testing
const { createJiti } = await ConfigLoader.loadJiti().catch(() => {
throw new Error(
"The 'jiti' library is required for loading TypeScript configuration files. Make sure to install it.",
);
});

// `createJiti` was added in jiti v2.
if (typeof createJiti !== "function") {
if (isTS) {
if (hasUnstableNativeNodeJsTSConfigFlag) {
if (isNativeTypeScriptSupportEnabled()) {
return await dynamicImportConfig(filePath, fileURL, mtime);
}

if (!("typescript" in process.features)) {
throw new Error(
"The unstable_native_nodejs_ts_config flag is not supported in older versions of Node.js.",
);
}

throw new Error(
"You are using an outdated version of the 'jiti' library. Please update to the latest version of 'jiti' to ensure compatibility and access to the latest features.",
"The unstable_native_nodejs_ts_config flag is enabled, but native TypeScript support is not enabled in the current Node.js process. You need to either enable native TypeScript support by passing --experimental-strip-types or remove the unstable_native_nodejs_ts_config flag.",
);
}

/*
* Disabling `moduleCache` allows us to reload a
* config file when the last modified timestamp changes.
*/

const jiti = createJiti(__filename, {
moduleCache: false,
interopDefault: false,
});
const config = await jiti.import(fileURL.href);

importedConfigFileModificationTime.set(filePath, mtime);

return config?.default ?? config;
if (!isDeno && !isBun) {
return await loadTypeScriptConfigFileWithJiti(
filePath,
fileURL,
mtime,
);
}
}

// fallback to normal runtime behavior

const config = (await import(fileURL)).default;

importedConfigFileModificationTime.set(filePath, mtime);

return config;
return await dynamicImportConfig(filePath, fileURL, mtime);
}

//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -495,6 +565,7 @@ class ConfigLoader {
ignoreEnabled,
ignorePatterns,
overrideConfig,
hasUnstableNativeNodeJsTSConfigFlag = false,
defaultConfigs = [],
} = options;

Expand All @@ -510,7 +581,10 @@ class ConfigLoader {
// load config file
if (configFilePath) {
debug(`Loading config file ${configFilePath}`);
const fileConfig = await loadConfigFile(configFilePath);
const fileConfig = await loadConfigFile(
configFilePath,
hasUnstableNativeNodeJsTSConfigFlag,
);

/*
* It's possible that a config file could be empty or else
Expand Down
11 changes: 7 additions & 4 deletions lib/config/flat-config-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
// Typedefs
//------------------------------------------------------------------------------

/** @typedef {import("../types").Rule.RuleModule} Rule */
/**
* @import { RuleDefinition } from "@eslint/core";
* @import { Linter } from "eslint";
*/

//------------------------------------------------------------------------------
// Private Members
Expand Down Expand Up @@ -59,8 +62,8 @@ function parseRuleId(ruleId) {
/**
* Retrieves a rule instance from a given config based on the ruleId.
* @param {string} ruleId The rule ID to look for.
* @param {FlatConfig} config The config to search.
* @returns {import("../shared/types").Rule|undefined} The rule if found
* @param {Linter.Config} config The config to search.
* @returns {RuleDefinition|undefined} The rule if found
* or undefined if not.
*/
function getRuleFromConfig(ruleId, config) {
Expand All @@ -71,7 +74,7 @@ function getRuleFromConfig(ruleId, config) {

/**
* Gets a complete options schema for a rule.
* @param {Rule} rule A rule object
* @param {RuleDefinition} rule A rule object
* @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`.
* @returns {Object|null} JSON Schema for the rule's options. `null` if `meta.schema` is `false`.
*/
Expand Down
12 changes: 9 additions & 3 deletions lib/eslint/eslint-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ const MINIMATCH_OPTIONS = { dot: true };
// Types
//-----------------------------------------------------------------------------

/**
* @import { ESLintOptions } from "./eslint.js";
* @import { LintMessage, LintResult } from "../shared/types.js";
* @import { ConfigLoader, LegacyConfigLoader } from "../config/config-loader.js";
*/

/**
* @typedef {Object} GlobSearch
* @property {Array<string>} patterns The normalized patterns to use for a search.
Expand Down Expand Up @@ -115,7 +121,7 @@ function isNonEmptyString(value) {
*/
function isArrayOfNonEmptyString(value) {
return (
Array.isArray(value) && value.length && value.every(isNonEmptyString)
Array.isArray(value) && !!value.length && value.every(isNonEmptyString)
);
}

Expand Down Expand Up @@ -351,7 +357,7 @@ async function globSearch({
* as the user inputted them. Used for errors.
* @param {Array<string>} options.unmatchedPatterns A non-empty array of absolute path glob patterns
* that were unmatched in the original search.
* @returns {void} Always throws an error.
* @returns {Promise<never>} Always throws an error.
* @throws {NoFilesFoundError} If the first unmatched pattern
* doesn't match any files even when there are no ignores.
* @throws {AllFilesIgnoredError} If the first unmatched pattern
Expand Down Expand Up @@ -454,7 +460,7 @@ async function globMultiSearch({
}
}

// second loop for `fulfulled` results
// second loop for `fulfilled` results
return results.flatMap(result => result.value);
}

Expand Down
33 changes: 16 additions & 17 deletions lib/eslint/eslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,15 @@ const { ConfigLoader, LegacyConfigLoader } = require("../config/config-loader");
//------------------------------------------------------------------------------

// For VSCode IntelliSense
/** @typedef {import("../cli-engine/cli-engine").ConfigArray} ConfigArray */
/** @typedef {import("../shared/types").ConfigData} ConfigData */
/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */
/** @typedef {import("../shared/types").LintMessage} LintMessage */
/** @typedef {import("../shared/types").LintResult} LintResult */
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
/** @typedef {import("../shared/types").Plugin} Plugin */
/** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */
/** @typedef {import("../shared/types").RuleConf} RuleConf */
/** @typedef {import("../types").Rule.RuleModule} Rule */
/**
* @import { ConfigArray } from "../cli-engine/cli-engine.js";
* @import { CLIEngineLintReport } from "./legacy-eslint.js";
* @import { FlatConfigArray } from "../config/flat-config-array.js";
* @import { RuleDefinition } from "@eslint/core";
* @import { ConfigData, DeprecatedRuleInfo, LintMessage, LintResult, Plugin, ResultsMeta } from "../shared/types.js";
*/

/** @typedef {ReturnType<ConfigArray.extractConfig>} ExtractedConfig */
/** @typedef {import('../cli-engine/cli-engine').CLIEngine} CLIEngine */
/** @typedef {import('./legacy-eslint').CLIEngineLintReport} CLIEngineLintReport */

/**
* The options with which to configure the ESLint instance.
Expand Down Expand Up @@ -113,7 +109,7 @@ const removedFormatters = new Set([

/**
* Create rulesMeta object.
* @param {Map<string,Rule>} rules a map of rules from which to generate the object.
* @param {Map<string,RuleDefinition>} rules a map of rules from which to generate the object.
* @returns {Object} metadata for all enabled rules.
*/
function createRulesMeta(rules) {
Expand All @@ -138,7 +134,7 @@ const usedDeprecatedRulesCache = new WeakMap();

/**
* Create used deprecated rule list.
* @param {CLIEngine} eslint The CLIEngine instance.
* @param {ESLint} eslint The ESLint instance.
* @param {string} maybeFilePath The absolute path to a lint target file or `"<text>"`.
* @returns {DeprecatedRuleInfo[]} The used deprecated rule list.
*/
Expand Down Expand Up @@ -191,7 +187,7 @@ function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) {
/**
* Processes the linting results generated by a CLIEngine linting report to
* match the ESLint class's API.
* @param {CLIEngine} eslint The CLIEngine instance.
* @param {ESLint} eslint The ESLint instance.
* @param {CLIEngineLintReport} report The CLIEngine linting report to process.
* @returns {LintResult[]} The processed linting results.
*/
Expand Down Expand Up @@ -347,7 +343,7 @@ function verifyText({
/**
* Checks whether a message's rule type should be fixed.
* @param {LintMessage} message The message to check.
* @param {FlatConfig} config The config for the file that generated the message.
* @param {FlatConfigArray} config The config for the file that generated the message.
* @param {string[]} fixTypes An array of fix types to check.
* @returns {boolean} Whether the message should be fixed.
*/
Expand Down Expand Up @@ -375,7 +371,7 @@ function createExtraneousResultsError() {
* Creates a fixer function based on the provided fix, fixTypesSet, and config.
* @param {Function|boolean} fix The original fix option.
* @param {Set<string>} fixTypesSet A set of fix types to filter messages for fixing.
* @param {FlatConfig} config The config for the file that generated the message.
* @param {FlatConfigArray} config The config for the file that generated the message.
* @returns {Function|boolean} The fixer function or the original fix value.
*/
function getFixerForFixTypes(fix, fixTypesSet, config) {
Expand Down Expand Up @@ -440,6 +436,9 @@ class ESLint {
ignoreEnabled: processedOptions.ignore,
ignorePatterns: processedOptions.ignorePatterns,
defaultConfigs,
hasUnstableNativeNodeJsTSConfigFlag: linter.hasFlag(
"unstable_native_nodejs_ts_config",
),
};

this.#configLoader = linter.hasFlag("unstable_config_lookup_from_file")
Expand Down
Loading
Loading