Skip to content

Commit dbef5d0

Browse files
committed
feat(sfcc): add no-e4x-syntax rule
recommended config now reports JSX/E4X-like tag syntax in JavaScript files as errors via sfcc/no-e4x-syntax.
1 parent 34963e7 commit dbef5d0

5 files changed

Lines changed: 108 additions & 0 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Shareable ESLint flat config for Salesforce Commerce Cloud (SFCC) projects.
1313

1414
- Modern language features not supported on SFCC/Rhino (e.g. optional chaining, nullish coalescing, async/await, object spread, many ES2015+ builtins)
1515
- Top-level `await`, dynamic `import()`, class fields, new builtins like `Map`, `Set`, `Promise`, `Symbol`, etc.
16+
- JSX/E4X-like tag syntax (e.g. `<a/>`) that may be misparsed in JavaScript linting workflows
1617
- Features that would cause runtime or syntax errors on SFCC
1718
- Many ES2015+ Array/String/Object methods missing in Rhino
1819
- ECMAScript modules (`import`/`export`), as SFCC only supports CommonJS
@@ -85,6 +86,7 @@ The new `sfcc` plugin contains the general Rhino/SFCC runtime rules:
8586

8687
| Rule | Description | Default |
8788
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
89+
| `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` |
8890
| `sfcc/prefer-const` | Requires `const` for `let` declarations that are never reassigned, excluding Rhino-sensitive nested/loop contexts. | `error` |
8991
| `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` |
9092
| `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` |
@@ -199,6 +201,14 @@ if (foo === "baz") {
199201

200202
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`.
201203

204+
Q: Are `XML` and `XMLList` identifiers allowed?
205+
206+
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/>`).
207+
208+
Q: Does `sfcc/no-e4x-syntax` report `default xml namespace = "..."`?
209+
210+
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.
211+
202212
---
203213

204214
## Migrating from v4

src/plugins/sfcc/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import noE4xSyntax from "./no-e4x-syntax.js"
12
import preferConst from "./prefer-const.js"
23
import rhinoConstCompat from "./rhino-const-compat.js"
34
import rhinoConstConflict from "./rhino-const-conflict.js"
45
import validRequirePath from "./valid-require-path.js"
56

67
const sfcc = {
78
rules: {
9+
"no-e4x-syntax": noE4xSyntax,
810
"prefer-const": preferConst,
911
"rhino-const-compat": rhinoConstCompat,
1012
"rhino-const-conflict": rhinoConstConflict,

src/plugins/sfcc/no-e4x-syntax.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Rule } from "eslint"
2+
3+
import { withSfccSettings } from "../_utils/sfcc-settings.js"
4+
5+
const noE4xSyntax: Rule.RuleModule = {
6+
meta: {
7+
type: "problem",
8+
docs: {
9+
description:
10+
"Disallow JSX/E4X-like syntax in SFCC JavaScript to avoid parser ambiguity and unsupported runtime patterns.",
11+
recommended: true,
12+
},
13+
schema: [],
14+
messages: {
15+
forbiddenSyntax:
16+
"JSX/E4X-like syntax is not allowed in SFCC JavaScript. Use plain JavaScript and SFCC XML APIs instead.",
17+
},
18+
},
19+
create: withSfccSettings((context) => {
20+
return {
21+
JSXElement(node: Rule.Node) {
22+
context.report({ node, messageId: "forbiddenSyntax" })
23+
},
24+
JSXFragment(node: Rule.Node) {
25+
context.report({ node, messageId: "forbiddenSyntax" })
26+
},
27+
}
28+
}),
29+
}
30+
31+
export default noE4xSyntax

src/rules/sfcc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Linter } from "eslint"
22

33
const sfcc: Linter.RulesRecord = {
4+
"sfcc/no-e4x-syntax": "error",
45
"sfcc/prefer-const": "error",
56
"sfcc/rhino-const-compat": "error",
67
"sfcc/rhino-const-conflict": "error",

tests/sfcc-no-e4x-syntax.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import tseslint from "@typescript-eslint/eslint-plugin"
2+
import { ESLint, type Linter } from "eslint"
3+
import { describe, expect, test } from "vite-plus/test"
4+
5+
import { createRecommendedConfig } from "../src/index.js"
6+
7+
const tsRecommended = tseslint.configs["flat/recommended"] as unknown as
8+
| Linter.Config
9+
| Linter.Config[]
10+
11+
const config: Linter.Config[] = [
12+
...(Array.isArray(tsRecommended) ? tsRecommended : [tsRecommended]),
13+
...createRecommendedConfig({
14+
files: ["**/*.js"],
15+
ignores: [],
16+
}),
17+
]
18+
19+
async function lint(code: string, filename = "cartridges/app_sfra/cartridge/controllers/Home.js") {
20+
const eslint = new ESLint({
21+
overrideConfigFile: true,
22+
overrideConfig: config,
23+
})
24+
25+
const results = await eslint.lintText(code, { filePath: filename })
26+
return results[0]?.messages || []
27+
}
28+
29+
describe("sfcc/no-e4x-syntax", () => {
30+
test("reports JSX/E4X-like syntax in JS files", async () => {
31+
const messages = await lint("const x = <a/>; module.exports = x")
32+
33+
expect(messages.some((m) => m.ruleId === "sfcc/no-e4x-syntax")).toBe(true)
34+
})
35+
36+
test("allows XML and XMLList constructor-style usage", async () => {
37+
const messages = await lint(`
38+
const xmlCtor = XML
39+
const xmlListCtor = XMLList
40+
module.exports = { xmlCtor, xmlListCtor }
41+
`)
42+
43+
expect(messages.some((m) => m.ruleId === "sfcc/no-e4x-syntax")).toBe(false)
44+
})
45+
46+
test("shows parsing error for default xml namespace declaration", async () => {
47+
const messages = await lint('default xml namespace = "urn:test"')
48+
49+
expect(messages.some((m) => m.fatal === true)).toBe(true)
50+
expect(messages.some((m) => m.ruleId === "sfcc/no-e4x-syntax")).toBe(false)
51+
})
52+
53+
test("keeps JSDoc-based typing flow in JS files", async () => {
54+
const messages = await lint(`
55+
/** @param {{ productID: string }} body */
56+
function buildContext(body) {
57+
return { productID: body.productID }
58+
}
59+
module.exports = buildContext({ productID: "123" })
60+
`)
61+
62+
expect(messages.some((m) => m.ruleId === "sfcc/no-e4x-syntax")).toBe(false)
63+
})
64+
})

0 commit comments

Comments
 (0)