Skip to content

Commit 409927b

Browse files
authored
@W-17471447 Add a ssr processor and update ssr rule (#185)
1 parent f05586e commit 409927b

File tree

9 files changed

+569
-2
lines changed

9 files changed

+569
-2
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ For more details about configuration please refer to the dedicated section in th
6565

6666
To choose from three configuration settings, install the [`eslint-config-lwc`](https://github.com/salesforce/eslint-config-lwc) sharable configuration package.
6767

68+
### Processors
69+
70+
| Processor ID | Description |
71+
| ----------------------------------- | ------------------------------------------------- |
72+
| [lwc/ssr](./docs/processors/ssr.md) | Lint only JavaScript files of SSR-able components |
73+
6874
## Rules
6975

7076
### LWC

docs/processors/ssr.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# `ssr` Processor
2+
3+
## Overview
4+
5+
The `ssr` processor is designed to streamline the linting of SSR-capable Lightning Web Components (LWCs). By reading the metadata in the `js-meta.xml` file, the processor identifies whether a component supports Server-Side Rendering (SSR). For SSR-capable components, the processor generates virtual files in memory with a .ssrjs extension. These virtual files allow you to apply SSR-specific linting rules in a targeted manner.
6+
7+
## Key Features
8+
9+
- SSR Capability Detection: The processor identifies components with SSR capabilities based on the js-meta.xml file.
10+
11+
- Virtual File Creation: For each SSR-capable component, the processor creates corresponding .ssrjs virtual files.
12+
13+
- Targeted Linting: These .ssrjs files can be processed with SSR-specific ESLint rules.
14+
15+
### **Usage**
16+
17+
The processor helps to specifically target ssr js files to apply ssr specific rules by reading `js-meta.xml` files of components.
18+
19+
**Example:**
20+
21+
```xml
22+
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
23+
<apiVersion>50.0</apiVersion>
24+
<isExposed>true</isExposed>
25+
<targets>
26+
<target>lightning__RecordPage</target>
27+
<target>lightning__AppPage</target>
28+
<target>lightning__RecordPage</target>
29+
</targets>
30+
<capabilities>
31+
<capability>lightning__ServerRenderableWithHydration</capability> <!-- Indicate SSR capability here -->
32+
</capabilities>
33+
</LightningComponentBundle>
34+
```
35+
36+
In this example, the capabilities tag indicates whether the component supports SSR. If either of the following capabilities is defined:
37+
38+
- lightning\_\_ServerRenderable
39+
- lightning\_\_ServerRenderableWithHydration
40+
41+
The processor will generate a `${filename}.ssrjs` virtual file for each js file associated with the component.
42+
43+
### Configuration Example
44+
45+
To configure the processor, ensure your ESLint configuration includes @lwc/eslint-plugin-lwc/ssr as a processor:
46+
47+
Flat config:
48+
49+
```
50+
import lwcPlugin from '@lwc/eslint-plugin-lwc';
51+
export default [
52+
{
53+
files: ['**/modules/**/*.js'],
54+
languageOptions: {
55+
parser: babelParser,
56+
parserOptions: {
57+
ecmaVersion: 2021,
58+
sourceType: 'module',
59+
requireConfigFile: false,
60+
},
61+
},
62+
plugins: {
63+
lwcPlugin
64+
},
65+
processor: lwcPlugin.processors.ssr
66+
},
67+
{
68+
files: ['**/modules/**/*.ssrjs'],
69+
languageOptions: {
70+
parser: babelParser,
71+
parserOptions: {
72+
ecmaVersion: 2021,
73+
sourceType: 'module',
74+
requireConfigFile: false,
75+
},
76+
},
77+
plugins: {
78+
lwcPlugin
79+
},
80+
rules: {
81+
"no-console" : "error",
82+
"lwcPlugin/ssr-no-node-env": "error"
83+
}
84+
}
85+
];
86+
```
87+
88+
Non-flat config:
89+
90+
```
91+
module.exports = {
92+
overrides: [
93+
{
94+
files: ['**/modules/**/*.js'],
95+
parser: '@babel/eslint-parser',
96+
parserOptions: {
97+
ecmaVersion: 2021,
98+
sourceType: 'module',
99+
requireConfigFile: false,
100+
},
101+
plugins: ['@lwc/lwc'],
102+
processor: '@lwc/lwc/ssr'
103+
},
104+
{
105+
files: ['**/modules/**/*.ssrjs'],
106+
parser: '@babel/eslint-parser',
107+
parserOptions: {
108+
ecmaVersion: 2021,
109+
sourceType: 'module',
110+
requireConfigFile: false,
111+
},
112+
plugins: ['@lwc/lwc'],
113+
rules: {
114+
"no-console": "error",
115+
"@lwc/lwc/ssr-no-node-env": "error"
116+
}
117+
}
118+
]
119+
};
120+
```
121+
122+
### Explanation of Configuration
123+
124+
- Processor Configuration:
125+
126+
The processor key applies the ssr processor to all `**/modules/**/*.js` files. The processor then created a virtual files for all js files of ssrable components with `.ssrjs` extension/
127+
128+
- Targeted Linting for .ssrjs Files:
129+
130+
A separate configuration block applies specific rules to these virtual `.ssrjs` files, ensuring SSR-specific rules like `lwc/ssr-no-node-env` are only applied SSR capable components.

lib/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ const rules = {
4242
'ssr-no-form-factor': require('./rules/ssr/ssr-no-form-factor'),
4343
};
4444

45+
const processors = {
46+
ssr: require('./processors/ssr'),
47+
};
48+
4549
module.exports = {
50+
processors,
4651
rules,
4752
};

lib/processors/ssr.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2024, salesforce.com, inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: MIT
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
6+
*/
7+
'use strict';
8+
9+
const path = require('path');
10+
const fs = require('fs');
11+
const { XMLParser } = require('fast-xml-parser');
12+
13+
const SSR_CAPABILITIES = [
14+
'lightning__ServerRenderable',
15+
'lightning__ServerRenderableWithHydration',
16+
];
17+
18+
const metaCache = new Map(); // Cache to store processed directories and their SSR status
19+
20+
function hasSSRCapabilities(dir) {
21+
// Check if the result for this directory is cached
22+
if (metaCache.has(dir)) {
23+
return metaCache.get(dir);
24+
}
25+
26+
const JS_META_REGEX = /\.js-meta\.xml$/i; // Regular expression to match meta.xml files
27+
const metaFile = fs.readdirSync(dir).find((file) => JS_META_REGEX.test(file));
28+
29+
if (!metaFile) {
30+
metaCache.set(dir, false); // Cache the result for this directory
31+
return false;
32+
}
33+
34+
const metaFilePath = path.join(dir, metaFile);
35+
const content = fs.readFileSync(metaFilePath, 'utf8');
36+
let xmlRoot;
37+
try {
38+
xmlRoot = new XMLParser({
39+
isArray: (_name, jPath) => jPath === 'LightningComponentBundle.capabilities',
40+
}).parse(content);
41+
} catch (error) {
42+
// If XML parsing fails, return false to indicate no SSR capabilities
43+
console.warn(`Failed to parse XML for ${metaFilePath}: ${error.message}`);
44+
return false;
45+
}
46+
47+
const bundle = xmlRoot.LightningComponentBundle;
48+
const hasSSR =
49+
bundle && bundle.capabilities
50+
? bundle.capabilities.some((capabilityObj) =>
51+
Array.isArray(capabilityObj.capability)
52+
? capabilityObj.capability.some((cap) => SSR_CAPABILITIES.includes(cap))
53+
: SSR_CAPABILITIES.includes(capabilityObj.capability),
54+
)
55+
: false;
56+
57+
// Cache the result for future use
58+
metaCache.set(dir, hasSSR);
59+
return hasSSR;
60+
}
61+
62+
// processor to only lint js files for components marked as ssrable in their .js-meta.xml capabilities
63+
module.exports = {
64+
preprocess(text, filename) {
65+
// If the file already has the .ssrjs return same file
66+
if (filename.endsWith('.ssrjs')) {
67+
return [text];
68+
}
69+
const dirName = path.dirname(filename); // Extract the directory name from the full file path
70+
const baseFileName = path.basename(filename);
71+
const updatedFileName = baseFileName.replace(/(js|ts)$/, 'ssr$1');
72+
// Check SSR capabilities before processing
73+
if (!hasSSRCapabilities(dirName)) {
74+
return [text];
75+
}
76+
// Creates a copy file for every ssr component js file with `${filePath}/${fileName}.ssrjs` path
77+
return [text, { text, filename: updatedFileName }];
78+
},
79+
80+
postprocess(messages) {
81+
// Return all messages for files that were processed
82+
return messages.flat();
83+
},
84+
};

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@
2929
"mocha": "^10.4.0",
3030
"nyc": "^15.1.0",
3131
"prettier": "^3.2.5",
32-
"semver": "^7.6.0"
32+
"semver": "^7.6.0",
33+
"sinon": "^19.0.2",
34+
"chai": "4.3.7"
3335
},
3436
"dependencies": {
37+
"fast-xml-parser": "^4.5.1",
3538
"globals": "^13.24.0",
3639
"minimatch": "^9.0.4"
3740
},

test/index.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const plugin = require('../lib');
1414

1515
const RULES_FOLDER = path.resolve(__dirname, '../lib/rules');
1616
const SSR_RULES_FOLDER = path.resolve(RULES_FOLDER, 'ssr');
17+
const PROCESSORS_FOLDER = path.resolve(__dirname, '../lib/processors'); // Add processor folder path
1718

1819
// Utility function to get only files from a directory (excluding subdirectories)
1920
function getRuleFiles(dir) {
@@ -27,6 +28,8 @@ function getRuleFiles(dir) {
2728
const RULE_FILES = getRuleFiles(RULES_FOLDER);
2829
const SSR_RULE_FILES = getRuleFiles(SSR_RULES_FOLDER);
2930
const DOC_FILES = fs.readdirSync(path.resolve(__dirname, '../docs/rules'));
31+
const PROCESSOR_FILES = getRuleFiles(PROCESSORS_FOLDER);
32+
const PROCESSOR_DOC_FILES = fs.readdirSync(path.resolve(__dirname, '../docs/processors'));
3033
const SSR_DOC_FILES = fs.readdirSync(path.resolve(__dirname, '../docs/rules/ssr'));
3134
const README_CONTENT = fs.readFileSync(path.resolve(__dirname, '../README.md'), 'utf-8');
3235

@@ -119,3 +122,25 @@ describe('SSR rules documentation', () => {
119122
});
120123
});
121124
});
125+
126+
describe('processor documentation', () => {
127+
PROCESSOR_FILES.forEach((processorFile) => {
128+
const processorName = path.basename(processorFile, '.js');
129+
130+
it(`should have a documentation file for processor "${processorName}"`, () => {
131+
assert(
132+
PROCESSOR_DOC_FILES.includes(`${processorName}.md`),
133+
`No associated documentation for processor "${processorName}".`,
134+
);
135+
});
136+
137+
it(`should have an entry in README.md for processor "${processorName}"`, () => {
138+
assert(
139+
README_CONTENT.includes(
140+
`| [lwc/${processorName}](./docs/processors/${processorName}.md)`,
141+
),
142+
`Processor "${processorName}" is not listed in the README.md.`,
143+
);
144+
});
145+
});
146+
});

test/integration.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,24 @@ it('should resolve plugin rules', async () => {
5858
assert.equal(messages[1].ruleId, '@lwc/lwc/no-inner-html');
5959
assert.equal(messages[1].severity, 1);
6060
});
61+
62+
it('should resolve ssr processor', async () => {
63+
const cli = new eslint.ESLint({
64+
useEslintrc: false,
65+
overrideConfig: {
66+
plugins: ['@lwc/eslint-plugin-lwc'],
67+
processor: '@lwc/lwc/ssr',
68+
rules: {
69+
'@lwc/lwc/no-document-query': 'error',
70+
'@lwc/lwc/no-inner-html': 'warn',
71+
},
72+
},
73+
});
74+
75+
const results = await cli.lintText(`
76+
document.querySelectorAll("a").innerHTML = 'Hello'
77+
`);
78+
79+
const { messages } = results[0];
80+
assert.equal(messages.length, 2);
81+
});

0 commit comments

Comments
 (0)