Skip to content

Commit

Permalink
feat!: support eslint@9 and flat config
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed May 29, 2024
1 parent 799968c commit 492b253
Show file tree
Hide file tree
Showing 20 changed files with 507 additions and 725 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ module.exports = {
};
```

Configuration file can also be written in TypeScript if [`ts-node`](https://www.npmjs.com/package/ts-node) is installed. Use `--config` argument for TypeScript configuration file, e.g. `--config eslint-remote-tester.config.ts`.
Configuration file can also be written in TypeScript. Use `--config` argument for TypeScript configuration file, e.g. `--config eslint-remote-tester.config.ts`.

```ts
import type { Config } from 'eslint-remote-tester';
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"conventional-changelog-cli": "^5.0.0",
"eslint": "^9.3.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-local-rules": "^2.0.1",
"eslint-plugin-prettier": "^5.1.3",
"pkg-pr-new": "^0.0.5",
"prettier": "^3.2.5",
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
module.exports = {
import js from '@eslint/js';

/** @type {import('./src/config/types').Config} */
const config = {
repositories: [
'AriPerkkio/eslint-remote-tester-integration-test-target',
'AriPerkkio/aria-live-capture',
Expand All @@ -17,20 +20,7 @@ module.exports = {

concurrentTasks: 3,

eslintrc: {
root: true,
env: {
es6: true,
},
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
extends: ['eslint:recommended'],
},
eslintConfig: [js.configs.recommended],

cache: true,

Expand Down Expand Up @@ -61,3 +51,5 @@ module.exports = {
*/
onComplete: undefined,
};

export default config;
36 changes: 36 additions & 0 deletions packages/eslint-remote-tester/eslint-remote-tester.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import js from '@eslint/js';
import type { Config } from './src/types';

export default defineConfig({
repositories: [
'AriPerkkio/eslint-remote-tester-integration-test-target',
'AriPerkkio/aria-live-capture',
'AriPerkkio/extend-to-be-announced',
'AriPerkkio/state-mgmt-examples',
'AriPerkkio/suspense-examples',
],

extensions: ['js', 'jsx', 'ts', 'tsx'],

maxFileSizeBytes: undefined,

rulesUnderTesting: [],

resultParser: undefined,

concurrentTasks: 3,

eslintConfig: [js.configs.recommended] as any,

cache: true,

compare: false,

updateComparisonReference: true,

onComplete: undefined,
});

function defineConfig(config: Config) {
return config;
}
19 changes: 6 additions & 13 deletions packages/eslint-remote-tester/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@
"scripts": {
"prebuild": "rm -rf ./dist",
"build": "tsc --project tsconfig.prod.json",
"start": "node dist --config eslint-remote-tester.config.cjs",
"start": "node dist --config eslint-remote-tester.config.js",
"start:memory-limit-crash": "NODE_OPTIONS=--max_old_space_size=50 node dist",
"lint": "eslint . --max-warnings 0 --ext .js,.ts,.tsx && publint",
"test:integration": "vitest run --config test/integration/vitest.config.integration.ts",
"test:smoke": "vitest run --config test/smoke/vitest.config.smoke.ts",
"validate": "pnpm build && pnpm lint && pnpm test && pnpm test:integration"
Expand All @@ -39,37 +38,31 @@
"@babel/code-frame": "^7.24.6",
"JSONStream": "^1.3.5",
"chalk": "^4.1.2",
"importx": "^0.3.2",
"ink": "^3.2.0",
"object-hash": "^3.0.0",
"react": "^17.0.2",
"simple-git": "^3.24.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@types/babel__code-frame": "^7.0.6",
"@types/eslint": "^8.56.10",
"@types/node": "^20.12.12",
"@types/object-hash": "^3.0.6",
"@types/react": "^17.0.80",
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"conventional-changelog-cli": "^5.0.0",
"eslint": "^8.57.0",
"eslint": "^9.3.0",
"eslint-plugin-local-rules": "^2.0.1",
"eslint-remote-tester-repositories": "workspace:*",
"ink-testing-library": "^2.1.0",
"node-pty": "^1.0.0",
"strip-ansi": "^6.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"vitest": "^1.6.0"
},
"peerDependencies": {
"eslint": ">=7",
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"ts-node": {
"optional": true
}
"eslint": ">=9"
},
"keywords": [
"eslint",
Expand Down
4 changes: 2 additions & 2 deletions packages/eslint-remote-tester/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { loadConfig } from './load.js';
import { WorkerData } from '../engine/types.js';

const DEFAULT_CONFIGURATION_FILE_NAME = 'eslint-remote-tester.config';
const DEFAULT_CONFIGURATION_FILE_JS = `${DEFAULT_CONFIGURATION_FILE_NAME}.cjs`;
const DEFAULT_CONFIGURATION_FILE_JS = `${DEFAULT_CONFIGURATION_FILE_NAME}.js`;
const DEFAULT_CONFIGURATION_FILE_TS = `${DEFAULT_CONFIGURATION_FILE_NAME}.ts`;
const CLI_ARGS_CONFIG = ['-c', '--config'];

Expand Down Expand Up @@ -48,7 +48,7 @@ if (!fs.existsSync(CONFIGURATION_FILE)) {
process.exit();
}

const configFileContents = loadConfig(path.resolve(CONFIGURATION_FILE));
const configFileContents = await loadConfig(path.resolve(CONFIGURATION_FILE));
const config = getConfigWithDefaults(configFileContents);

export default config;
40 changes: 7 additions & 33 deletions packages/eslint-remote-tester/src/config/load.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,16 @@
import { createRequire } from 'node:module';
import type { Service, RegisterOptions } from 'ts-node';

let registerer: Service | null = null;
const require = createRequire(import.meta.url);

/* istanbul ignore next */
const interopRequireDefault = (obj: any): { default: any } =>
obj && obj.__esModule ? obj : { default: obj };

/** @internal */
export const loadTSConfig = (configPath: string) => {
try {
registerer ||= require('ts-node').register({
moduleTypes: { '**/*.ts': 'cjs' },
} satisfies RegisterOptions) as Service;
} catch (e: any) {
if (e.code === 'MODULE_NOT_FOUND') {
throw new Error(
`'ts-node' is required for TypeScript configuration files. Make sure it is installed\nError: ${e.message}`
);
}

throw e;
}

registerer.enabled(true);

const configObject = interopRequireDefault(require(configPath)).default;

registerer.enabled(false);
export const loadTSConfig = async (configPath: string) => {
const importx = await import('importx');
const config = await importx.import(configPath, import.meta.url);

return configObject;
return config.default;
};

export const loadConfig = (configPath: string) => {
export const loadConfig = async (configPath: string) => {
if (configPath.endsWith('.ts')) {
return loadTSConfig(configPath);
}

return require(configPath);
const { default: config } = await import(configPath);
return config;
};
11 changes: 7 additions & 4 deletions packages/eslint-remote-tester/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ export interface Config {
* Supports lazy initialization based on currently tested repository when a function is passed.
* Function is called with current repository and its location on filesystem.
*/
eslintrc:
| Linter.Config
eslintConfig:
| Linter.FlatConfig
| ((options?: {
repository: string;
location: string;
}) => Linter.Config);
}) => Linter.FlatConfig | Promise<Linter.FlatConfig>);

/** Flag used to set CI mode. `process.env.CI` is used when not set. */
CI: boolean;
Expand Down Expand Up @@ -102,7 +102,10 @@ export interface Config {
) => Promise<void> | void;
}

type RequiredFields = Pick<Config, 'repositories' | 'extensions' | 'eslintrc'>;
type RequiredFields = Pick<
Config,
'repositories' | 'extensions' | 'eslintConfig'
>;
type OptionalFields = AllKeysOptional<
Pick<
Config,
Expand Down
29 changes: 17 additions & 12 deletions packages/eslint-remote-tester/src/config/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export default async function validate(
rulesUnderTesting,
resultParser,
concurrentTasks,
eslintrc,
eslintConfig,
CI,
logLevel,
slowLintTimeLimit,
Expand All @@ -127,32 +127,37 @@ export default async function validate(

errors.push(validateStringArray('extensions', extensions));

if (!eslintrc) {
errors.push(`Missing eslintrc.`);
if (!eslintConfig) {
errors.push(`Missing eslintConfig.`);
} else {
try {
// This will throw when eslintrc is invalid
// This will throw when eslintConfig is invalid
const linter = new ESLint({
useEslintrc: false,
// @ts-expect-error -- `@types/eslint` for v9 are unavailable
overrideConfigFile: true,

// @ts-expect-error -- `@types/eslint` for v9 are unavailable
overrideConfig:
typeof eslintrc === 'function' ? eslintrc() : eslintrc,
typeof eslintConfig === 'function'
? await eslintConfig()
: eslintConfig,
});

errors.push(await validateEslintRules(linter));
} catch (e) {
errors.push(`eslintrc: ${e.message}`);
errors.push(`eslintConfig: ${e.message}`);

if (typeof eslintrc === 'function') {
if (typeof eslintConfig === 'function') {
errors.push(
'Note that "config.eslintrc" is called with empty options during configuration validation.'
'Note that "config.eslintConfig" is called with empty options during configuration validation.'
);
}
}
}

// Optional fields

// TODO nice-to-have: Validate rules match eslintrc config
// TODO nice-to-have: Validate rules match eslintConfig config
// https://eslint.org/docs/developer-guide/nodejs-api#lintergetrules
if (rulesUnderTesting) {
if (Array.isArray(rulesUnderTesting)) {
Expand Down Expand Up @@ -294,7 +299,7 @@ export function getConfigWithDefaults(config: ConfigWithOptionals): Config {
}

/**
* Validate given rules of `config.eslintrc.rules`
* Validate given rules of `config.eslintConfig.rules`
* - When unknown rules are defined, or known ones are misspelled they are not
* reported during linting. We need to specifically look for them.
*/
Expand All @@ -313,7 +318,7 @@ async function validateEslintRules(
}

if (errors.length) {
return `Configuration validation errors at eslintrc.rules: \n - ${errors.join(
return `Configuration validation errors at eslintConfig.rules: \n - ${errors.join(
'\n - '
)}`;
}
Expand Down
18 changes: 11 additions & 7 deletions packages/eslint-remote-tester/src/engine/worker-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,25 +211,29 @@ export default async function workerTask(): Promise<void> {
onReadFailure: () => postMessage({ type: 'READ_FAILURE' }),
});

const eslintrc =
typeof config.eslintrc === 'function'
? config.eslintrc({
const eslintConfig =
typeof config.eslintConfig === 'function'
? await config.eslintConfig({
repository,
location: resolve(`${CACHE_LOCATION}/${repository}`),
})
: config.eslintrc;
: config.eslintConfig;

const linter = new ESLint({
useEslintrc: false,
overrideConfig: eslintrc,
// @ts-expect-error -- `@types/eslint` for v9 are unavailable
overrideConfigFile: true,

// @ts-expect-error -- `@types/eslint` for v9 are unavailable
overrideConfig: eslintConfig,

// Only rules set in configuration are expected.
// Ignore all inline configurations found from target repositories.
allowInlineConfig: false,

// Lint all given files, ignore none. Cache is located under node_modules.
// config.pathIgnorePattern is used for exclusions.
ignore: false,
ignore: true,
ignorePatterns: ['!**/node_modules/'],
});

postMessage({ type: 'LINT_START', payload: files.length });
Expand Down
20 changes: 0 additions & 20 deletions packages/eslint-remote-tester/test/integration/base.config.cjs

This file was deleted.

26 changes: 26 additions & 0 deletions packages/eslint-remote-tester/test/integration/base.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @type {import('../../src/config/types').Config} */
const config = {
repositories: ['AriPerkkio/eslint-remote-tester-integration-test-target'],
extensions: ['.js'],
pathIgnorePattern: '(expected-to-be-excluded)',
rulesUnderTesting: [
'no-unreachable',
'no-undef',
'no-empty',
'getter-return',
'no-compare-neg-zero',
],
eslintConfig: `async function initialize() {
const { default: js } = await import('@eslint/js');
const { FlatCompat } = await import('@eslint/eslintrc');
const compat = new FlatCompat({ baseDirectory: process.cwd() });
return [
js.configs.recommended,
...compat.plugins('eslint-plugin-local-rules'),
{ rules: { 'local-rules/some-unstable-rule': 'error' } },
];
}`,
};

export default config;
Loading

0 comments on commit 492b253

Please sign in to comment.