@etchteam/eslint-config 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,8 @@
1
- ## <small>3.0.1 (2026-04-02)</small>
1
+ ## 3.1.0 (2026-04-02)
2
2
 
3
- * Merge pull request #550 from etchteam/dependabot/npm_and_yarn/eslint-plugin-storybook-10.3.4 ([6b5fa37](https://github.com/etchteam/eslint/commit/6b5fa37)), closes [#550](https://github.com/etchteam/eslint/issues/550)
4
- * fix: Bump eslint-plugin-storybook from 10.3.3 to 10.3.4 ([4df8b90](https://github.com/etchteam/eslint/commit/4df8b90))
3
+ * Merge pull request #549 from etchteam/webc-support ([9b27bb3](https://github.com/etchteam/eslint/commit/9b27bb3)), closes [#549](https://github.com/etchteam/eslint/issues/549)
4
+ * refactor: address sonarcloud issues in webc processor ([069302f](https://github.com/etchteam/eslint/commit/069302f))
5
+ * refactor: deduplicate block extraction logic in webc processor ([2f98a07](https://github.com/etchteam/eslint/commit/2f98a07))
6
+ * feat: add WebC template linting support ([af7aad0](https://github.com/etchteam/eslint/commit/af7aad0))
7
+ * feat: enable curly rule to require braces on all if statements ([8b410d2](https://github.com/etchteam/eslint/commit/8b410d2))
8
+ * style: remove eslint-disable comments from webc processor ([77f7635](https://github.com/etchteam/eslint/commit/77f7635))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etchteam/eslint-config",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Etch's standard eslint config",
5
5
  "type": "module",
6
6
  "main": "src/index.mjs",
@@ -16,6 +16,7 @@
16
16
  "./preact": "./src/configs/environments/preact.mjs",
17
17
  "./angular": "./src/configs/environments/angular.mjs",
18
18
  "./web-components": "./src/configs/environments/web-components.mjs",
19
+ "./webc": "./src/configs/environments/webc.mjs",
19
20
  "./nestjs": "./src/configs/environments/nestjs.mjs",
20
21
  "./package.json": "./package.json"
21
22
  },
package/src/base.mjs CHANGED
@@ -58,6 +58,7 @@ export default [
58
58
  ],
59
59
  'security/detect-object-injection': 'off',
60
60
  'spaced-comment': 'error',
61
+ curly: 'error',
61
62
  },
62
63
  settings: {
63
64
  'import/resolver': {
@@ -0,0 +1,17 @@
1
+ import base from '../../base.mjs';
2
+ import json from '../json.mjs';
3
+ import storybook from '../storybook.mjs';
4
+ import webc from '../webc.mjs';
5
+ import yaml from '../yaml.mjs';
6
+
7
+ /**
8
+ * WebC ESLint configuration.
9
+ * Includes: base + JSON + YAML + Storybook + WebC processor
10
+ *
11
+ * Usage:
12
+ * ```js
13
+ * import webc from '@etchteam/eslint-config/webc';
14
+ * export default webc;
15
+ * ```
16
+ */
17
+ export default [...base, ...json, ...yaml, ...storybook, ...webc];
@@ -0,0 +1,18 @@
1
+ import webcPlugin from '../plugins/webc-processor.mjs';
2
+
3
+ /**
4
+ * WebC template linting configuration.
5
+ * Provides a processor for extracting and linting inline JavaScript in .webc files.
6
+ *
7
+ * Usage:
8
+ * ```js
9
+ * import { webc } from '@etchteam/eslint-config';
10
+ * export default [...base, ...webc];
11
+ * ```
12
+ */
13
+ export default [
14
+ {
15
+ files: ['**/*.webc'],
16
+ processor: webcPlugin.processors.webc,
17
+ },
18
+ ];
package/src/index.mjs CHANGED
@@ -9,6 +9,7 @@ export { default as base } from './base.mjs';
9
9
  export { default as json } from './configs/json.mjs';
10
10
  export { default as react } from './configs/react.mjs';
11
11
  export { default as storybook } from './configs/storybook.mjs';
12
+ export { default as webc } from './configs/webc.mjs';
12
13
  export { default as yaml } from './configs/yaml.mjs';
13
14
 
14
15
  /**
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Block definitions for each type of inline JavaScript in WebC files.
3
+ * Each entry maps an opening tag pattern to its closing pattern and type resolver.
4
+ */
5
+ const BLOCK_DEFS = [
6
+ {
7
+ open: /^(\s*)<script(\s[^>]*)?\s*>/i,
8
+ close: /<\/script\s*>/i,
9
+ getType: (line) => (/webc:setup/.test(line) ? 'script-setup' : 'script'),
10
+ },
11
+ {
12
+ open: /^(\s*)<template\s+webc:type=["'](js|render)["'](\s[^>]*)?\s*>/i,
13
+ close: /<\/template\s*>/i,
14
+ getType: (_line, match) => `template-${match[2]}`,
15
+ },
16
+ ];
17
+
18
+ /** @type {Map<string, Array<{ startLine: number, charOffset: number, type: string }>>} */
19
+ const blockMetadata = new Map();
20
+
21
+ /**
22
+ * Try to match a line against all block definitions.
23
+ *
24
+ * @param {string} line
25
+ * @returns {{ match: RegExpMatchArray, close: RegExp, type: string } | null}
26
+ */
27
+ function matchOpenTag(line) {
28
+ for (const def of BLOCK_DEFS) {
29
+ const match = def.open.exec(line);
30
+
31
+ if (match) {
32
+ return { match, close: def.close, type: def.getType(line, match) };
33
+ }
34
+ }
35
+
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Handle a line that opens a new block. Returns the block if it's a
41
+ * single-line block, or the parser state for a multi-line block.
42
+ *
43
+ * @param {string} line
44
+ * @param {{ match: RegExpMatchArray, close: RegExp, type: string }} opened
45
+ * @param {number} lineIndex
46
+ * @param {number} currentCharOffset
47
+ * @returns {{ block: object } | { state: object }}
48
+ */
49
+ function handleOpenTag(line, opened, lineIndex, currentCharOffset) {
50
+ const tagEnd = line.indexOf('>', opened.match.index) + 1;
51
+ const closeMatch = opened.close.exec(line);
52
+
53
+ if (closeMatch && closeMatch.index > opened.match.index) {
54
+ return {
55
+ block: {
56
+ code: line.slice(tagEnd, closeMatch.index),
57
+ startLine: lineIndex + 1,
58
+ charOffset: currentCharOffset + tagEnd,
59
+ type: opened.type,
60
+ },
61
+ };
62
+ }
63
+
64
+ const afterTag = line.slice(tagEnd);
65
+ const hasContentOnSameLine = afterTag.trim();
66
+
67
+ return {
68
+ state: {
69
+ closePattern: opened.close,
70
+ blockType: opened.type,
71
+ blockLines: hasContentOnSameLine ? [afterTag] : [],
72
+ blockStartLine: hasContentOnSameLine ? lineIndex + 1 : lineIndex + 2,
73
+ blockCharOffset: hasContentOnSameLine
74
+ ? currentCharOffset + tagEnd
75
+ : currentCharOffset + line.length + 1,
76
+ },
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Handle a line while inside a block. Returns the completed block if the
82
+ * closing tag is found, or null to continue accumulating.
83
+ *
84
+ * @param {string} line
85
+ * @param {RegExp} closePattern
86
+ * @param {string[]} blockLines
87
+ * @param {number} blockStartLine
88
+ * @param {number} blockCharOffset
89
+ * @param {string} blockType
90
+ * @returns {{ code: string, startLine: number, charOffset: number, type: string } | null}
91
+ */
92
+ function handleBlockLine(
93
+ line,
94
+ closePattern,
95
+ blockLines,
96
+ blockStartLine,
97
+ blockCharOffset,
98
+ blockType,
99
+ ) {
100
+ const closeMatch = closePattern.exec(line);
101
+
102
+ if (!closeMatch) {
103
+ blockLines.push(line);
104
+ return null;
105
+ }
106
+
107
+ const beforeClose = line.slice(0, closeMatch.index);
108
+
109
+ if (beforeClose.trim()) {
110
+ blockLines.push(beforeClose);
111
+ }
112
+
113
+ const code = blockLines.join('\n');
114
+
115
+ if (!code.trim()) {
116
+ return null;
117
+ }
118
+
119
+ return {
120
+ code: code + '\n',
121
+ startLine: blockStartLine,
122
+ charOffset: blockCharOffset,
123
+ type: blockType,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Extract JavaScript blocks from a WebC file.
129
+ *
130
+ * @param {string} text - The full file content
131
+ * @returns {Array<{ code: string, startLine: number, charOffset: number, type: string }>}
132
+ */
133
+ function extractBlocks(text) {
134
+ const lines = text.split('\n');
135
+ const blocks = [];
136
+
137
+ let insideBlock = false;
138
+ let closePattern = null;
139
+ let blockType = '';
140
+ let blockLines = [];
141
+ let blockStartLine = 0;
142
+ let blockCharOffset = 0;
143
+ let currentCharOffset = 0;
144
+
145
+ for (let i = 0; i < lines.length; i++) {
146
+ const line = lines[i];
147
+
148
+ if (insideBlock) {
149
+ const completed = handleBlockLine(
150
+ line,
151
+ closePattern,
152
+ blockLines,
153
+ blockStartLine,
154
+ blockCharOffset,
155
+ blockType,
156
+ );
157
+
158
+ if (completed) {
159
+ blocks.push(completed);
160
+ insideBlock = false;
161
+ closePattern = null;
162
+ blockLines = [];
163
+ }
164
+ } else {
165
+ const opened = matchOpenTag(line);
166
+
167
+ if (opened) {
168
+ const result = handleOpenTag(line, opened, i, currentCharOffset);
169
+
170
+ if (result.block) {
171
+ blocks.push(result.block);
172
+ } else {
173
+ insideBlock = true;
174
+ closePattern = result.state.closePattern;
175
+ blockType = result.state.blockType;
176
+ blockLines = result.state.blockLines;
177
+ blockStartLine = result.state.blockStartLine;
178
+ blockCharOffset = result.state.blockCharOffset;
179
+ }
180
+ }
181
+ }
182
+
183
+ currentCharOffset += line.length + 1;
184
+ }
185
+
186
+ return blocks;
187
+ }
188
+
189
+ /**
190
+ * @param {string} text
191
+ * @param {string} filename
192
+ * @returns {Array<{ text: string, filename: string }>}
193
+ */
194
+ function preprocess(text, filename) {
195
+ const blocks = extractBlocks(text);
196
+ const metadata = [];
197
+ const result = [];
198
+
199
+ for (let i = 0; i < blocks.length; i++) {
200
+ const block = blocks[i];
201
+ const virtualFilename = `${filename}/${i}.${block.type}.js`;
202
+
203
+ metadata.push({
204
+ startLine: block.startLine,
205
+ charOffset: block.charOffset,
206
+ type: block.type,
207
+ });
208
+
209
+ result.push({
210
+ text: block.code,
211
+ filename: virtualFilename,
212
+ });
213
+ }
214
+
215
+ blockMetadata.set(filename, metadata);
216
+
217
+ return result;
218
+ }
219
+
220
+ /**
221
+ * @param {import('eslint').Linter.LintMessage} message
222
+ * @param {number} lineOffset
223
+ * @param {number} charOffset
224
+ * @returns {import('eslint').Linter.LintMessage}
225
+ */
226
+ function adjustMessage(message, lineOffset, charOffset) {
227
+ return {
228
+ ...message,
229
+ line: message.line + lineOffset,
230
+ endLine: message.endLine != null ? message.endLine + lineOffset : undefined,
231
+ fix: message.fix
232
+ ? {
233
+ range: [
234
+ message.fix.range[0] + charOffset,
235
+ message.fix.range[1] + charOffset,
236
+ ],
237
+ text: message.fix.text,
238
+ }
239
+ : undefined,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * @param {Array<Array<import('eslint').Linter.LintMessage>>} messages
245
+ * @param {string} filename
246
+ * @returns {Array<import('eslint').Linter.LintMessage>}
247
+ */
248
+ function postprocess(messages, filename) {
249
+ const metadata = blockMetadata.get(filename);
250
+
251
+ blockMetadata.delete(filename);
252
+
253
+ if (!metadata) {
254
+ return messages.flat();
255
+ }
256
+
257
+ const result = [];
258
+
259
+ for (let i = 0; i < messages.length; i++) {
260
+ const blockMessages = messages[i];
261
+ const meta = metadata[i];
262
+
263
+ if (meta) {
264
+ const lineOffset = meta.startLine - 1;
265
+
266
+ for (const message of blockMessages) {
267
+ result.push(adjustMessage(message, lineOffset, meta.charOffset));
268
+ }
269
+ } else {
270
+ result.push(...blockMessages);
271
+ }
272
+ }
273
+
274
+ return result;
275
+ }
276
+
277
+ export default {
278
+ meta: {
279
+ name: 'eslint-plugin-webc',
280
+ version: '1.0.0',
281
+ },
282
+ processors: {
283
+ webc: {
284
+ preprocess,
285
+ postprocess,
286
+ supportsAutofix: true,
287
+ },
288
+ },
289
+ };
package/src/types.d.ts CHANGED
@@ -52,6 +52,12 @@ export declare const preact: FlatConfigArray;
52
52
  */
53
53
  export declare const angular: FlatConfigArray;
54
54
 
55
+ /**
56
+ * WebC template linting configuration.
57
+ * Provides a processor for extracting and linting inline JavaScript in .webc files.
58
+ */
59
+ export declare const webc: FlatConfigArray;
60
+
55
61
  /**
56
62
  * Web Components (Lit) ESLint configuration.
57
63
  * Includes: base + JSON + YAML + Storybook + Lit rules