diff --git a/README.md b/README.md index e321a26..d2bed50 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ To choose from three configuration settings, install the [`eslint-config-lwc`](h | [lwc/consistent-component-name](./docs/rules/consistent-component-name.md) | ensure component class name matches file name | 🔧 | | [lwc/no-api-reassignments](./docs/rules/no-api-reassignments.md) | prevent public property reassignments | | | [lwc/no-deprecated](./docs/rules/no-deprecated.md) | disallow usage of deprecated LWC APIs | | +| [lwc/newer-version-available](./docs/rules/newer-version-available.md) | suggest newer versions of module imports when available | ✓ | | [lwc/no-document-query](./docs/rules/no-document-query.md) | disallow DOM query at the document level | | | [lwc/no-attributes-during-construction](./docs/rules/no-attributes-during-construction.md) | disallow setting attributes during construction | | | [lwc/no-disallowed-lwc-imports](./docs/rules/no-disallowed-lwc-imports.md) | disallow importing unsupported APIs from the `lwc` package | | diff --git a/docs/rules/newer-version-available.md b/docs/rules/newer-version-available.md new file mode 100644 index 0000000..a67ad57 --- /dev/null +++ b/docs/rules/newer-version-available.md @@ -0,0 +1,64 @@ +# Suggest newer versions of module imports when available (`lwc/newer-version-available`) + +This rule suggests using newer versions of module imports when they are available. + +## Rule Details + +This rule checks for imports from modules that have newer versions available and provides automatic fixes to update them to their recommended replacements. + +Modules with newer versions available: + +- `lightning/uiGraphQLApi` → Use `lightning/graphql` instead + - **Note:** `refreshGraphQL` is not available in `lightning/graphql`. Use `refresh` from the wire adapter result instead + +Examples of **incorrect** code: + +```javascript +import { gql, graphql } from 'lightning/uiGraphQLApi'; +import * as graphqlApi from 'lightning/uiGraphQLApi'; +import { refreshGraphQL } from 'lightning/uiGraphQLApi'; // Will not auto-fix +``` + +Examples of **correct** code: + +```javascript +import { gql, graphql } from 'lightning/graphql'; +import * as graphqlApi from 'lightning/graphql'; +``` + +## Options + +You can configure additional modules with newer versions through the rule options: + +```json +{ + "@lwc/lwc/newer-version-available": [ + "error", + { + "modulesWithNewerVersions": { + "old/module/path": { + "replacement": "new/module/path", + "message": "A newer version is available: use new/module/path instead." + } + } + } + ] +} +``` + +### Options Schema + +- `modulesWithNewerVersions` (object): An object mapping module paths to their newer versions + - Each entry should have: + - `replacement` (string): The newer module path to use instead + - `message` (string): The error message to display + +## When Not To Use It + +If your codebase has legitimate use cases for older module versions (such as Mobile-Offline functionality for `lightning/uiGraphQLApi`), you may need to disable this rule for specific files or directories. See the (Wire Adapter Comparison)[https://developer.salesforce.com/docs/platform/lwc/guide/reference-graphql-intro.html#graphql-api-wire-adapter-comparison] for more details. + +## Auto-fix + +This rule provides automatic fixes that will update module imports to their newer versions. Use `--fix` with ESLint to automatically update your imports. + +**Important:** Auto-fix is disabled when importing APIs that don't exist in the newer version (such as `refreshGraphQL`). You'll need to manually update these imports and refactor your code to use the appropriate alternatives. diff --git a/lib/index.js b/lib/index.js index 8952da3..6b3f716 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,6 +24,7 @@ const rules = { 'no-disallowed-lwc-imports': require('./rules/no-disallowed-lwc-imports'), 'no-template-children': require('./rules/no-template-children'), 'no-unexpected-wire-adapter-usages': require('./rules/no-unexpected-wire-adapter-usages'), + 'newer-version-available': require('./rules/newer-version-available'), 'no-rest-parameter': require('./rules/no-rest-parameter'), 'no-unknown-wire-adapters': require('./rules/no-unknown-wire-adapters'), 'prefer-custom-event': require('./rules/prefer-custom-event'), diff --git a/lib/rules/newer-version-available.js b/lib/rules/newer-version-available.js new file mode 100644 index 0000000..9bc0303 --- /dev/null +++ b/lib/rules/newer-version-available.js @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +'use strict'; + +const { docUrl } = require('../util/doc-url'); + +// Map of modules with newer versions available to their replacements and messages +const MODULES_WITH_NEWER_VERSIONS = { + 'lightning/uiGraphQLApi': { + replacement: 'lightning/graphql', + message: + 'A newer version is available: use "lightning/graphql" instead of "lightning/uiGraphQLApi" for non Mobile-Offline use cases.', + removedExports: ['refreshGraphQL'], // APIs that don't exist in the replacement + }, + // Add more modules with newer versions here in the future as needed + // 'old/module': { + // replacement: 'new/module', + // message: 'A newer version is available: use new/module instead.', + // removedExports: [], // Optional: list of exports that don't exist in replacement + // }, +}; + +module.exports = { + meta: { + docs: { + description: 'suggest newer versions of module imports when available', + category: 'LWC', + recommended: true, + url: docUrl('newer-version-available'), + }, + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + // Allow customization of modules with newer versions + modulesWithNewerVersions: { + type: 'object', + additionalProperties: { + type: 'object', + properties: { + replacement: { + type: 'string', + }, + message: { + type: 'string', + }, + }, + required: ['replacement', 'message'], + }, + }, + }, + additionalProperties: false, + }, + ], + }, + + create(context) { + const options = context.options[0] || {}; + + // Merge default modules with newer versions with any custom ones from options + const modulesWithNewerVersions = { + ...MODULES_WITH_NEWER_VERSIONS, + ...(options.modulesWithNewerVersions || {}), + }; + + return { + ImportDeclaration(node) { + // Check if this import is from a module with a newer version available + if (node.source && node.source.type === 'Literal') { + const importPath = node.source.value; + const moduleWithNewerVersion = modulesWithNewerVersions[importPath]; + + if (moduleWithNewerVersion) { + // Check if any imported specifiers are removed in the replacement + const hasRemovedExports = node.specifiers.some((specifier) => { + if ( + specifier.type === 'ImportSpecifier' && + moduleWithNewerVersion.removedExports + ) { + return moduleWithNewerVersion.removedExports.includes( + specifier.imported.name, + ); + } + return false; + }); + + // Report the module with newer version available + context.report({ + node, + message: moduleWithNewerVersion.message, + fix(fixer) { + // Don't auto-fix if there are removed exports + if (hasRemovedExports) { + return null; + } + // Provide auto-fix if replacement is available + if (moduleWithNewerVersion.replacement) { + return fixer.replaceText( + node.source, + `'${moduleWithNewerVersion.replacement}'`, + ); + } + }, + }); + + // Report additional warnings for removed exports + if (moduleWithNewerVersion.removedExports) { + node.specifiers.forEach((specifier) => { + if ( + specifier.type === 'ImportSpecifier' && + moduleWithNewerVersion.removedExports.includes( + specifier.imported.name, + ) + ) { + context.report({ + node: specifier, + message: `"${specifier.imported.name}" is not available in ${moduleWithNewerVersion.replacement}. ${ + specifier.imported.name === 'refreshGraphQL' + ? 'Use refresh from the wire adapter result instead.' + : 'This API has been removed.' + }`, + }); + } + }); + } + } + } + }, + }; + }, +}; diff --git a/test/lib/rules/newer-version-available.js b/test/lib/rules/newer-version-available.js new file mode 100644 index 0000000..2460755 --- /dev/null +++ b/test/lib/rules/newer-version-available.js @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +'use strict'; + +const { testRule, testTypeScript } = require('../shared'); + +testRule('newer-version-available', { + valid: [ + { + code: `import { gql, graphql } from 'lightning/graphql';`, + }, + { + code: `import { LightningElement } from 'lwc';`, + }, + { + code: `import { wire } from 'lwc';`, + }, + { + code: `const module = 'lightning/uiGraphQLApi'; // just a string, not an import`, + }, + { + code: `// This is allowed with empty modules with newer versions list + import something from 'some/module';`, + options: [{ modulesWithNewerVersions: {} }], + }, + ], + invalid: [ + { + code: `import { gql, graphql } from 'lightning/uiGraphQLApi';`, + errors: [ + { + message: + 'A newer version is available: use "lightning/graphql" instead of "lightning/uiGraphQLApi" for non Mobile-Offline use cases.', + }, + ], + output: `import { gql, graphql } from 'lightning/graphql';`, + }, + { + code: `import { gql } from 'lightning/uiGraphQLApi';`, + errors: [ + { + message: + 'A newer version is available: use "lightning/graphql" instead of "lightning/uiGraphQLApi" for non Mobile-Offline use cases.', + }, + ], + output: `import { gql } from 'lightning/graphql';`, + }, + { + code: `import { graphql } from 'lightning/uiGraphQLApi';`, + errors: [ + { + message: + 'A newer version is available: use "lightning/graphql" instead of "lightning/uiGraphQLApi" for non Mobile-Offline use cases.', + }, + ], + output: `import { graphql } from 'lightning/graphql';`, + }, + { + code: `import graphqlApi from 'lightning/uiGraphQLApi';`, + errors: [ + { + message: + 'A newer version is available: use "lightning/graphql" instead of "lightning/uiGraphQLApi" for non Mobile-Offline use cases.', + }, + ], + output: `import graphqlApi from 'lightning/graphql';`, + }, + { + code: `import * as graphqlApi from 'lightning/uiGraphQLApi';`, + errors: [ + { + message: + 'A newer version is available: use "lightning/graphql" instead of "lightning/uiGraphQLApi" for non Mobile-Offline use cases.', + }, + ], + output: `import * as graphqlApi from 'lightning/graphql';`, + }, + { + // Test refreshGraphQL import - should warn and NOT auto-fix + code: `import { refreshGraphQL } from 'lightning/uiGraphQLApi';`, + errors: [ + { + message: + 'A newer version is available: use "lightning/graphql" instead of "lightning/uiGraphQLApi" for non Mobile-Offline use cases.', + }, + { + message: + '"refreshGraphQL" is not available in lightning/graphql. Use refresh from the wire adapter result instead.', + }, + ], + output: null, // No auto-fix because refreshGraphQL doesn't exist in replacement + }, + { + // Test mixed imports with refreshGraphQL - should warn and NOT auto-fix + code: `import { gql, graphql, refreshGraphQL } from 'lightning/uiGraphQLApi';`, + errors: [ + { + message: + 'A newer version is available: use "lightning/graphql" instead of "lightning/uiGraphQLApi" for non Mobile-Offline use cases.', + }, + { + message: + '"refreshGraphQL" is not available in lightning/graphql. Use refresh from the wire adapter result instead.', + }, + ], + output: null, // No auto-fix because refreshGraphQL doesn't exist in replacement + }, + { + // Test custom modules with newer versions through options + code: `import something from 'old/deprecated/module';`, + options: [ + { + modulesWithNewerVersions: { + 'old/deprecated/module': { + replacement: 'new/modern/module', + message: + 'A newer version is available: use new/modern/module instead of old/deprecated/module.', + }, + }, + }, + ], + errors: [ + { + message: + 'A newer version is available: use new/modern/module instead of old/deprecated/module.', + }, + ], + output: `import something from 'new/modern/module';`, + }, + ], +}); + +testTypeScript('newer-version-available', { + valid: [ + { + code: `import { gql, graphql } from 'lightning/graphql'; + import { LightningElement } from 'lwc'; + + export default class Test extends LightningElement {}`, + }, + ], + invalid: [ + { + code: `import { gql, graphql } from 'lightning/uiGraphQLApi'; + import { LightningElement } from 'lwc'; + + export default class Test extends LightningElement {}`, + errors: [ + { + message: + 'A newer version is available: use "lightning/graphql" instead of "lightning/uiGraphQLApi" for non Mobile-Offline use cases.', + }, + ], + output: `import { gql, graphql } from 'lightning/graphql'; + import { LightningElement } from 'lwc'; + + export default class Test extends LightningElement {}`, + }, + ], +});