Skip to content

Commit 034639c

Browse files
feat: support linting the html content dynamicly
1 parent 7229a95 commit 034639c

File tree

6 files changed

+236
-22
lines changed

6 files changed

+236
-22
lines changed

lib/lwc-bundle.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,16 @@ class LwcBundle {
123123
}
124124

125125
/**
126-
* Gets a unique key for this bundle based on its primary file
126+
* Gets a unique key for this bundle based on its js file since komaci only supports js files. This key is used as file name for further linting.
127127
*
128128
* @returns {string} A unique key in the format `<base file name>_<uuid>.<extension>`
129129
*/
130130
getBundleKey() {
131-
const primaryFile = this.primaryFile;
132-
if (!primaryFile) {
133-
throw new Error('Cannot generate bundle key: no primary file exists');
131+
if (!this.#js) {
132+
throw new Error('Cannot generate bundle key: no js file exists');
134133
}
135134

136-
const { name, ext } = parse(primaryFile.filename);
135+
const { name, ext } = parse(this.#js.filename);
137136
return `${name}_${this.#primaryFileUuidKey}${ext}`;
138137
}
139138

lib/processor.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,18 @@ class BundleAnalyzer {
124124

125125
setLwcBundleCacheEntry(this.#lwcBundle);
126126

127-
// Get the unique key for this bundle, which will be used as the filename for ESLint processing
127+
// Get the unique key for this bundle, which will be used as the filename for ESLint processing. It should always be a js file since komaci only supports js files.
128128
const uniqueFilename = this.#lwcBundle.getBundleKey();
129129

130130
// For JavaScript files, we need to return the original text so that ESLint can parse it
131131
// and properly handle disable comments. For all other file types (like HTML), we return
132-
// an empty string to prevent ESLint from attempting to parse them, since our rules
133-
// don't need the parsed AST for non-JS files.
132+
// the JS content from the bundle so that rules can run and report violations targeting
133+
// the HTML file.
134134
if (fileExtension === '.js') {
135135
return [{ text, filename: uniqueFilename }];
136136
} else {
137-
return [{ text: '', filename: uniqueFilename }];
137+
// Return JS content so ESLint can parse it and rules can run
138+
return [{ text: this.#lwcBundle.js?.content || '', filename: uniqueFilename }];
138139
}
139140
};
140141

test/lib/lwc-bundle.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,10 @@ describe('LwcBundle', () => {
191191
expect(key).to.match(/^test_[0-9a-f-]+\.js$/);
192192
});
193193

194-
it('should throw error when no primary file exists', () => {
195-
const bundle = LwcBundle.lwcBundleFromContent('test', 'js content', 'html content');
194+
it('should throw error when no js file exists', () => {
195+
const bundle = LwcBundle.lwcBundleFromContent('test', undefined, 'html content');
196196
expect(() => bundle.getBundleKey()).to.throw(
197-
'Cannot generate bundle key: no primary file exists'
197+
'Cannot generate bundle key: no js file exists'
198198
);
199199
});
200200
});

test/lib/processor.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,14 @@ describe('BundleAnalyzer', () => {
6565
expect(result[0].filename).to.equal(bundleAnalyzer.lwcBundle.getBundleKey());
6666
});
6767

68-
it('should process html file with existing bundle', () => {
68+
it('should throw error when preprocessing html file with existing bundle that has no js file', () => {
6969
const htmlContent = '<template></template>';
7070
bundleAnalyzer.setLwcBundleFromContent('test', undefined, htmlContent);
71-
bundleAnalyzer.lwcBundle.setPrimaryFileByContent(htmlContent);
7271

73-
const result = bundleAnalyzer.preprocess(htmlContent, 'test.html');
74-
expect(result).to.have.length(1);
75-
expect(result[0].text).to.equal('');
76-
expect(result[0].filename).to.equal(bundleAnalyzer.lwcBundle.getBundleKey());
72+
expect(() => bundleAnalyzer.preprocess(htmlContent, 'test.html')).to.throw(
73+
Error,
74+
'Cannot generate bundle key: no js file exists'
75+
);
7776
});
7877

7978
it('should return empty array when no matching file found in bundle', () => {
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Copyright (c) 2025, 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+
8+
const { readFileSync } = require('fs');
9+
const { join } = require('path');
10+
const { Linter } = require('eslint');
11+
const lwcGraphAnalyzer = require('../../../lib/index');
12+
const bundleAnalyzer = require('../../../lib/processor');
13+
const baseConfig = require('../../../lib/configs/base');
14+
15+
/**
16+
* Test to verify programmatic linting of both HTML and JS content at runtime
17+
* using setLwcBundleFromContent() approach
18+
*/
19+
describe('Programmatic Bundle Linting', () => {
20+
it('should lint both HTML and JS files with runtime content', () => {
21+
// Create JS content with a getter that violates the rule
22+
// The getter has more than just a return statement AND is referenced in the template
23+
const jsContent = `
24+
import { LightningElement, track } from 'lwc';
25+
26+
export default class TestComponent extends LightningElement {
27+
data = [];
28+
29+
@track
30+
descriptionValue = '';
31+
32+
get displayValue() {
33+
const value = this.data[0];
34+
if (value) {
35+
return value.name;
36+
}
37+
return 'N/A';
38+
}
39+
40+
handleDescriptionInputChange(event) {
41+
this.descriptionValue = event.detail.value;
42+
}
43+
44+
}`;
45+
46+
// HTML content that references the getter
47+
const htmlContent = `<template>
48+
<div>{displayValue}</div>
49+
<lightning-input
50+
type="text"
51+
label="Description"
52+
value={descriptionValue}
53+
onchange={handleDescriptionInputChange}
54+
></lightning-input>
55+
</template>`;
56+
57+
// 1. Set up the bundle with all content
58+
bundleAnalyzer.setLwcBundleFromContent(
59+
'testComponent', // component base name
60+
jsContent, // JS content
61+
htmlContent // HTML content
62+
);
63+
64+
// 2. Configure ESLint with the rule that has violations in both files
65+
const pluginPrefix = '@salesforce/lwc-graph-analyzer';
66+
const config = {
67+
...baseConfig,
68+
plugins: { [pluginPrefix]: lwcGraphAnalyzer },
69+
rules: {
70+
[`${pluginPrefix}/no-getter-contains-more-than-return-statement`]: 'error',
71+
[`${pluginPrefix}/no-composition-on-unanalyzable-property-non-public`]: 'error'
72+
}
73+
};
74+
75+
const linter = new Linter();
76+
77+
// 3. Lint JS file
78+
const jsErrors = linter.verify(jsContent, config, {
79+
filename: 'testComponent.js'
80+
});
81+
82+
// 4. Set up the bundle with all content again since the bundle is cleared after each lint call
83+
bundleAnalyzer.setLwcBundleFromContent(
84+
'testComponent', // component base name
85+
jsContent, // JS content
86+
htmlContent // HTML content
87+
);
88+
89+
// 5. Lint HTML file
90+
const htmlErrors = linter.verify(htmlContent, config, {
91+
filename: 'testComponent.html'
92+
});
93+
94+
// Verify we got violations for the JS file
95+
// The displayValue getter violates the rule because it's referenced in HTML
96+
expect(jsErrors.length).toBeGreaterThan(0);
97+
const jsViolation = jsErrors.find(
98+
(err) =>
99+
err.ruleId ===
100+
'@salesforce/lwc-graph-analyzer/no-getter-contains-more-than-return-statement'
101+
);
102+
expect(jsViolation).toBeDefined();
103+
expect(jsViolation.ruleId).toBe(
104+
'@salesforce/lwc-graph-analyzer/no-getter-contains-more-than-return-statement'
105+
);
106+
expect(jsViolation.message).toBe('Getters can only contain a return statement.');
107+
expect(jsViolation.line).toBeGreaterThan(0);
108+
109+
expect(htmlErrors.length).toBe(1);
110+
const htmlViolation = htmlErrors[0];
111+
expect(htmlViolation).toBeDefined();
112+
expect(htmlViolation.ruleId).toBe(
113+
'@salesforce/lwc-graph-analyzer/no-composition-on-unanalyzable-property-non-public'
114+
);
115+
expect(htmlViolation.message).toBe(
116+
"This child component references an unanalyzable property 'descriptionValue' that’s not a public property."
117+
);
118+
expect(htmlViolation.line).toBeGreaterThan(0);
119+
});
120+
121+
it('should handle multiple HTML templates', () => {
122+
const jsContent = `
123+
import { LightningElement } from 'lwc';
124+
export default class MyComponent extends LightningElement {
125+
get value() {
126+
const x = 1;
127+
return x;
128+
}
129+
}`;
130+
131+
const htmlContent1 = `<template><div>{value}</div></template>`;
132+
const htmlContent2 = `<template><div>Second template</div></template>`;
133+
134+
// Set up bundle with multiple HTML templates
135+
bundleAnalyzer.setLwcBundleFromContent(
136+
'myComponent',
137+
jsContent,
138+
htmlContent1,
139+
htmlContent2
140+
);
141+
142+
const pluginPrefix = '@salesforce/lwc-graph-analyzer';
143+
const config = {
144+
...baseConfig,
145+
plugins: { [pluginPrefix]: lwcGraphAnalyzer },
146+
rules: {
147+
[`${pluginPrefix}/no-getter-contains-more-than-return-statement`]: 'error'
148+
}
149+
};
150+
151+
const linter = new Linter();
152+
153+
// Lint JS file
154+
const jsErrors = linter.verify(jsContent, config, {
155+
filename: 'myComponent.js'
156+
});
157+
158+
// Should have violation because getter is referenced in HTML
159+
expect(jsErrors.length).toBeGreaterThan(0);
160+
const violation = jsErrors.find(
161+
(err) =>
162+
err.ruleId ===
163+
'@salesforce/lwc-graph-analyzer/no-getter-contains-more-than-return-statement'
164+
);
165+
expect(violation).toBeDefined();
166+
});
167+
168+
it('should return empty violations when no errors exist', () => {
169+
const jsContent = `
170+
import { LightningElement } from 'lwc';
171+
export default class MyComponent extends LightningElement {
172+
value = 'test';
173+
}`;
174+
175+
const htmlContent = `<template><div>{value}</div></template>`;
176+
177+
bundleAnalyzer.setLwcBundleFromContent('myComponent', jsContent, htmlContent);
178+
179+
const pluginPrefix = '@salesforce/lwc-graph-analyzer';
180+
const config = {
181+
...baseConfig,
182+
plugins: { [pluginPrefix]: lwcGraphAnalyzer },
183+
rules: {
184+
[`${pluginPrefix}/no-getter-contains-more-than-return-statement`]: 'error'
185+
}
186+
};
187+
188+
const linter = new Linter();
189+
190+
const jsErrors = linter.verify(jsContent, config, {
191+
filename: 'myComponent.js'
192+
});
193+
194+
const htmlErrors = linter.verify(htmlContent, config, {
195+
filename: 'myComponent.html'
196+
});
197+
198+
// No violations expected since we're using a simple property, not a getter
199+
const jsViolations = jsErrors.filter(
200+
(err) =>
201+
err.ruleId ===
202+
'@salesforce/lwc-graph-analyzer/no-getter-contains-more-than-return-statement'
203+
);
204+
const htmlViolations = htmlErrors.filter(
205+
(err) =>
206+
err.ruleId ===
207+
'@salesforce/lwc-graph-analyzer/no-getter-contains-more-than-return-statement'
208+
);
209+
210+
expect(jsViolations.length).toBe(0);
211+
expect(htmlViolations.length).toBe(0);
212+
});
213+
});

test/lib/util/bundle-state-manager.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,20 @@ describe('BundleStateManager', () => {
2727

2828
it('should add a bundle with html file as primary', () => {
2929
const htmlContent = '<template></template>';
30-
const bundle = LwcBundle.lwcBundleFromContent('test', undefined, htmlContent);
30+
const jsContent = 'export default class Test {}';
31+
const bundle = LwcBundle.lwcBundleFromContent('test', jsContent, htmlContent);
3132
bundle.setPrimaryFileByContent(htmlContent);
3233

3334
const key = bundleStateManager.addBundleState(bundle);
3435
expect(key).to.equal(bundle.getBundleKey());
3536
expect(bundleStateManager.getBundleByKey(bundle.getBundleKey())).to.equal(bundle);
3637
});
3738

38-
it('should return undefined when bundle has no primary file', () => {
39-
const bundle = LwcBundle.lwcBundleFromContent('test', 'js content', 'html content');
39+
it('should return defined key when bundle has js file', () => {
40+
const jsContent = 'export default class Test {}';
41+
const bundle = LwcBundle.lwcBundleFromContent('test', jsContent);
4042
const key = bundleStateManager.addBundleState(bundle);
41-
expect(key).to.be.undefined;
43+
expect(key).to.not.be.undefined;
4244
});
4345
});
4446

0 commit comments

Comments
 (0)