Skip to content

Commit dd12bce

Browse files
committed
feat(sfcc): add site template cartridge resolution via site setting
support resolving custom-cartridges from <siteTemplatePath>/sites/<site>/site.xml use lightweight XML parsing with fast-xml-parser extract site-template cartridge-path logic into a shared utility add dedicated utility tests and update valid-require-path tests
1 parent 6bce0e7 commit dd12bce

9 files changed

Lines changed: 258 additions & 9 deletions

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ The new `sfcc` plugin contains the general Rhino/SFCC runtime rules:
9292

9393
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`.
9494

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

9797
By default, `sfcc/valid-require-path` validates path patterns only and allows bare `server` requires.
9898

@@ -112,6 +112,10 @@ export default defineConfig(
112112
checkCartridgeExists: true,
113113
// Optional: explicit cartridge order for */* lookup (otherwise folders in cartridgesDir are used)
114114
cartridgePath: ["app_storefront", "modules", "app_custom"],
115+
// Optional: path to site template directory
116+
siteTemplatePath: "sites/site_template",
117+
// Optional: site id under <siteTemplatePath>/sites/<site>/site.xml
118+
site: "example",
115119
},
116120
}),
117121
)

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"semantic-release": "semantic-release"
4242
},
4343
"dependencies": {
44-
"eslint-plugin-es": "^4.1.0"
44+
"eslint-plugin-es": "^4.1.0",
45+
"fast-xml-parser": "^5.8.0"
4546
},
4647
"devDependencies": {
4748
"@eslint/js": "^10.0.1",

pnpm-lock.yaml

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/configs/recommended.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export function createRecommendedConfig(options: RecommendedConfigOptions = {}):
3131
(sfccOptions.allowBareModules !== undefined ||
3232
sfccOptions.checkCartridgeExists !== undefined ||
3333
sfccOptions.cartridgePath !== undefined ||
34-
sfccOptions.cartridgesDir !== undefined)
34+
sfccOptions.cartridgesDir !== undefined ||
35+
sfccOptions.siteTemplatePath !== undefined ||
36+
sfccOptions.site !== undefined)
3537

3638
const sfccSettings: SfccSettings | undefined = hasSfccOptions
3739
? {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { XMLParser } from "fast-xml-parser"
2+
import fs from "node:fs"
3+
import path from "node:path"
4+
5+
function resolveSiteTemplateXmlPath(
6+
siteTemplatePath: string | undefined,
7+
site: string | undefined,
8+
cwd: string,
9+
): string | undefined {
10+
if (!siteTemplatePath || !site) {
11+
return undefined
12+
}
13+
14+
const resolvedSiteTemplatePath = path.isAbsolute(siteTemplatePath)
15+
? siteTemplatePath
16+
: path.resolve(cwd, siteTemplatePath)
17+
18+
return path.join(resolvedSiteTemplatePath, "sites", site, "site.xml")
19+
}
20+
21+
function findCustomCartridges(value: unknown): string | undefined {
22+
if (!value || typeof value !== "object") {
23+
return undefined
24+
}
25+
26+
if (Array.isArray(value)) {
27+
for (const item of value) {
28+
const found = findCustomCartridges(item)
29+
if (found) {
30+
return found
31+
}
32+
}
33+
return undefined
34+
}
35+
36+
const objectValue = value as Record<string, unknown>
37+
const directValue = objectValue["custom-cartridges"]
38+
if (typeof directValue === "string") {
39+
return directValue
40+
}
41+
42+
for (const nestedValue of Object.values(objectValue)) {
43+
const found = findCustomCartridges(nestedValue)
44+
if (found) {
45+
return found
46+
}
47+
}
48+
49+
return undefined
50+
}
51+
52+
export function getSiteTemplateCartridgePath(
53+
siteTemplatePath: string | undefined,
54+
site: string | undefined,
55+
cwd: string,
56+
): string[] {
57+
const siteTemplateXmlPath = resolveSiteTemplateXmlPath(siteTemplatePath, site, cwd)
58+
if (!siteTemplateXmlPath) {
59+
return []
60+
}
61+
62+
try {
63+
const xmlContent = fs.readFileSync(siteTemplateXmlPath, "utf8")
64+
const parser = new XMLParser({
65+
ignoreAttributes: true,
66+
trimValues: true,
67+
parseTagValue: false,
68+
parseAttributeValue: false,
69+
})
70+
const parsed = parser.parse(xmlContent) as unknown
71+
const customCartridges = findCustomCartridges(parsed)
72+
73+
if (!customCartridges) {
74+
return []
75+
}
76+
77+
return customCartridges
78+
.split(":")
79+
.map((entry) => entry.trim())
80+
.filter((entry) => entry.length > 0)
81+
} catch {
82+
return []
83+
}
84+
}

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from "node:fs"
44
import path from "node:path"
55

66
import { withSfccSettings } from "../_utils/sfcc-settings.js"
7+
import { getSiteTemplateCartridgePath } from "../_utils/site-template-cartridge-path.js"
78

89
const SUPPORTED_EXTENSIONS = ["js", "ds", "json"]
910

@@ -101,17 +102,15 @@ function starReferenceExists(
101102
requirePath: string,
102103
cartridgesDir: string,
103104
cwd: string,
104-
configuredCartridgePath: string[],
105+
cartridgePath: string[],
105106
): boolean {
106107
const moduleTarget = requirePath.slice(2)
107108
if (!moduleTarget) {
108109
return false
109110
}
110111

111112
const cartridgeNames =
112-
configuredCartridgePath.length > 0
113-
? configuredCartridgePath
114-
: getFilesystemCartridges(cartridgesDir, cwd)
113+
cartridgePath.length > 0 ? cartridgePath : getFilesystemCartridges(cartridgesDir, cwd)
115114

116115
return cartridgeNames.some((name) =>
117116
moduleExistsInCartridge(name, moduleTarget, cartridgesDir, cwd),
@@ -173,11 +172,18 @@ const validRequirePath: Rule.RuleModule = {
173172
const allowBareModules = new Set(options.allowBareModules ?? ["server"])
174173
const checkCartridgeExists = options.checkCartridgeExists === true
175174
const cartridgesDir = options.cartridgesDir ?? "cartridges"
176-
const configuredCartridgePath = getConfiguredCartridgePath(options.cartridgePath)
177175
const cwd =
178176
(context as Rule.RuleContext & { cwd?: string }).cwd ??
179177
(context as Rule.RuleContext & { getCwd?: () => string }).getCwd?.() ??
180178
process.cwd()
179+
const configuredCartridgePath = getConfiguredCartridgePath(options.cartridgePath)
180+
const templateCartridgePath = getSiteTemplateCartridgePath(
181+
options.siteTemplatePath,
182+
options.site,
183+
cwd,
184+
)
185+
const cartridgePath =
186+
configuredCartridgePath.length > 0 ? configuredCartridgePath : templateCartridgePath
181187
const filename =
182188
(context as Rule.RuleContext & { filename?: string }).filename ??
183189
(context as Rule.RuleContext & { getFilename?: () => string }).getFilename?.() ??
@@ -207,7 +213,7 @@ const validRequirePath: Rule.RuleModule = {
207213
if (requirePath.startsWith("*/")) {
208214
if (
209215
checkCartridgeExists &&
210-
!starReferenceExists(requirePath, cartridgesDir, cwd, configuredCartridgePath)
216+
!starReferenceExists(requirePath, cartridgesDir, cwd, cartridgePath)
211217
) {
212218
context.report({
213219
node: firstArgument,

src/types/sfcc-settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ export interface SfccSettings {
33
checkCartridgeExists?: boolean
44
cartridgesDir?: string
55
cartridgePath?: string[]
6+
siteTemplatePath?: string
7+
site?: string
68
}

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,57 @@ test("supports checkCartridgeExists option", () => {
127127
expect(hits.some((m) => m.message.includes("~/cartridge/scripts/does-not-exist"))).toBe(true)
128128
})
129129

130+
test("supports cartridge order from site template", () => {
131+
const testSuffix = `${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`
132+
const cartridgeName = `app_storefront_${testSuffix}`
133+
const ownCartridgeName = `app_sfra_${testSuffix}`
134+
const cartridgesDir = path.join(process.cwd(), "cartridges")
135+
const targetCartridge = path.join(cartridgesDir, cartridgeName)
136+
const ownCartridge = path.join(cartridgesDir, ownCartridgeName)
137+
const siteTemplatePath = path.join(process.cwd(), `site_template_${testSuffix}`)
138+
const site = "example"
139+
const siteTemplateXmlPath = path.join(siteTemplatePath, "sites", site, "site.xml")
140+
141+
fs.mkdirSync(path.join(targetCartridge, "cartridge", "scripts"), { recursive: true })
142+
fs.mkdirSync(path.join(ownCartridge, "cartridge", "scripts"), { recursive: true })
143+
fs.mkdirSync(path.dirname(siteTemplateXmlPath), { recursive: true })
144+
fs.writeFileSync(
145+
path.join(targetCartridge, "cartridge", "scripts", "ok.js"),
146+
"module.exports = true",
147+
)
148+
try {
149+
fs.writeFileSync(
150+
siteTemplateXmlPath,
151+
`<site><custom-cartridges>app_base:${cartridgeName}:int_payments</custom-cartridges></site>`,
152+
)
153+
154+
const linter = new Linter()
155+
const config = createRecommendedConfig({
156+
cartridgesDir: "cartridges",
157+
sfcc: {
158+
checkCartridgeExists: true,
159+
siteTemplatePath,
160+
site,
161+
},
162+
})
163+
164+
const messages = linter.verify(
165+
`
166+
const okStar = require("*/cartridge/scripts/ok")
167+
module.exports = { okStar }
168+
`,
169+
config,
170+
{ filename: `cartridges/${ownCartridgeName}/cartridge/controllers/Home.js` },
171+
)
172+
173+
expect(messages.some((m) => m.ruleId === "sfcc/valid-require-path")).toBe(false)
174+
} finally {
175+
fs.rmSync(targetCartridge, { recursive: true, force: true })
176+
fs.rmSync(ownCartridge, { recursive: true, force: true })
177+
fs.rmSync(siteTemplatePath, { recursive: true, force: true })
178+
}
179+
})
180+
130181
test("rejects direct rule options and requires shared settings", () => {
131182
const linter = new Linter()
132183

0 commit comments

Comments
 (0)