Skip to content

NPM version Downloads

@commerce-klaus/eslint-config-sfcc

Shareable ESLint flat config for Salesforce Commerce Cloud (SFCC) projects.

This package continues @jenssimon/eslint-config-sfcc under the Commerce Klaus organization.

Modern JavaScript where SFCC supports it

This config is not an ES5 style guide. Its goal is to let SFCC projects use the most modern, idiomatic JavaScript that is known to run reliably on the platform, while catching unsupported features before deployment.

Compatibility policy

Write modern JavaScript by default. A feature is restricted only when SFCC/Rhino does not support it reliably or when it conflicts with an SFCC-specific runtime contract.

AreaPolicyExamples
Verified modern JavaScriptUse itconst/let, arrow functions, destructuring, template literals, generators, for...of
Standard library APIsUse APIs verified on supported SFCC versionsArray.from, String.prototype.includes, Object.values, Number.isFinite
Known runtime gapsReport before the code reaches a sandboxclasses, default parameters, spread syntax, Promise, dynamic import(), unsupported builtins
SFCC runtime contractsPrefer platform-compatible patternsCommonJS modules, valid cartridge paths, public Custom API exports, registered hook exports
Rhino-specific behaviorApply targeted compatibility rulessafe const usage, loop declarations, repeated names in nested blocks
Legacy or ambiguous syntaxReject or migrate to standard JavaScript.ds files, E4X-like markup, Rhino import globals, empty(...), Java-style String.equals(...)

ES5 code remains valid, but it is the compatibility floor, not the target style.

Verified modern syntax and APIs
  • Language syntax: const and let, arrow functions, destructuring, template literals, generator functions, exponentiation, and for...of
  • Array: Array.from, Array.of, Array.prototype.find, Array.prototype.findIndex, and Array.prototype.includes
  • String: String.raw, String.fromCodePoint, and the includes, startsWith, endsWith, repeat, padStart, and padEnd prototype methods
  • Object: Object.assign, Object.values, and Object.entries
  • Number: Number.isFinite, Number.isNaN, Number.isSafeInteger, Number.parseInt, and Number.parseFloat

The rule configuration and integration tests define the compatibility contract. Features outside this verified set may still be restricted when the sandbox cannot execute them reliably.

Install

bash
pnpm add -D eslint @commerce-klaus/eslint-config-sfcc
bash
yarn add -D eslint @commerce-klaus/eslint-config-sfcc
bash
npm install -D eslint @commerce-klaus/eslint-config-sfcc

Use in eslint.config.js

eslint.config.js
js
import { defineConfig } from "eslint/config"
import sfcc from "@commerce-klaus/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.

Storefront architecture presets

Storefront presets are policy overlays that describe which controller architecture a project uses. Compose one after recommended:

They do not change the JavaScript compatibility baseline: modern syntax and standard library APIs supported by SFCC remain allowed in every storefront preset. sitegenesis/no-global-require also remains enabled in every preset because the rule limits itself to files under cartridge/controllers/.

eslint.config.js
js
import { defineConfig } from "eslint/config"
import sfcc from "@commerce-klaus/eslint-config-sfcc"

export default defineConfig(
  sfcc.configs.recommended,
  sfcc.configs["storefront-next"],
)
PresetEnabled architecture rules
storefront-nextno-controllers, no-forms, no-isml-rendering, no-pipeline-api, no-sfra-server
pwano-controllers, no-forms, no-isml-rendering, no-pipeline-api, no-sfra-server
sfrano-pipeline-api
sitegenesis-controllersno-pipeline-api, no-sfra-server
sitegenesis-pipelinesno-sfra-server

pwa and storefront-next currently enforce the same headless boundaries. They have separate semantic names so their policies can evolve independently as their platform contracts diverge.

The sitegenesis-pipelines preset applies to JavaScript only. It is the only preset that permits access to dw/system/Pipeline; all other presets reject pipeline execution. ESLint cannot validate pipeline XML files.

Restrict a preset to selected cartridges

Use createStorefrontConfig() when a repository contains multiple storefront architectures:

eslint.config.js
js
import { defineConfig } from "eslint/config"
import sfcc, { createStorefrontConfig } from "@commerce-klaus/eslint-config-sfcc"

export default defineConfig(
  sfcc.configs.recommended,
  createStorefrontConfig("sitegenesis-controllers", {
    cartridges: ["app_sitegenesis"],
  }),
  createStorefrontConfig("pwa", {
    cartridges: ["app_pwa", "int_pwa_backend"],
  }),
)

The helper accepts cartridgesDir, cartridges, and files. Explicit files globs take precedence over generated cartridge globs. Because each overlay both enables its own policy and disables incompatible controller rules, later presets can safely override recommended for their selected files.

The recommended config deliberately disables selected rules from commonly used ESLint presets when they suggest syntax, APIs, or module patterns that SFCC/Rhino does not support. This allows you to combine the SFCC config with those presets without forcing otherwise valid server-side SFCC code into incompatible modern JavaScript patterns.

The compatibility overrides cover:

  • ESLint recommended and core rules: disables rules such as object-shorthand, prefer-object-spread, prefer-rest-params, and prefer-spread that can suggest unsupported syntax or APIs. The core prefer-const rule is replaced by SFCC-aware rules that account for Rhino's scoping behavior.
  • eslint-plugin-unicorn: disables modern syntax and API preferences such as prefer-at, prefer-module, prefer-spread, and prefer-string-replace-all, as well as rules whose fixes or assumptions are unsafe for Rhino and SFCC APIs.
  • typescript-eslint: disables @typescript-eslint/no-require-imports because SFCC server-side modules use CommonJS require().
  • eslint-plugin-sonarjs: disables sonarjs/no-implicit-global because its assumptions conflict with SFCC's server-side module environment.

These overrides only take effect for rules enabled by another config in your ESLint setup; this package does not otherwise enable the external presets.

Use with Oxlint

Oxlint can run the included sfcc and sitegenesis plugins through its alpha JavaScript plugin API. Create an oxlint.config.mjs file that exports the included preset:

oxlint.config.mjs
js
import sfcc from "@commerce-klaus/eslint-config-sfcc/configs/oxlint"

export default sfcc

The preset loads both plugins and enables the supported SFCC and SiteGenesis rules. sfcc/no-ds-files is excluded because Oxlint ignores .ds files. sfcc/no-e4x-syntax and sfcc/no-type-annotations are excluded because their invalid JavaScript syntax causes Oxlint parser errors before JavaScript plugins can run.

For custom rule severities, import the rule record directly:

oxlint.config.mjs
js
import sfcc, { oxlintRules } from "@commerce-klaus/eslint-config-sfcc"

export default {
  lint: {
    ...sfcc.lint,
    rules: {
      ...oxlintRules,
      "sfcc/prefer-const": "warn",
    },
  },
}

Rules use their syntax-based behavior in Oxlint. Oxlint JavaScript plugins do not provide TypeScript parser services, so type-aware refinements are unavailable.

ESLint after Oxlint

Run ESLint after Oxlint to cover only the three rules that Oxlint cannot run: sfcc/no-ds-files, sfcc/no-e4x-syntax, and sfcc/no-type-annotations.

This optional fallback requires @typescript-eslint/parser:

bash
pnpm add -D @typescript-eslint/parser
bash
yarn add -D @typescript-eslint/parser
bash
npm install -D @typescript-eslint/parser
eslint.config.js
js
import { defineConfig } from "eslint/config"
import eslintAfterOxlint from "@commerce-klaus/eslint-config-sfcc/configs/eslint-after-oxlint"

export default defineConfig(eslintAfterOxlint)

This config deliberately does not enable the Oxlint-compatible rules, so the second lint pass does not duplicate their diagnostics. The subpath also exports createEslintAfterOxlintConfig() when the cartridge path, file globs, or ignored paths differ from the defaults.

Customize with shared SFCC settings

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.

eslint.config.js
js
import { defineConfig } from "eslint/config"
import { createRecommendedConfig } from "@commerce-klaus/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 (defaults to sites/site_template when site is set)
      siteTemplatePath: "sites/site_template",
      // Optional: site id under <siteTemplatePath>/sites/<site>/site.xml
      site: "example",
    },
  }),
)

Register plugins manually

The recommended config already registers both built-in plugins. Register them manually only when composing individual rules without the preset.

eslint.config.js
js
import { defineConfig } from "eslint/config"
import { sfcc as sfccPlugin, sitegenesis } from "@commerce-klaus/eslint-config-sfcc"

export default defineConfig({
  plugins: {
    sfcc: sfccPlugin,
    sitegenesis,
  },
  rules: {
    "sfcc/prefer-const": "error",
    "sitegenesis/no-global-require": "error",
  },
})

Compatibility Guide

Use this section to decide whether a pattern is safe on SFCC, requires a targeted lint fix, or must be migrated before ESLint can parse it.

Quick reference

PatternResultRecommended action
XML and XMLList identifiersAllowedConstructor-style references remain available.
Static JSX/E4X-like markupLint errorConvert it to XML(\...`); see [sfcc/no-e4x-syntax`](rules/sfcc/no-e4x-syntax.md).
Dynamic JSX/E4X-like markupLint errorRefactor manually; no automatic conversion is offered.
Type annotations in .jsLint errorMove types to JSDoc; see sfcc/no-type-annotations.
importScript, importPackage, or importClassLint errorUse CommonJS require(); see sfcc/no-rhino-import-globals.
SFCC empty(...)Lint errorUse an explicit type-appropriate check; see sfcc/no-empty-global.
Java-style String.equals(...)Lint errorUse strict equality; see sfcc/no-string-equals.
.ds fileLint errorRename it to .js; see sfcc/no-ds-files.
default xml namespace = "..." or for each (...)Parser errorRewrite it before lint rules can run.

Rhino const strategy

Use const wherever Rhino can handle it reliably. Three coordinated rules keep declarations modern without introducing Rhino scoping failures.

ContextDeclarationRule
Function top level, never reassignedconstsfcc/prefer-const
Loop header or declaration inside a loop bodyletsfcc/rhino-const-compat
Nested block with a unique nameconstAllowed; no rule reports it.
Nested blocks that reuse the same declaration nameletsfcc/rhino-const-conflict

The rules are designed to run together: sfcc/prefer-const modernizes safe bindings, while the Rhino-specific rules protect loop and nested-block scopes. Repeated --fix runs therefore remain stable.

See all three rules in one example
js
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 // sfcc/rhino-const-conflict -> let
    process(temp)
  }
  if (flagB) {
    const temp = 2 // sfcc/rhino-const-conflict -> let
    process(temp)
  }

  return topLevel
}

Rhino can treat nested const declarations as function-scoped. A unique nested binding is safe, but reusing the same name in another nested block can produce a redeclaration error even though modern JavaScript accepts it.

E4X and parser boundaries

sfcc/no-e4x-syntax runs only after ESLint has parsed the file. It can report JSX/E4X-like elements and fragments that the configured parser accepts. Static markup receives an explicit XML(...) conversion suggestion; dynamic markup is reported without a suggestion because preserving escaping and runtime behavior requires manual review.

Some Rhino/E4X-era constructs are rejected earlier. default xml namespace = "..." and for each (value in collection) cause fatal parser errors, so no ESLint rule can inspect them. Rewrite these constructs manually using explicit XML handling and standard loops.

Migration Recipes

These focused replacements cover the legacy syntax most likely to prevent modern linting.

Iterate values

diff
- for each (item in items) {
+ for (let item of items) {
    process(item)
  }

Iterate object keys and values

diff
- for each (value in obj) {
-   process(value)
+ for (let key in obj) {
+   if (Object.prototype.hasOwnProperty.call(obj, key)) {
+     let value = obj[key]
+     process(value)
+   }
  }

Replace E4X literal markup

diff
- const payload = (
-   <request>
-     <id>{id}</id>
-   </request>
- )
+ const payload = XML(`<request><id>${id}</id></request>`)

default xml namespace = "..." is also parser-incompatible in modern JavaScript and must be refactored manually.

Built-in Plugins

This package ships two built-in ESLint plugins, both automatically registered in the recommended config:

  1. sfcc for general SFCC/Rhino compatibility rules
  2. sitegenesis for the SiteGenesis-specific controller rule ported from eslint-plugin-sitegenesis

sfcc

The sfcc plugin contains the general Rhino/SFCC runtime rules:

Recommended is not a ranking

Every rule in this plugin addresses an intentional compatibility or project-policy concern, whether or not it is enabled by the recommended config. The preset is a conservative baseline; opt-in rules are provided for conventions that depend on a team's architecture or migration goals. This distinction follows the Commerce Klaus philosophy of making platform constraints and project decisions explicit.

RuleDescriptionDefault
sfcc/no-controllersDisallows files below cartridge/controllers/ for Storefront Next, PWA, or selected cartridges that must remain controller-free.Off
sfcc/no-custom-apiDisallows JavaScript below cartridge/rest-apis/ in cartridges that must not expose Custom APIs.Off
sfcc/no-ds-filesDisallows legacy .ds files in SFCC projects. Use .js files instead.error
sfcc/no-dw-apiDisallows SFCC dw/* APIs in portable domain, utility, or shared cartridges, with support for exact and namespace allow lists.Off
sfcc/no-e4x-syntaxDisallows JSX/E4X-like tag syntax (e.g. <a/>) in SFCC JavaScript to avoid parser ambiguity and unsupported runtime patterns.error
sfcc/no-empty-globalDisallows the SFCC-specific empty(...) global. Use explicit checks such as .length === 0, Object.keys(...).length === 0, or .isEmpty() instead.error
sfcc/no-formsDisallows SFCC dw/web/Form* modules and the SFRA server.forms API in headless cartridges.Off
sfcc/no-hooksDisallows scripts registered by a cartridge's hooks.json, including Salesforce and project-specific hooks.Off
sfcc/no-isml-renderingDisallows ISML/template modules and SFRA response rendering in headless cartridges.Off
sfcc/no-page-designerDisallows dw/experience/* APIs in cartridges that must remain independent of Page Designer.Off
sfcc/no-pipeline-apiDisallows JavaScript access to the legacy dw/system/Pipeline API.Off
sfcc/no-platform-globalsDisallows stateful SFCC globals (customer, request, response, and session) in favor of explicit dependencies.Off
sfcc/no-proprietary-module-syntaxDisallows configurable SFCC-specific module syntax (*/*, ~/*, and module.superModule) for projects that require portable modules.Off
sfcc/no-custom-api-additional-propertiesDisallows additionalProperties in Custom API request body schemas, since the platform does not register such endpoints.error
sfcc/no-custom-api-response-methodsDisallows legacy global response APIs in Custom API implementations, which must return JSON through RESTResponseMgr.error
sfcc/no-string-equalsDisallows Java-style String.equals(...) calls in JavaScript files. Use strict equality (===) instead.error
sfcc/no-type-annotationsDisallows 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-globalsDisallows legacy Rhino globals importScript(...), importPackage(...), and importClass(...). Use CommonJS require() instead.error
sfcc/no-rhino-extensionsDisallows Rhino and LiveConnect runtime globals such as Iterator, Packages, java, and javax.Off
sfcc/no-service-frameworkDisallows dw/svc/* APIs in cartridges that must delegate external communication to an integration layer.Off
sfcc/no-sfra-serverDisallows the SFRA server module in headless and SiteGenesis cartridges.Off
sfcc/prefer-native-collectionsPrefers native Array, Map, and Set collections over explicit imports of concrete dw/util collection implementations.Off
sfcc/prefer-constRequires const for let declarations that are never reassigned, excluding Rhino-sensitive nested/loop contexts.error
sfcc/rhino-const-compatEnforces let instead of const in Rhino loop-critical contexts (loop headers and declarations inside loop bodies) and supports auto-fix.error
sfcc/rhino-const-conflictDetects 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-custom-api-dir-nameRequires Custom API rest-apis directory names to contain only lowercase alphanumeric characters and hyphens.error
sfcc/valid-custom-api-exportRequires a public static CommonJS export for each Custom API endpoint mapped to the file in the rest-apis api.json.error
sfcc/valid-hook-exportRequires a static CommonJS export for each Salesforce hook method registered for the file in the cartridge's hooks.json.error
sfcc/valid-require-pathValidates 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.

sitegenesis

sitegenesis only contains sitegenesis/no-global-require.

That rule is 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/.

RuleDescriptionDefault
sitegenesis/no-global-requireDisallows top-level require() calls in controller files when not every route function uses them. Only applies to files under cartridge/controllers/.error

Package history and migration

This package was originally maintained and published as @jenssimon/eslint-config-sfcc. Version 5.0.0 of that package introduced ESLint Flat Config and the breaking changes described below. Development continued under the @jenssimon scope before the package moved to Commerce Klaus as @commerce-klaus/eslint-config-sfcc, whose release history starts at version 1.0.0.

If you use @jenssimon/eslint-config-sfcc version 4 or earlier, migrate both the ESLint configuration format and the package name. If you already use version 5 or later under the @jenssimon scope, your configuration is already based on Flat Config; update the dependency and imports to the @commerce-klaus package name and review the current configuration API.

Migrating from @jenssimon/eslint-config-sfcc <= 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-es5eslint-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

  1. Replace .eslintrc.* with eslint.config.js
  2. Update the package name and import (see Usage above)
  3. Remove @jenssimon/eslint-config-base, eslint-plugin-es5, and eslint-plugin-sitegenesis from your dependencies — sitegenesis/no-global-require is built in and the general Rhino rules are now sfcc/*
  4. Add any formatting rules you need directly to your own eslint.config.js

Development

bash
vp install
vp test
vp check
vp pack

Released under the MIT License.