Skip to content

Commit cd8a021

Browse files
committed
feat(sfcc): validate */ and ~/ require paths against filesystem existence
extend sfcc/valid-require-path with optional existence checks for */ and ~/ add optional cartridgePath ordering for */ resolution extend tests for valid/invalid star and tilde references update README docs for the new options
1 parent 787e577 commit cd8a021

3 files changed

Lines changed: 174 additions & 14 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ The new `sfcc` plugin contains the general Rhino/SFCC runtime rules:
8383
| `sfcc/prefer-const` | Requires `const` for `let` declarations that are never reassigned, excluding Rhino-sensitive nested/loop contexts. | `error` |
8484
| `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` |
8585
| `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` |
86-
| `sfcc/valid-require-path` | Validates SFCC-compatible `require()` paths (`dw/*`, `cartridgeName/*`, `./*`, `../*`, `*/*`, `~/*`) and supports optional cartridge-existence checks. | `error` |
86+
| `sfcc/valid-require-path` | Validates SFCC-compatible `require()` paths (`dw/*`, `cartridgeName/*`, `./*`, `../*`, `*/*`, `~/*`) and supports optional filesystem existence checks. | `error` |
8787

8888
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`.
8989

@@ -102,10 +102,12 @@ export default defineConfig(sfcc.configs.recommended, {
102102
{
103103
// Optional: allow additional bare module ids
104104
allowBareModules: ["server", "proxyquire"],
105-
// Optional: verify cartridgeName/* against filesystem
105+
// Optional: verify cartridgeName/* plus */* and ~/* against filesystem
106106
checkCartridgeExists: true,
107107
// Optional: absolute or relative path to cartridges root
108108
cartridgesDir: "cartridges",
109+
// Optional: explicit cartridge order for */* lookup (otherwise folders in cartridgesDir are used)
110+
cartridgePath: ["app_storefront", "modules", "app_custom"],
109111
},
110112
],
111113
},

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

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ type RuleOptions = {
77
allowBareModules?: string[]
88
checkCartridgeExists?: boolean
99
cartridgesDir?: string
10+
cartridgePath?: string[]
1011
}
1112

13+
const SUPPORTED_EXTENSIONS = ["js", "ds", "json"]
14+
1215
function getStringArgument(node: Rule.Node): string | undefined {
1316
if (node.type === "Literal" && typeof node.value === "string") {
1417
return node.value
@@ -44,6 +47,101 @@ function resolveCartridgesDir(cartridgesDir: string, cwd: string): string {
4447
return path.isAbsolute(cartridgesDir) ? cartridgesDir : path.resolve(cwd, cartridgesDir)
4548
}
4649

50+
function getConfiguredCartridgePath(cartridgePath: string[] | undefined): string[] {
51+
if (!cartridgePath) {
52+
return []
53+
}
54+
55+
return cartridgePath.filter((entry) => entry.trim().length > 0)
56+
}
57+
58+
function getFilesystemCartridges(cartridgesDir: string, cwd: string): string[] {
59+
const baseDir = resolveCartridgesDir(cartridgesDir, cwd)
60+
61+
try {
62+
return fs
63+
.readdirSync(baseDir, { withFileTypes: true })
64+
.filter((entry) => entry.isDirectory())
65+
.map((entry) => entry.name)
66+
} catch {
67+
return []
68+
}
69+
}
70+
71+
function moduleExistsInCartridge(
72+
cartridgeName: string,
73+
moduleTarget: string,
74+
cartridgesDir: string,
75+
cwd: string,
76+
): boolean {
77+
const baseDir = resolveCartridgesDir(cartridgesDir, cwd)
78+
const normalizedTarget = moduleTarget.replace(/^\/+/, "")
79+
const targetPath = path.join(baseDir, cartridgeName, normalizedTarget)
80+
81+
if (path.extname(normalizedTarget)) {
82+
return fs.existsSync(targetPath)
83+
}
84+
85+
return SUPPORTED_EXTENSIONS.some((extension) => fs.existsSync(`${targetPath}.${extension}`))
86+
}
87+
88+
function getOwnCartridge(filename: string, cartridgesDir: string, cwd: string): string | undefined {
89+
if (filename === "<input>") {
90+
return undefined
91+
}
92+
93+
const resolvedFilename = path.isAbsolute(filename) ? filename : path.resolve(cwd, filename)
94+
const baseDir = resolveCartridgesDir(cartridgesDir, cwd)
95+
const relativePath = path.relative(baseDir, resolvedFilename)
96+
97+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
98+
return undefined
99+
}
100+
101+
const firstSegment = relativePath.split(path.sep)[0]
102+
return firstSegment && firstSegment !== "." ? firstSegment : undefined
103+
}
104+
105+
function starReferenceExists(
106+
requirePath: string,
107+
cartridgesDir: string,
108+
cwd: string,
109+
configuredCartridgePath: string[],
110+
): boolean {
111+
const moduleTarget = requirePath.slice(2)
112+
if (!moduleTarget) {
113+
return false
114+
}
115+
116+
const cartridgeNames =
117+
configuredCartridgePath.length > 0
118+
? configuredCartridgePath
119+
: getFilesystemCartridges(cartridgesDir, cwd)
120+
121+
return cartridgeNames.some((name) =>
122+
moduleExistsInCartridge(name, moduleTarget, cartridgesDir, cwd),
123+
)
124+
}
125+
126+
function tildeReferenceExists(
127+
requirePath: string,
128+
filename: string,
129+
cartridgesDir: string,
130+
cwd: string,
131+
): boolean {
132+
const moduleTarget = requirePath.slice(2)
133+
if (!moduleTarget) {
134+
return false
135+
}
136+
137+
const ownCartridge = getOwnCartridge(filename, cartridgesDir, cwd)
138+
if (!ownCartridge) {
139+
return false
140+
}
141+
142+
return moduleExistsInCartridge(ownCartridge, moduleTarget, cartridgesDir, cwd)
143+
}
144+
47145
function cartridgeExists(cartridgeName: string, cartridgesDir: string, cwd: string): boolean {
48146
const baseDir = resolveCartridgesDir(cartridgesDir, cwd)
49147
const cartridgeRoot = path.join(baseDir, cartridgeName)
@@ -74,6 +172,11 @@ const validRequirePath: Rule.RuleModule = {
74172
},
75173
checkCartridgeExists: { type: "boolean" },
76174
cartridgesDir: { type: "string" },
175+
cartridgePath: {
176+
type: "array",
177+
items: { type: "string" },
178+
uniqueItems: true,
179+
},
77180
},
78181
additionalProperties: false,
79182
},
@@ -83,17 +186,26 @@ const validRequirePath: Rule.RuleModule = {
83186
'Invalid require path "{{requirePath}}". Allowed: dw/*, cartridgeName/*, ./*, ../*, */*, ~/* or configured bare modules.',
84187
unknownCartridge:
85188
'Unknown cartridge "{{cartridgeName}}" in require path "{{requirePath}}" (checked in "{{cartridgesDir}}/").',
189+
unresolvedStarPath:
190+
'Cannot resolve "{{requirePath}}" against configured cartridges in "{{cartridgesDir}}/".',
191+
unresolvedTildePath:
192+
'Cannot resolve "{{requirePath}}" in current cartridge (checked in "{{cartridgesDir}}/").',
86193
},
87194
},
88195
create: (context) => {
89196
const options = (context.options[0] ?? {}) as RuleOptions
90197
const allowBareModules = new Set(options.allowBareModules ?? ["server"])
91198
const checkCartridgeExists = options.checkCartridgeExists === true
92199
const cartridgesDir = options.cartridgesDir ?? "cartridges"
200+
const configuredCartridgePath = getConfiguredCartridgePath(options.cartridgePath)
93201
const cwd =
94202
(context as Rule.RuleContext & { cwd?: string }).cwd ??
95203
(context as Rule.RuleContext & { getCwd?: () => string }).getCwd?.() ??
96204
process.cwd()
205+
const filename =
206+
(context as Rule.RuleContext & { filename?: string }).filename ??
207+
(context as Rule.RuleContext & { getFilename?: () => string }).getFilename?.() ??
208+
"<input>"
97209

98210
return {
99211
CallExpression(node) {
@@ -116,6 +228,34 @@ const validRequirePath: Rule.RuleModule = {
116228
return
117229
}
118230

231+
if (requirePath.startsWith("*/")) {
232+
if (
233+
checkCartridgeExists &&
234+
!starReferenceExists(requirePath, cartridgesDir, cwd, configuredCartridgePath)
235+
) {
236+
context.report({
237+
node: firstArgument,
238+
messageId: "unresolvedStarPath",
239+
data: { requirePath, cartridgesDir },
240+
})
241+
}
242+
return
243+
}
244+
245+
if (requirePath.startsWith("~/")) {
246+
if (
247+
checkCartridgeExists &&
248+
!tildeReferenceExists(requirePath, filename, cartridgesDir, cwd)
249+
) {
250+
context.report({
251+
node: firstArgument,
252+
messageId: "unresolvedTildePath",
253+
data: { requirePath, cartridgesDir },
254+
})
255+
}
256+
return
257+
}
258+
119259
if (isAllowedPrefix(requirePath)) {
120260
return
121261
}

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

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Linter } from "eslint"
22
import fs from "node:fs"
3-
import os from "node:os"
43
import path from "node:path"
54
import { expect, test } from "vite-plus/test"
65

@@ -78,11 +77,23 @@ test("ignores dynamic requires", () => {
7877
})
7978

8079
test("supports checkCartridgeExists option", () => {
81-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "eslint-config-sfcc-"))
82-
const cartridgesDir = path.join(tempRoot, "cartridges")
83-
const existingCartridge = path.join(cartridgesDir, "app_storefront")
84-
85-
fs.mkdirSync(existingCartridge, { recursive: true })
80+
const testSuffix = `${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`
81+
const existingCartridgeName = `app_storefront_${testSuffix}`
82+
const ownCartridgeName = `app_sfra_${testSuffix}`
83+
const cartridgesDir = path.join(process.cwd(), "cartridges")
84+
const existingCartridge = path.join(cartridgesDir, existingCartridgeName)
85+
const ownCartridge = path.join(cartridgesDir, ownCartridgeName)
86+
87+
fs.mkdirSync(path.join(existingCartridge, "cartridge", "scripts"), { recursive: true })
88+
fs.mkdirSync(path.join(ownCartridge, "cartridge", "scripts"), { recursive: true })
89+
fs.writeFileSync(
90+
path.join(existingCartridge, "cartridge", "scripts", "ok.js"),
91+
"module.exports = true",
92+
)
93+
fs.writeFileSync(
94+
path.join(ownCartridge, "cartridge", "scripts", "local.js"),
95+
"module.exports = true",
96+
)
8697

8798
const linter = new Linter()
8899
const config: Linter.Config[] = [
@@ -94,7 +105,7 @@ test("supports checkCartridgeExists option", () => {
94105
"error",
95106
{
96107
checkCartridgeExists: true,
97-
cartridgesDir,
108+
cartridgesDir: "cartridges",
98109
},
99110
],
100111
},
@@ -103,17 +114,24 @@ test("supports checkCartridgeExists option", () => {
103114

104115
const messages = linter.verify(
105116
`
106-
const ok = require("app_storefront/cartridge/scripts/ok")
117+
const ok = require("${existingCartridgeName}/cartridge/scripts/ok")
118+
const okStar = require("*/cartridge/scripts/ok")
119+
const okTilde = require("~/cartridge/scripts/local")
107120
const bad = require("missing_cartridge/cartridge/scripts/bad")
108-
module.exports = { ok, bad }
121+
const badStar = require("*/cartridge/scripts/does-not-exist")
122+
const badTilde = require("~/cartridge/scripts/does-not-exist")
123+
module.exports = { ok, okStar, okTilde, bad, badStar, badTilde }
109124
`,
110125
config,
111-
{ filename: "cartridges/app_sfra/cartridge/controllers/Home.js" },
126+
{ filename: `cartridges/${ownCartridgeName}/cartridge/controllers/Home.js` },
112127
)
113128

114-
fs.rmSync(tempRoot, { recursive: true, force: true })
129+
fs.rmSync(existingCartridge, { recursive: true, force: true })
130+
fs.rmSync(ownCartridge, { recursive: true, force: true })
115131

116132
const hits = messages.filter((m) => m.ruleId === "sfcc/valid-require-path")
117-
expect(hits).toHaveLength(1)
133+
expect(hits).toHaveLength(3)
118134
expect(hits[0]?.message.includes("missing_cartridge")).toBe(true)
135+
expect(hits.some((m) => m.message.includes("*/cartridge/scripts/does-not-exist"))).toBe(true)
136+
expect(hits.some((m) => m.message.includes("~/cartridge/scripts/does-not-exist"))).toBe(true)
119137
})

0 commit comments

Comments
 (0)