Skip to content

Commit 6bce0e7

Browse files
committed
refactor(sfcc): centralize shared settings handling across sfcc rules
extract shared sfcc settings access into reusable utilities apply withSfccSettings consistently across sfcc rules keep runtime behavior unchanged align tests and docs with the shared settings pattern
1 parent a8227e3 commit 6bce0e7

10 files changed

Lines changed: 126 additions & 71 deletions

README.md

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ import { createRecommendedConfig } from "@jenssimon/eslint-config-sfcc"
5151
export default defineConfig(
5252
createRecommendedConfig({
5353
cartridgesDir: "cartridges/",
54+
sfcc: {
55+
checkCartridgeExists: true,
56+
allowBareModules: ["server", "proxyquire"],
57+
cartridgePath: ["app_storefront", "modules", "app_custom"],
58+
},
5459
}),
5560
)
5661
```
@@ -87,31 +92,29 @@ The new `sfcc` plugin contains the general Rhino/SFCC runtime rules:
8792

8893
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`.
8994

90-
### `sfcc/valid-require-path` options
95+
### Shared `sfcc` options (including `sfcc/valid-require-path`)
9196

92-
By default, the rule validates path patterns only and allows bare `server` requires.
97+
By default, `sfcc/valid-require-path` validates path patterns only and allows bare `server` requires.
98+
99+
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.
93100

94101
```js
95102
import { defineConfig } from "eslint/config"
96-
import sfcc from "@jenssimon/eslint-config-sfcc"
103+
import { createRecommendedConfig } from "@jenssimon/eslint-config-sfcc"
97104

98-
export default defineConfig(sfcc.configs.recommended, {
99-
rules: {
100-
"sfcc/valid-require-path": [
101-
"error",
102-
{
103-
// Optional: allow additional bare module ids
104-
allowBareModules: ["server", "proxyquire"],
105-
// Optional: verify cartridgeName/* plus */* and ~/* against filesystem
106-
checkCartridgeExists: true,
107-
// Optional: absolute or relative path to cartridges root
108-
cartridgesDir: "cartridges",
109-
// Optional: explicit cartridge order for */* lookup (otherwise folders in cartridgesDir are used)
110-
cartridgePath: ["app_storefront", "modules", "app_custom"],
111-
},
112-
],
113-
},
114-
})
105+
export default defineConfig(
106+
createRecommendedConfig({
107+
cartridgesDir: "cartridges",
108+
sfcc: {
109+
// Optional: allow additional bare module ids
110+
allowBareModules: ["server", "proxyquire"],
111+
// Optional: verify cartridgeName/* plus */* and ~/* against filesystem
112+
checkCartridgeExists: true,
113+
// Optional: explicit cartridge order for */* lookup (otherwise folders in cartridgesDir are used)
114+
cartridgePath: ["app_storefront", "modules", "app_custom"],
115+
},
116+
}),
117+
)
115118
```
116119

117120
### Rhino const strategy example

src/configs/recommended.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { fixupPluginRules } from "@eslint/compat"
44
import es from "eslint-plugin-es"
55
import globals from "globals"
66

7+
import type { SfccSettings } from "../types/sfcc-settings.js"
8+
79
import sfcc from "../plugins/sfcc/index.js"
810
import sitegenesis from "../plugins/sitegenesis/index.js"
911
import rules from "../rules/index.js"
@@ -16,12 +18,29 @@ export interface RecommendedConfigOptions {
1618
files?: string[]
1719
/** Optional override for ignore globs. */
1820
ignores?: string[]
21+
/** Optional shared options for sfcc rules. */
22+
sfcc?: SfccSettings
1923
}
2024

2125
/** Creates the recommended flat config for SFCC projects. */
2226
export function createRecommendedConfig(options: RecommendedConfigOptions = {}): Linter.Config[] {
23-
const { cartridgesDir = "cartridges", files, ignores } = options
27+
const { cartridgesDir = "cartridges", files, ignores, sfcc: sfccOptions } = options
2428
const normalizedCartridgesDir = cartridgesDir.replace(/\/+$/u, "") || "/"
29+
const hasSfccOptions =
30+
sfccOptions !== undefined &&
31+
(sfccOptions.allowBareModules !== undefined ||
32+
sfccOptions.checkCartridgeExists !== undefined ||
33+
sfccOptions.cartridgePath !== undefined ||
34+
sfccOptions.cartridgesDir !== undefined)
35+
36+
const sfccSettings: SfccSettings | undefined = hasSfccOptions
37+
? {
38+
...sfccOptions,
39+
...(sfccOptions?.cartridgesDir === undefined
40+
? { cartridgesDir: normalizedCartridgesDir }
41+
: {}),
42+
}
43+
: undefined
2544

2645
function withBaseDir(suffix: string): string {
2746
return normalizedCartridgesDir === "/" ? `/${suffix}` : `${normalizedCartridgesDir}/${suffix}`
@@ -46,6 +65,7 @@ export function createRecommendedConfig(options: RecommendedConfigOptions = {}):
4665
sfcc,
4766
sitegenesis,
4867
},
68+
...(sfccSettings === undefined ? {} : { settings: { sfcc: sfccSettings } }),
4969
rules,
5070
},
5171
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Rule } from "eslint"
2+
3+
import type { SfccSettings } from "../../types/sfcc-settings.js"
4+
5+
type RuleContextWithSettings = Rule.RuleContext & {
6+
settings?: { sfcc?: SfccSettings }
7+
}
8+
9+
export function getSfccSettings(context: Rule.RuleContext): SfccSettings {
10+
return (context as RuleContextWithSettings).settings?.sfcc ?? {}
11+
}
12+
13+
export function withSfccSettings(
14+
createWithSettings: (context: Rule.RuleContext, sfccSettings: SfccSettings) => Rule.RuleListener,
15+
): (context: Rule.RuleContext) => Rule.RuleListener {
16+
return (context) => createWithSettings(context, getSfccSettings(context))
17+
}

src/plugins/sfcc/prefer-const.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Rule, Scope } from "eslint"
22

33
import { isInNestedBlock, isRhinoCriticalScope } from "../_utils/rhino-scope.js"
4+
import { withSfccSettings } from "../_utils/sfcc-settings.js"
45

56
function isNeverReassigned(variable: Scope.Variable): boolean {
67
// A variable is "never reassigned" if all its write references are the
@@ -23,7 +24,7 @@ const preferConst: Rule.RuleModule = {
2324
useConst: "Prefer const over {{kind}} when the variable is never reassigned.",
2425
},
2526
},
26-
create: (context) => {
27+
create: withSfccSettings((context) => {
2728
function checkDeclaration(node: Rule.Node) {
2829
// In Rhino-critical scopes const is broken — don't suggest it here.
2930
// In any nested block, const might cause Rhino re-declaration conflicts.
@@ -85,7 +86,7 @@ const preferConst: Rule.RuleModule = {
8586
"VariableDeclaration[kind='let']": checkDeclaration,
8687
"VariableDeclaration[kind='var']": checkVarDeclaration,
8788
}
88-
},
89+
}),
8990
}
9091

9192
export default preferConst

src/plugins/sfcc/rhino-const-compat.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Rule } from "eslint"
22

33
import { isRhinoCriticalScope } from "../_utils/rhino-scope.js"
4+
import { withSfccSettings } from "../_utils/sfcc-settings.js"
45

56
function isRhinoCriticalConstDeclaration(node: Rule.Node): boolean {
67
return node.type === "VariableDeclaration" && node.kind === "const" && isRhinoCriticalScope(node)
@@ -19,7 +20,7 @@ const rhinoConstCompat: Rule.RuleModule = {
1920
useLet: "Use let instead of const in this block-scoped context for Rhino compatibility.",
2021
},
2122
},
22-
create: (context) => ({
23+
create: withSfccSettings((context) => ({
2324
VariableDeclaration(node) {
2425
if (!isRhinoCriticalConstDeclaration(node as Rule.Node)) {
2526
return
@@ -38,7 +39,7 @@ const rhinoConstCompat: Rule.RuleModule = {
3839
},
3940
})
4041
},
41-
}),
42+
})),
4243
}
4344

4445
export default rhinoConstCompat

src/plugins/sfcc/rhino-const-conflict.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Rule } from "eslint"
22

33
import { isInNestedBlock } from "../_utils/rhino-scope.js"
4+
import { withSfccSettings } from "../_utils/sfcc-settings.js"
45

56
interface FuncScope {
67
/** All const declarations per identifier name (function-level + nested). */
@@ -24,7 +25,7 @@ const rhinoConstConflict: Rule.RuleModule = {
2425
"'{{name}}' is declared as const in multiple block scopes of the same function. Use let to avoid a Rhino re-declaration error.",
2526
},
2627
},
27-
create: (context) => {
28+
create: withSfccSettings((context) => {
2829
const stack: FuncScope[] = []
2930

3031
function enterFunction(): void {
@@ -82,7 +83,7 @@ const rhinoConstConflict: Rule.RuleModule = {
8283
}
8384
},
8485
}
85-
},
86+
}),
8687
}
8788

8889
export default rhinoConstConflict

src/plugins/sfcc/valid-require-path.ts

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@ import type { Rule } from "eslint"
33
import fs from "node:fs"
44
import path from "node:path"
55

6-
type RuleOptions = {
7-
allowBareModules?: string[]
8-
checkCartridgeExists?: boolean
9-
cartridgesDir?: string
10-
cartridgePath?: string[]
11-
}
6+
import { withSfccSettings } from "../_utils/sfcc-settings.js"
127

138
const SUPPORTED_EXTENSIONS = ["js", "ds", "json"]
149

@@ -161,26 +156,8 @@ const validRequirePath: Rule.RuleModule = {
161156
"Enforce SFCC-compatible require paths (dw/, relative, cartridge-name/, */, ~/).",
162157
recommended: true,
163158
},
164-
schema: [
165-
{
166-
type: "object",
167-
properties: {
168-
allowBareModules: {
169-
type: "array",
170-
items: { type: "string" },
171-
uniqueItems: true,
172-
},
173-
checkCartridgeExists: { type: "boolean" },
174-
cartridgesDir: { type: "string" },
175-
cartridgePath: {
176-
type: "array",
177-
items: { type: "string" },
178-
uniqueItems: true,
179-
},
180-
},
181-
additionalProperties: false,
182-
},
183-
],
159+
// Shared sfcc configuration is provided via ESLint settings.sfcc.
160+
schema: [],
184161
messages: {
185162
invalidPath:
186163
'Invalid require path "{{requirePath}}". Allowed: dw/*, cartridgeName/*, ./*, ../*, */*, ~/* or configured bare modules.',
@@ -192,8 +169,7 @@ const validRequirePath: Rule.RuleModule = {
192169
'Cannot resolve "{{requirePath}}" in current cartridge (checked in "{{cartridgesDir}}/").',
193170
},
194171
},
195-
create: (context) => {
196-
const options = (context.options[0] ?? {}) as RuleOptions
172+
create: withSfccSettings((context, options) => {
197173
const allowBareModules = new Set(options.allowBareModules ?? ["server"])
198174
const checkCartridgeExists = options.checkCartridgeExists === true
199175
const cartridgesDir = options.cartridgesDir ?? "cartridges"
@@ -294,7 +270,7 @@ const validRequirePath: Rule.RuleModule = {
294270
}
295271
},
296272
}
297-
},
273+
}),
298274
}
299275

300276
export default validRequirePath

src/types/sfcc-settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface SfccSettings {
2+
allowBareModules?: string[]
3+
checkCartridgeExists?: boolean
4+
cartridgesDir?: string
5+
cartridgePath?: string[]
6+
}

tests/index.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,25 @@ test("accepts cartridgesDir with trailing slash", () => {
3333
expect(createdConfig).toEqual(recommended)
3434
})
3535

36+
test("forwards shared sfcc options via settings", () => {
37+
const createdConfig = createRecommendedConfig({
38+
cartridgesDir: "custom-cartridges",
39+
sfcc: {
40+
checkCartridgeExists: true,
41+
cartridgePath: ["app_storefront", "modules"],
42+
},
43+
})
44+
45+
expect(createdConfig[0]?.settings).toEqual({
46+
sfcc: {
47+
checkCartridgeExists: true,
48+
cartridgePath: ["app_storefront", "modules"],
49+
cartridgesDir: "custom-cartridges",
50+
},
51+
})
52+
expect(createdConfig[0]?.rules?.["sfcc/valid-require-path"]).toBe("error")
53+
})
54+
3655
test("exports sitegenesis plugin", () => {
3756
expect(sitegenesis).toBe(plugins.sitegenesis)
3857
})

tests/sfcc-valid-require-path.test.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from "node:fs"
33
import path from "node:path"
44
import { expect, test } from "vite-plus/test"
55

6-
import { recommended } from "../src/index.js"
6+
import { createRecommendedConfig, recommended } from "../src/index.js"
77

88
function lint(code: string, filename = "cartridges/app_sfra/cartridge/controllers/Home.js") {
99
const linter = new Linter()
@@ -96,21 +96,12 @@ test("supports checkCartridgeExists option", () => {
9696
)
9797

9898
const linter = new Linter()
99-
const config: Linter.Config[] = [
100-
{
101-
...recommended[0],
102-
rules: {
103-
...recommended[0]?.rules,
104-
"sfcc/valid-require-path": [
105-
"error",
106-
{
107-
checkCartridgeExists: true,
108-
cartridgesDir: "cartridges",
109-
},
110-
],
111-
},
99+
const config = createRecommendedConfig({
100+
cartridgesDir: "cartridges",
101+
sfcc: {
102+
checkCartridgeExists: true,
112103
},
113-
]
104+
})
114105

115106
const messages = linter.verify(
116107
`
@@ -135,3 +126,23 @@ test("supports checkCartridgeExists option", () => {
135126
expect(hits.some((m) => m.message.includes("*/cartridge/scripts/does-not-exist"))).toBe(true)
136127
expect(hits.some((m) => m.message.includes("~/cartridge/scripts/does-not-exist"))).toBe(true)
137128
})
129+
130+
test("rejects direct rule options and requires shared settings", () => {
131+
const linter = new Linter()
132+
133+
const config: Linter.Config[] = [
134+
{
135+
...recommended[0],
136+
rules: {
137+
...recommended[0]?.rules,
138+
"sfcc/valid-require-path": ["error", { checkCartridgeExists: true }],
139+
},
140+
},
141+
]
142+
143+
expect(() =>
144+
linter.verify('const x = require("server"); module.exports = x', config, {
145+
filename: "cartridges/app_sfra/cartridge/controllers/Home.js",
146+
}),
147+
).toThrow()
148+
})

0 commit comments

Comments
 (0)