@salesforce/eslint-plugin-vscode-extensions
v65.12.2
Published
Custom ESLint rules for Salesforce VSCode extensions
Downloads
65
Readme
@salesforce/eslint-plugin-vscode-extensions
Custom ESLint rules for Salesforce VSCode extensions.
Rules
no-duplicate-i18n-values
Disallows English text in translation files that should be localized. This rule checks i18n locale files (e.g., i18n.ja.ts) and flags any translations that appear to be in English or duplicate the English source text.
no-vscode-message-literals
Enforces that vscode.window.show*Message calls use localized strings via nls.localize() or variables, not string literals.
Bad:
vscode.window.showErrorMessage('An error occurred');
vscode.window.showWarningMessage(`Failed: ${error}`);Good:
vscode.window.showErrorMessage(nls.localize('error_message'));
const msg = nls.localize('error_with_details', error);
vscode.window.showWarningMessage(msg);
vscode.window.showErrorMessage(`${nls.localize('prefix')} - ${details}`);The rule allows template literals that contain nls.localize() calls.
no-inline-esbuild-platform
Enforces that process.env.ESBUILD_PLATFORM is compared inline against a string literal (e.g. === 'web' / !== 'web', including ternary tests). esbuild's define replaces the literal at bundle time so 'web' === 'web' constant-folds and dead branches tree-shake (ADR 0013); assigning it to a variable, object/class property, destructuring it, or comparing against a non-literal (=== someVar) defeats the strip and leaks node-only code into the web bundle.
Bad:
const isWebMode = process.env.ESBUILD_PLATFORM === 'web';
const { ESBUILD_PLATFORM } = process.env;
doThing(process.env.ESBUILD_PLATFORM);
if (process.env.ESBUILD_PLATFORM === someVar) { ... }Good:
if (process.env.ESBUILD_PLATFORM === 'web') { ... }
const reporter = process.env.ESBUILD_PLATFORM === 'web' ? webReporter : nodeReporter;Test files set/delete/save-restore the env var as jest plumbing; that is allowed via an off override in eslint.config.mjs, not the rule.
no-effect-fn-wrapper
Enforces that an arrow function wrapping an Effect.fn call hoists its params into the generator, where they become typed arguments instead of closure captures. The wrapper arrow then disappears.
Bad:
// params on wrapper arrow, generator has none (closure capture)
const findById = (id: UserId) =>
Effect.fn('UserService.findById')(function* () {
yield* repo.findById(id); // id from closure
});Good:
const findById = Effect.fn('UserService.findById')(function* (id: UserId) {
yield* repo.findById(id);
});Note: Immediately-invoked Effect.fn calls (e.g. Effect.fn('x')(function* (){})()) are flagged by the Effect Language Service rule effectFnIife (config-enforced in config/effect-diagnostics.json), not this rule. Use Effect.gen(...).pipe(Effect.withSpan(...)) for one-shot effects.
no-successive-annotate-current-span
Enforces that back-to-back Effect.annotateCurrentSpan calls are merged into a single call. Each call opens and annotates the current span independently, so two or more adjacent calls do redundant work that a single object-argument call expresses more cheaply. The rule detects two forms: consecutive yield* Effect.annotateCurrentSpan(...) statements inside a generator, and consecutive Effect.tap(x => Effect.annotateCurrentSpan(...)) arguments inside one .pipe(...) chain. Any intervening statement or non-annotate .tap breaks the run, so only genuinely successive calls are flagged. Array forms (Effect.all/forEach) and andThen chains are out of scope.
The autofix merges the calls into one Effect.annotateCurrentSpan({ ... }): the (key, value) form becomes key: value (computed [key]: value when the key is not a string literal), and the single-object form is spread (...obj). When the merged calls set the same key more than once the rule reports duplicateKey and the autofix keeps the last value (object-literal later-wins semantics).
Bad:
yield* Effect.annotateCurrentSpan('fileName', fileName);
yield* Effect.annotateCurrentSpan('workspacePath', workspacePath.toString());Good:
yield* Effect.annotateCurrentSpan({ fileName, workspacePath: workspacePath.toString() });package-json-i18n-descriptions
Enforces that user-facing strings in package.json contributes sections use i18n placeholders (%key%) and that those keys exist in the sibling package.nls.json file.
package-json-extension-icon
Validates published VS Code extensions (packages with name starting with salesforcedx-vscode):
- Must have top-level
iconfield - Icon path must exist on disk when specified
package-json-icon-paths
Validates icon paths in package.json contributes sections:
- Icon objects must have both
lightanddarkproperties (or neither) - Referenced icon files must exist on disk
package-json-command-refs
Validates command references in package.json:
- Commands referenced in menus must be defined in
contributes.commands - Warns about orphaned commands (defined but never referenced)
package-json-view-refs
Validates view ID references in package.json:
- View IDs in
whenclauses must match defined views incontributes.views - View IDs in
viewsWelcomemust reference defined views
vscodeignore-required-patterns
Validates .vscodeignore required patterns for web extensions (package.json contains a browser field):
- Enforces a baseline set of required ignore patterns
- Enforces
scripts/**anddocs/**when those directories exist in the package
Note: The package-json-* rules require @eslint/json to be installed and configured.
Usage
Installation
npm install --save-dev @salesforce/eslint-plugin-vscode-extensions @eslint/jsonConfiguration
In your eslint.config.mjs (or eslint.config.js):
import jsonPlugin from '@eslint/json';
import localRulesPlugin from '@salesforce/eslint-plugin-vscode-extensions';
export default [
// Register JSON plugin
{
plugins: {
json: jsonPlugin
}
},
// Enable JSON linting for package.json files
{
files: ['**/package.json'],
language: 'json/json',
plugins: {
json: jsonPlugin,
local: localRulesPlugin
},
rules: {
'local/package-json-i18n-descriptions': 'error',
'local/package-json-extension-icon': 'error',
'local/package-json-icon-paths': 'error',
'local/package-json-command-refs': 'error',
'local/package-json-view-refs': 'error'
}
},
// Enable .vscodeignore linting for extension package folders
{
files: ['packages/*/.vscodeignore'],
plugins: {
local: localRulesPlugin
},
processor: 'local/vscodeignoreText',
rules: {
'local/vscodeignore-required-patterns': 'error'
}
}
];Why @eslint/json is a peerDependency:
The rule itself doesn't import @eslint/json - it only works with AST nodes that ESLint provides. However, @eslint/json is required in your ESLint configuration to:
- Register the JSON language parser
- Enable ESLint to parse JSON files
This is a peerDependency (not a regular dependency) because:
- The rule doesn't directly import it
- Users need to configure it in their ESLint config
- It allows users to control the version they use
- npm will warn if it's missing during installation
This package is also used internally in the Salesforce VSCode Extensions monorepo via eslint.config.mjs.
Writing JSON Rules
When writing custom ESLint rules for JSON files using @eslint/json, be aware that the AST structure differs from JavaScript/TypeScript.
Root Node Type
The @eslint/json plugin uses Document as the root AST node type, not Program. Your rule visitor must use Document:exit (or Document):
// ❌ WRONG - Program is never called for JSON files
'Program:exit': (node) => { ... }
// ✅ CORRECT - Document is the root node for JSON
'Document:exit': (node) => {
const ast = node?.body; // The actual JSON content
...
}AST Structure
The JSON AST uses @humanwhocodes/momoa node types:
Document- Root node, containsbody(the JSON value)Object- JSON object{}Array- JSON array[]String,Number,Boolean,Null- Primitive valuesMember- Key-value pair in an object (hasnameandvalue)Element- Item in an array (hasvalue)
Example traversal:
const findNodeAtPath = (node: ValueNode, pathSegments: string[]): ValueNode[] => {
if (pathSegments.length === 0) return [node];
const [key, ...rest] = pathSegments;
if (node.type === 'Object') {
const member = node.members.find(m => m.name.value === key);
return member ? findNodeAtPath(member.value, rest) : [];
}
if (node.type === 'Array' && key === '*') {
return node.elements.flatMap(el => findNodeAtPath(el.value, rest));
}
return [];
};Testing JSON Rules
JSON rules can be unit tested using ESLint's Linter class with flat config:
import { Linter } from 'eslint';
import * as json from '@eslint/json';
import { myJsonRule } from '../src/myJsonRule';
const linter = new Linter({ configType: 'flat' });
const lintJson = (code: string, filename = 'packages/test/package.json') => {
const config = [
{
files: ['**/*.json'],
plugins: {
// IMPORTANT: include both rules AND languages from @eslint/json
json: { rules: json.rules, languages: json.languages },
local: { rules: { 'my-json-rule': myJsonRule } }
},
language: 'json/json',
rules: { 'local/my-json-rule': 'error' }
}
];
return linter.verify(code, config, { filename });
};
// Use in tests:
const messages = lintJson('{"invalid": "json"}');
expect(messages).toHaveLength(1);