@jenssimon/eslint-config-sfcc
v6.7.2
Published
A collection of shareable ESLint configurations for Salesforce Commerce Cloud (SFCC)
Downloads
3,650
Maintainers
Readme
@jenssimon/eslint-config-sfcc
Shareable ESLint flat config for Salesforce Commerce Cloud (SFCC) projects.
Key Features Checked (Allow/Block)
Allowed:
- ES5 syntax and common patterns that are guaranteed to work on SFCC/Rhino
- Selected ES2015+ features that are proven to work on SFCC (e.g.
String.raw, some Array methods)
Blocked:
- Modern language features not supported on SFCC/Rhino (e.g. optional chaining, nullish coalescing, async/await, object spread, many ES2015+ builtins)
- Top-level
await, dynamicimport(), class fields, new builtins likeMap,Set,Promise,Symbol, etc. - JSX/E4X-like tag syntax (e.g.
<a/>) that may be misparsed in JavaScript linting workflows - Features that would cause runtime or syntax errors on SFCC
- Many ES2015+ Array/String/Object methods missing in Rhino
- ECMAScript modules (
import/export), as SFCC only supports CommonJS - Common pitfalls like duplicate
constdeclarations in blocks (Rhino scoping)
See the integration tests for concrete examples.
Recommended Config
Install
pnpm add -D eslint @jenssimon/eslint-config-sfccUse in eslint.config.js
import { defineConfig } from "eslint/config"
import sfcc from "@jenssimon/eslint-config-sfcc"
export default defineConfig(
// ...
sfcc.configs.recommended,
)By default, JavaScript files under cartridges/ are linted. Client-side and static asset folders are excluded.
Customize with helper
import { defineConfig } from "eslint/config"
import { createRecommendedConfig } from "@jenssimon/eslint-config-sfcc"
export default defineConfig(
createRecommendedConfig({
cartridgesDir: "cartridges/",
sfcc: {
checkCartridgeExists: true,
allowBareModules: ["server", "proxyquire"],
cartridgePath: ["app_storefront", "modules", "app_custom"],
},
}),
)Built-in Plugins
This package ships two built-in ESLint plugins, both automatically registered in the recommended config:
sfccfor general SFCC/Rhino compatibility rulessitegenesisfor the SiteGenesis-specific controller rule ported fromeslint-plugin-sitegenesis
sitegenesis
sitegenesis now only contains sitegenesis/no-global-require.
That rule stays enabled in the recommended config by default, because it is still useful protection for repositories that contain SiteGenesis-style controller code. In non-SiteGenesis projects it is effectively dormant, because it only applies to files under cartridge/controllers/.
| Rule | Description | Default |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| sitegenesis/no-global-require | Disallows top-level require() calls in controller files when not every route function uses them. Only applies to files under cartridge/controllers/. | error |
sfcc
The new sfcc plugin contains the general Rhino/SFCC runtime rules:
| Rule | Description | Default |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| sfcc/no-ds-files | Disallows legacy .ds files in SFCC projects. Use .js files instead. | error |
| sfcc/no-e4x-syntax | Disallows JSX/E4X-like tag syntax (e.g. <a/>) in SFCC JavaScript to avoid parser ambiguity and unsupported runtime patterns. | error |
| sfcc/no-type-annotations | Disallows type annotation syntax in JavaScript files (e.g. const x: string = ..., function y(): number {}). Rhino/E4X may accept it, but it is invalid in standard JavaScript; use JSDoc typing instead. | error |
| sfcc/no-rhino-import-globals | Disallows legacy Rhino globals importScript(...), importPackage(...), and importClass(...). Use CommonJS require() instead. | error |
| sfcc/prefer-const | Requires const for let declarations that are never reassigned, excluding Rhino-sensitive nested/loop contexts. | error |
| sfcc/rhino-const-compat | Enforces let instead of const in Rhino loop-critical contexts (loop headers and declarations inside loop bodies) and supports auto-fix. | error |
| sfcc/rhino-const-conflict | Detects same-name const declarations in nested blocks within the same function (Rhino treats them as function-scoped) and supports auto-fix to let. | error |
| sfcc/valid-require-path | Validates SFCC-compatible require() paths (dw/*, cartridgeName/*, ./*, ../*, */*, ~/*) and supports optional filesystem existence checks. | error |
The recommended config intentionally combines these sfcc/* rules so --fix does not bounce between conflicting suggestions: Rhino-unsafe const becomes let, while genuinely safe top-level function bindings still become const.
Shared sfcc options
By default, sfcc/valid-require-path validates path patterns only and allows bare server requires.
Use createRecommendedConfig({ sfcc: ... }) to define shared SFCC plugin options centrally. These values are exposed through ESLint settings.sfcc, so future sfcc/* rules can reuse them without adding per-rule options.
import { defineConfig } from "eslint/config"
import { createRecommendedConfig } from "@jenssimon/eslint-config-sfcc"
export default defineConfig(
createRecommendedConfig({
cartridgesDir: "cartridges",
sfcc: {
// Optional: allow additional bare module ids
allowBareModules: ["server", "proxyquire"],
// Optional: verify cartridgeName/* plus */* and ~/* against filesystem
checkCartridgeExists: true,
// Optional: explicit cartridge order for */* lookup (otherwise folders in cartridgesDir are used)
cartridgePath: ["app_storefront", "modules", "app_custom"],
// Optional: path to site template directory
siteTemplatePath: "sites/site_template",
// Optional: site id under <siteTemplatePath>/sites/<site>/site.xml
site: "example",
},
}),
)Rhino const strategy example
Example:
function route() {
let topLevel = 1 // sfcc/prefer-const -> const
for (let i = 0; i < 3; i += 1) {
const loopValue = i * 2 // sfcc/rhino-const-compat -> let
process(loopValue)
}
if (flagA) {
const temp = 1 // with another nested const temp below: sfcc/rhino-const-conflict -> let
process(temp)
}
if (flagB) {
const temp = 2 // sfcc/rhino-const-conflict -> let
process(temp)
}
return topLevel
}Direct plugin usage
import { defineConfig } from "eslint/config"
import eslintConfigSfcc, { sfcc as sfccPlugin, sitegenesis } from "@jenssimon/eslint-config-sfcc"
export default defineConfig(eslintConfigSfcc.configs.recommended, {
plugins: {
sfcc: sfccPlugin,
sitegenesis,
},
rules: {
"sfcc/prefer-const": "error",
"sitegenesis/no-global-require": "error",
},
})Decision matrix: const vs let
- Function top-level (
function route() { ... }) and never reassigned: useconst(sfcc/prefer-const) - Loop header (
for (const x of xs),for (const k in obj),for (const i = 0; ...)): uselet(sfcc/rhino-const-compat) - Declaration inside a loop body: use
let(sfcc/rhino-const-compat) - Nested block with unique name in same function:
constis allowed - Nested block with same
constname reused in sibling/other nested blocks of same function: uselet(sfcc/rhino-const-conflict)
Mini-FAQ
Q: Is this safe?
if (foo === "bar") {
const value = 1
}A: Yes. A single nested-block const with a unique name in that function is allowed.
Q: What about this?
if (foo === "bar") {
const test = 1
}
if (foo === "baz") {
const test = 2
}A: Not safe for Rhino. Both declarations are treated as function-scoped const bindings with the same name. sfcc/rhino-const-conflict reports this and auto-fixes to let.
Q: Are XML and XMLList identifiers allowed?
A: Yes. Constructor-style usage such as const xmlCtor = XML and const xmlListCtor = XMLList is allowed. sfcc/no-e4x-syntax only targets JSX/E4X-like tag syntax (for example <a/>).
Q: Are type annotations allowed in .js files?
A: No. sfcc/no-type-annotations reports annotation syntax in JavaScript files (for example const x: string = "foo" or function y(): number {}). Rhino/E4X may accept this syntax, but .js here follows standard JavaScript where it is invalid. Use JSDoc types instead.
Q: Are legacy Rhino import globals allowed?
A: No. sfcc/no-rhino-import-globals reports importScript(...), importPackage(...), and importClass(...) and points you to CommonJS require() instead.
Q: Are .ds files still allowed?
A: No. sfcc/no-ds-files reports .ds files and enforces .js files instead.
Q: What suggestion is shown for multiline static markup?
A: For static multiline JSX/E4X-like markup, sfcc/no-e4x-syntax suggests converting to XML(\...`). For dynamic markup (for example with {value}`), no conversion suggestion is offered.
Q: Does sfcc/no-e4x-syntax report default xml namespace = "..."?
A: No. That construct fails during parsing before rules run, so ESLint reports a fatal parsing error first. The rule cannot execute on code that does not parse.
Q: Is for each (x in y) allowed?
A: No. for each is Rhino/E4X-era syntax and not valid modern JavaScript, so ESLint fails with a parsing error before rules run. Treat it as unsupported project syntax and migrate to standard constructs such as for (x of y).
Migration recipes (Rhino/E4X -> modern JS)
Use these patterns when modernizing legacy SFCC code.
- Iterate values (
for each->for...of)
Before:
for each (item in items) {
process(item)
}After:
for (const item of items) {
process(item)
}- Iterate object keys and values (legacy
for eachon objects -> explicit key/value handling)
Before:
for each (value in obj) {
process(value)
}After:
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key]
process(value)
}
}- Replace E4X literal markup with explicit XML construction
Before:
const payload = (
<request>
<id>{id}</id>
</request>
)After:
const payload = XML(`<request><id>${id}</id></request>`)Notes:
default xml namespace = "..."is also parser-incompatible in modern JS/ESLint and must be refactored manually.
Migrating from v4
This is a major release with breaking changes.
What changed
ESLint Flat Config
The package now uses the flat config format (eslint.config.js). The legacy .eslintrc-based format is no longer supported.
Focus: compatibility, not formatting The config no longer enforces any code style or formatting rules. Its sole purpose is to detect JavaScript features that are not supported on SFCC sandboxes (Rhino engine). Formatting should be handled separately, e.g. with Prettier or Oxfmt.
No more base config
The previous version extended @jenssimon/eslint-config-base (Airbnb style guide). This dependency has been removed entirely. Rules like comma-dangle, no-var, import/*, consistent-return, etc. are no longer part of this config.
eslint-plugin-es5 → eslint-plugin-es
The old eslint-plugin-es5 has been replaced by eslint-plugin-es. Rules have been mapped accordingly.
No more SiteGenesis / SFRA configs
The sfra and sfra-storefront configurations have been removed. These configurations were specific to SFRA and SiteGenesis and are not part of this general-purpose SFCC config. The external eslint-plugin-sitegenesis dependency is no longer used — sitegenesis/no-global-require is now built in, and the Rhino-specific general rules live in the built-in sfcc plugin.
Migration steps
- Replace
.eslintrc.*witheslint.config.js - Update the package name and import (see Usage above)
- Remove
@jenssimon/eslint-config-base,eslint-plugin-es5, andeslint-plugin-sitegenesisfrom your dependencies —sitegenesis/no-global-requireis built in and the general Rhino rules are nowsfcc/* - Add any formatting rules you need directly to your own
eslint.config.js
Development
vp install
vp test
vp check
vp pack