Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
416 changes: 389 additions & 27 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/code-analyzer-eslint-engine/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@salesforce/code-analyzer-eslint-engine",
"description": "Plugin package that adds 'eslint' as an engine into Salesforce Code Analyzer",
"version": "0.38.0",
"version": "0.39.0-SNAPSHOT",
"author": "The Salesforce Code Analyzer Team",
"license": "BSD-3-Clause",
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
Expand All @@ -13,6 +13,7 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@babel/preset-react": "^7.27.1",
"@eslint/js": "^9.39.2",
"@lwc/eslint-plugin-lwc": "^3.3.0",
"@lwc/eslint-plugin-lwc-platform": "^6.3.0",
Expand All @@ -27,6 +28,7 @@
"eslint": "^9.39.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.5.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^16.5.0",
"semver": "^7.7.3",
"typescript": "^5.9.3",
Expand Down
83 changes: 75 additions & 8 deletions packages/code-analyzer-eslint-engine/src/base-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import eslintTs from "typescript-eslint";
import lwcEslintPluginLwcPlatform from "@lwc/eslint-plugin-lwc-platform";
import salesforceEslintConfigLwc from "@salesforce/eslint-config-lwc";
import sldsEslintPlugin from "@salesforce-ux/eslint-plugin-slds";
import eslintPluginReact from "eslint-plugin-react";
import {ESLintEngineConfig} from "./config";
import globals from "globals";

Expand Down Expand Up @@ -54,19 +55,37 @@ export class BaseConfigFactory {
if (this.useTsBaseConfig()) {
configArray.push(...this.createTypescriptConfigArray());
}
// Add React plugin config for JSX files
if (this.useReactBaseConfig()) {
configArray.push(...this.createReactConfigArray());
}
return configArray;
}

private createJavascriptPlusLwcConfigArray(): Linter.Config[] {
let configs: Linter.Config[] = validateAndGetRawLwcConfigArray();

// TODO: Remove the For the following 2 updates when https://github.com/salesforce/eslint-config-lwc/issues/158 is fixed
// 1) Turn off the babel parser's configFile option from the lwc base plugin
(configs[0].languageOptions!.parserOptions as Linter.ParserOptions).babelOptions.configFile = false;
// 2) For some reason babel doesn't like .cjs files unless we explicitly set this to undefined because I think
// ESLint 9 is setting it to "commonjs" automatically when the field doesn't exist in the parserOptions (and for
// babel "commonjs" isn't a valid option)
(configs[0].languageOptions!.parserOptions as Linter.ParserOptions).sourceType = undefined;
// Reconstruct languageOptions to avoid mutating the original shared config from the LWC package
// TODO: Remove configFile and sourceType overrides when https://github.com/salesforce/eslint-config-lwc/issues/158 is fixed
const originalParserOptions = configs[0].languageOptions!.parserOptions as Linter.ParserOptions;
const originalBabelOptions = originalParserOptions.babelOptions || {};
configs[0].languageOptions = {
...configs[0].languageOptions,
parserOptions: {
...originalParserOptions,
// For some reason babel doesn't like .cjs files unless we explicitly set this to undefined
// because ESLint 9 is setting it to "commonjs" automatically when the field doesn't exist
// in the parserOptions (and for babel "commonjs" isn't a valid option)
sourceType: undefined,
babelOptions: {
...originalBabelOptions,
// Turn off the babel parser's configFile option from the lwc base plugin
configFile: false,
// Add @babel/preset-react to enable JSX parsing for React/JSX files
presets: [...(originalBabelOptions.presets || []), '@babel/preset-react']
}
}
};

// Swap out eslintJs.configs.recommended with eslintJs.configs.all
configs[1] = eslintJs.configs.all;
Expand Down Expand Up @@ -115,7 +134,14 @@ export class BaseConfigFactory {
private createJavascriptConfigArray(): Linter.Config[] {
return [{
... eslintJs.configs.all,
files: this.engineConfig.file_extensions.javascript.map(ext => `**/*${ext}`)
files: this.engineConfig.file_extensions.javascript.map(ext => `**/*${ext}`),
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true // Enable JSX parsing for React/JSX files
}
}
}
}];
}

Expand Down Expand Up @@ -160,6 +186,40 @@ export class BaseConfigFactory {
return configs;
}

/**
* Creates React plugin config for JavaScript files.
*
* React rules are applied to all JS files (.js, .jsx, .cjs, .mjs) - if a file
* doesn't contain React code, the rules simply won't report any violations.
*
* Note: TypeScript React support (.tsx) is planned for the next iteration.
*/
private createReactConfigArray(): Linter.Config[] {
// Apply React rules to all JavaScript files
const jsExtensions = this.engineConfig.file_extensions.javascript;

if (jsExtensions.length === 0) {
return [];
}

// Get all rules from eslint-plugin-react's flat config
const reactAllConfig = eslintPluginReact.configs.flat.all;

return [{
...reactAllConfig,
files: jsExtensions.map(ext => `**/*${ext}`),
settings: {
...reactAllConfig.settings,
react: {
// React version - "detect" automatically picks the installed version, falls back to latest
version: 'detect',
// Pragma is the function JSX compiles to (e.g., <div> → React.createElement('div'))
pragma: 'React'
}
}
}];
}

private useJsBaseConfig(): boolean {
return !this.engineConfig.disable_javascript_base_config && this.engineConfig.file_extensions.javascript.length > 0;
}
Expand All @@ -179,6 +239,13 @@ export class BaseConfigFactory {
private useTsBaseConfig(): boolean {
return !this.engineConfig.disable_typescript_base_config && this.engineConfig.file_extensions.typescript.length > 0;
}

private useReactBaseConfig(): boolean {
// React config is independently controlled by disable_react_base_config
// React rules apply to all JS files - no harm if file has no React code
return !this.engineConfig.disable_react_base_config &&
this.engineConfig.file_extensions.javascript.length > 0;
}
}

// In order to supply all the eslint rules (instead of just the recommended ones) to be selectable, and in order to
Expand Down
17 changes: 15 additions & 2 deletions packages/code-analyzer-eslint-engine/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export type ESLintEngineConfig = {
// Default: false
disable_typescript_base_config: boolean

// If true then the base configuration that supplies the React/JSX rules will not be applied.
// Default: true (React support is currently gated; will change to false when released)
disable_react_base_config: boolean

// Extensions of the files in your workspace that will be used to discover rules.
// To associate file extensions to the standard ESLint JavaScript rules, LWC rules, or custom JavaScript-based
// rules, add them under the 'javascript' language. To associate file extensions to the standard TypeScript
Expand Down Expand Up @@ -66,9 +70,10 @@ export const DEFAULT_CONFIG: ESLintEngineConfig = {
disable_lwc_base_config: false,
disable_slds_base_config: false,
disable_typescript_base_config: false,
disable_react_base_config: true, // Gated for now - will change to false when released
file_extensions: {
javascript: ['.js', '.cjs', '.mjs'],
typescript: ['.ts'],
javascript: ['.js', '.cjs', '.mjs', '.jsx'],
typescript: ['.ts'], // Note: .tsx support planned for next iteration
html: ['.html', '.htm', '.cmp'],
css: ['.css', '.scss'],
other: []
Expand Down Expand Up @@ -114,6 +119,8 @@ export const ESLINT_ENGINE_CONFIG_DESCRIPTION: ConfigDescription = {
valueType: "boolean",
defaultValue: DEFAULT_CONFIG.disable_typescript_base_config
},
// Note: disable_react_base_config is gated and not user-configurable yet
// TODO: Add to fieldDescriptions when React support is released
file_extensions: {
descriptionText: getMessage('ConfigFieldDescription_file_extensions'),
valueType: "object",
Expand All @@ -136,6 +143,9 @@ export const LEGACY_ESLINT_IGNORE_FILE: string = '.eslintignore';


export function validateAndNormalizeConfig(configValueExtractor: ConfigValueExtractor): ESLintEngineConfig {
// disable_react_base_config bypasses validation - React support is gated but we need it for internal testing
// TODO: Move 'disable_react_base_config' to validateContainsOnlySpecifiedKeys when React support is released
configValueExtractor.addKeysThatBypassValidation(['disable_react_base_config']);
configValueExtractor.validateContainsOnlySpecifiedKeys(['eslint_config_file', 'eslint_ignore_file',
'auto_discover_eslint_config', 'disable_javascript_base_config', 'disable_lwc_base_config',
'disable_slds_base_config', 'disable_typescript_base_config', 'file_extensions']);
Expand All @@ -150,6 +160,9 @@ export function validateAndNormalizeConfig(configValueExtractor: ConfigValueExtr
disable_lwc_base_config: eslintConfigValueExtractor.extractBooleanValue('disable_lwc_base_config'),
disable_slds_base_config: eslintConfigValueExtractor.extractBooleanValue('disable_slds_base_config'),
disable_typescript_base_config: eslintConfigValueExtractor.extractBooleanValue('disable_typescript_base_config'),
// React support is gated - always force to true regardless of customer config
// TODO: Change to eslintConfigValueExtractor.extractBooleanValue('disable_react_base_config') when released
disable_react_base_config: true,
file_extensions: eslintConfigValueExtractor.extractFileExtensionsValue(),
};
}
Expand Down
5 changes: 5 additions & 0 deletions packages/code-analyzer-eslint-engine/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const MESSAGE_CATALOG : { [key: string]: string } = {
`The base configuration for TypeScript files adds the rules from the "plugin:@typescript-eslint:all" configuration to Code Analyzer.\n` +
`See https://typescript-eslint.io/rules and https://eslint.org/docs/latest/rules for the lists of rules.`,

ConfigFieldDescription_disable_react_base_config:
`Whether to turn off the default base configuration that supplies the React/JSX rules for .jsx and .tsx files\n` +
`The base configuration for React adds the rules from the "eslint-plugin-react" configuration to Code Analyzer.\n` +
`See https://www.npmjs.com/package/eslint-plugin-react for the list of rules.`,

ConfigFieldDescription_file_extensions:
`Extensions of the files in your workspace that will be used to discover rules.\n` +
`To associate file extensions to the standard ESLint JavaScript rules, LWC rules, or custom JavaScript-based\n` +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,24 @@ declare module '@salesforce-ux/eslint-plugin-slds' {
};
export = plugin;
}

// This declaration adds in the missing types for "eslint-plugin-react"
declare module 'eslint-plugin-react' {
import type { ESLint, Linter } from 'eslint';
import type { RuleDefinition } from "@eslint/core";

const plugin: ESLint.Plugin & {
readonly rules: Record<string, RuleDefinition>;
readonly configs: {
readonly recommended: Linter.Config;
readonly all: Linter.Config;
readonly "jsx-runtime": Linter.Config;
readonly flat: {
readonly recommended: Linter.Config;
readonly all: Linter.Config;
readonly "jsx-runtime": Linter.Config;
};
};
};
export = plugin;
}
Loading