@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.48 → 2.0.0-next.49
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/.storybook/ComponentPreview.tsx +26 -0
- package/.storybook/PreviewTemplate.tsx +99 -0
- package/.storybook/action-handler.ts +81 -0
- package/.storybook/channels.ts +44 -0
- package/.storybook/main.ts +313 -0
- package/.storybook/manager.ts +71 -0
- package/.storybook/openbridgeTheme.ts +197 -0
- package/.storybook/preview-head.html +15 -0
- package/.storybook/preview.tsx +129 -0
- package/.storybook/vitest.setup.ts +23 -0
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +7 -1
- package/dist/components/icon-button/icon-button.d.ts +4 -0
- package/dist/components/icon-button/icon-button.d.ts.map +1 -1
- package/dist/components/icon-button/icon-button.js.map +1 -1
- package/eslint.config.mjs +589 -0
- package/fix-imports.mjs +185 -0
- package/fix-js-extensions.mjs +44 -0
- package/lit-localize.json +15 -0
- package/new-component.ts +148 -0
- package/package.json +55 -3
- package/postcss.config.mjs +195 -0
- package/script/check-css-mixins.ts +191 -0
- package/script/check-css-variables.ts +280 -0
- package/script/convert-icons.ts +202 -0
- package/script/convert-vessel-svg-to-ts.ts +110 -0
- package/script/docgen/README.md +95 -0
- package/script/docgen/docs-gen.ts +225 -0
- package/script/docgen/prompt-system.txt +187 -0
- package/script/download-alert-icons.ts +193 -0
- package/script/download-icons.ts +314 -0
- package/script/figmavariables.json +139 -0
- package/script/generate-bundle-entry.ts +77 -0
- package/script/prepare-full-bundle.ts +105 -0
- package/script/sort-custom-element-manifest.ts +67 -0
- package/src/components/icon-button/icon-button.ts +4 -0
- package/vite.config.ts +107 -0
- package/vitest.browser.config.ts +14 -0
- package/vitest.config.ts +51 -0
- package/xliff/es-419.xlf +111 -0
- package/xliff/fi-FI.xlf +139 -0
- package/dist/NotoSans.ttf +0 -0
- package/dist/oicl.svg +0 -4
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
|
2
|
+
import globals from 'globals';
|
|
3
|
+
import tsParser from '@typescript-eslint/parser';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import js from '@eslint/js';
|
|
7
|
+
import {FlatCompat} from '@eslint/eslintrc';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const compat = new FlatCompat({
|
|
12
|
+
baseDirectory: __dirname,
|
|
13
|
+
recommendedConfig: js.configs.recommended,
|
|
14
|
+
allConfig: js.configs.all,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Custom plugin for handling customElement imports
|
|
18
|
+
const customElementPlugin = {
|
|
19
|
+
rules: {
|
|
20
|
+
'prefer-local-decorator': {
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'suggestion',
|
|
23
|
+
docs: {
|
|
24
|
+
description:
|
|
25
|
+
'Ensure customElement is imported from local decorator.js',
|
|
26
|
+
category: 'Best Practices',
|
|
27
|
+
recommended: true,
|
|
28
|
+
},
|
|
29
|
+
fixable: 'code',
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
return {
|
|
33
|
+
ImportDeclaration(node) {
|
|
34
|
+
if (node.source.value === 'lit/decorators.js') {
|
|
35
|
+
const specifiers = node.specifiers;
|
|
36
|
+
const customElementSpec = specifiers.find(
|
|
37
|
+
(spec) =>
|
|
38
|
+
spec.type === 'ImportSpecifier' &&
|
|
39
|
+
spec.imported.name === 'customElement'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (customElementSpec) {
|
|
43
|
+
const otherSpecs = specifiers.filter(
|
|
44
|
+
(spec) =>
|
|
45
|
+
spec.type === 'ImportSpecifier' &&
|
|
46
|
+
spec.imported.name !== 'customElement'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
context.report({
|
|
50
|
+
node,
|
|
51
|
+
message:
|
|
52
|
+
'customElement should be imported from local src/decorator.js',
|
|
53
|
+
fix(fixer) {
|
|
54
|
+
const fixes = [];
|
|
55
|
+
const sourceFile = context.filename;
|
|
56
|
+
const relativePathToDecorator = path
|
|
57
|
+
.relative(
|
|
58
|
+
path.dirname(sourceFile),
|
|
59
|
+
path.join(path.dirname(sourceFile), 'decorator.js')
|
|
60
|
+
)
|
|
61
|
+
.replace(/\\/g, '/');
|
|
62
|
+
|
|
63
|
+
// Remove the old import
|
|
64
|
+
fixes.push(fixer.remove(node));
|
|
65
|
+
|
|
66
|
+
// Add the new customElement import
|
|
67
|
+
fixes.push(
|
|
68
|
+
fixer.insertTextBefore(
|
|
69
|
+
node,
|
|
70
|
+
`import { customElement } from '${relativePathToDecorator}';\n`
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Add back other imports if any
|
|
75
|
+
if (otherSpecs.length > 0) {
|
|
76
|
+
const names = otherSpecs
|
|
77
|
+
.map((spec) => spec.imported.name)
|
|
78
|
+
.join(', ');
|
|
79
|
+
fixes.push(
|
|
80
|
+
fixer.insertTextBefore(
|
|
81
|
+
node,
|
|
82
|
+
`import { ${names} } from 'lit/decorators.js';\n`
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return fixes;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Words that should stay lowercase in Title Case (unless they are the first word of a segment)
|
|
100
|
+
const TITLE_CASE_LOWERCASE_WORDS = new Set([
|
|
101
|
+
'and',
|
|
102
|
+
'of',
|
|
103
|
+
'or',
|
|
104
|
+
'in',
|
|
105
|
+
'on',
|
|
106
|
+
'at',
|
|
107
|
+
'to',
|
|
108
|
+
'for',
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
function capitalizeWord(word) {
|
|
112
|
+
if (word.length === 0) return word;
|
|
113
|
+
// All-uppercase words (acronyms like POI, AR, ROT) are kept as-is
|
|
114
|
+
if (word === word.toUpperCase() && word.length > 1) return word;
|
|
115
|
+
// Capitalize each part of a hyphenated compound (e.g. Action-change → Action-Change)
|
|
116
|
+
if (word.includes('-')) {
|
|
117
|
+
return word
|
|
118
|
+
.split('-')
|
|
119
|
+
.map((part) => capitalizeWord(part))
|
|
120
|
+
.join('-');
|
|
121
|
+
}
|
|
122
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function titleCaseWords(text) {
|
|
126
|
+
const words = text.split(/\s+/);
|
|
127
|
+
const lastIndex = words.length - 1;
|
|
128
|
+
return words
|
|
129
|
+
.map((word, i) => {
|
|
130
|
+
if (word.length === 0) return word;
|
|
131
|
+
// First and last words are always capitalized
|
|
132
|
+
if (i === 0 || i === lastIndex) return capitalizeWord(word);
|
|
133
|
+
// Short conjunctions / prepositions stay lowercase (only unhyphenated)
|
|
134
|
+
if (TITLE_CASE_LOWERCASE_WORDS.has(word.toLowerCase())) {
|
|
135
|
+
return word.toLowerCase();
|
|
136
|
+
}
|
|
137
|
+
// Everything else is capitalized
|
|
138
|
+
return capitalizeWord(word);
|
|
139
|
+
})
|
|
140
|
+
.join(' ');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Separator pattern: em dash, en dash, or spaced hyphen used as phrase dividers
|
|
144
|
+
const PHRASE_SEPARATOR = /(\s*[\u2013\u2014]\s*|\s+-\s+)/;
|
|
145
|
+
|
|
146
|
+
function toTitleCase(segment) {
|
|
147
|
+
// Split into parts outside and inside parentheses; only title-case outside
|
|
148
|
+
return segment.replace(/[^()]+/g, (part, offset) => {
|
|
149
|
+
// Check if this part is inside parentheses by looking at the character before it
|
|
150
|
+
if (offset > 0 && segment[offset - 1] === '(') return part;
|
|
151
|
+
// Split on phrase separators (em/en dash, spaced hyphen) so each phrase
|
|
152
|
+
// gets independent title-casing (first word always capitalized)
|
|
153
|
+
return part
|
|
154
|
+
.split(PHRASE_SEPARATOR)
|
|
155
|
+
.map((chunk) =>
|
|
156
|
+
PHRASE_SEPARATOR.test(chunk) ? chunk : titleCaseWords(chunk)
|
|
157
|
+
)
|
|
158
|
+
.join('');
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Custom plugin for local OpenBridge lint rules
|
|
163
|
+
const openbridgePlugin = {
|
|
164
|
+
rules: {
|
|
165
|
+
'storybook-title-case': {
|
|
166
|
+
meta: {
|
|
167
|
+
type: 'problem',
|
|
168
|
+
docs: {
|
|
169
|
+
description:
|
|
170
|
+
'Enforce Title Case on Storybook meta `title` (with `/`-separated segments) and story `name` fields',
|
|
171
|
+
},
|
|
172
|
+
fixable: 'code',
|
|
173
|
+
},
|
|
174
|
+
create(context) {
|
|
175
|
+
function checkTitleCase(node, value, isSegmented) {
|
|
176
|
+
if (isSegmented) {
|
|
177
|
+
const segments = value.split('/');
|
|
178
|
+
const problems = [];
|
|
179
|
+
|
|
180
|
+
for (const [i, segment] of segments.entries()) {
|
|
181
|
+
const trimmed = segment.trim();
|
|
182
|
+
if (trimmed.length === 0) {
|
|
183
|
+
problems.push({index: i, segment, issue: 'empty segment'});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (/(?<=[a-zA-Z])-(?=[a-zA-Z])/.test(trimmed)) {
|
|
187
|
+
problems.push({
|
|
188
|
+
index: i,
|
|
189
|
+
segment: trimmed,
|
|
190
|
+
issue: 'use spaces instead of dashes',
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const expected = toTitleCase(trimmed);
|
|
195
|
+
if (trimmed !== expected) {
|
|
196
|
+
problems.push({
|
|
197
|
+
index: i,
|
|
198
|
+
segment: trimmed,
|
|
199
|
+
expected,
|
|
200
|
+
issue: `should be "${expected}"`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (problems.length === 0) return;
|
|
206
|
+
|
|
207
|
+
const fixedTitle = segments
|
|
208
|
+
.map((s) => {
|
|
209
|
+
const trimmed = s.trim();
|
|
210
|
+
const spacedOut = trimmed.replace(
|
|
211
|
+
/(?<=[a-zA-Z])-(?=[a-zA-Z])/g,
|
|
212
|
+
' '
|
|
213
|
+
);
|
|
214
|
+
return toTitleCase(spacedOut);
|
|
215
|
+
})
|
|
216
|
+
.join('/');
|
|
217
|
+
|
|
218
|
+
context.report({
|
|
219
|
+
node,
|
|
220
|
+
message: `Storybook title segments must use Title Case with spaces (not dashes). ${problems.map((p) => `Segment "${p.segment}": ${p.issue}`).join('; ')}.`,
|
|
221
|
+
fix(fixer) {
|
|
222
|
+
return fixer.replaceText(node, `'${fixedTitle}'`);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
const expected = toTitleCase(value);
|
|
227
|
+
if (value === expected) return;
|
|
228
|
+
|
|
229
|
+
context.report({
|
|
230
|
+
node,
|
|
231
|
+
message: `Storybook story name must use Title Case. Expected "${expected}".`,
|
|
232
|
+
fix(fixer) {
|
|
233
|
+
return fixer.replaceText(node, `'${expected}'`);
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
Property(node) {
|
|
241
|
+
if (node.key.type !== 'Identifier') return;
|
|
242
|
+
if (
|
|
243
|
+
node.value.type !== 'Literal' ||
|
|
244
|
+
typeof node.value.value !== 'string'
|
|
245
|
+
)
|
|
246
|
+
return;
|
|
247
|
+
|
|
248
|
+
const propName = node.key.name;
|
|
249
|
+
|
|
250
|
+
if (propName === 'title') {
|
|
251
|
+
const value = node.value.value;
|
|
252
|
+
if (!value.includes('/')) return;
|
|
253
|
+
checkTitleCase(node.value, value, true);
|
|
254
|
+
} else if (propName === 'name') {
|
|
255
|
+
// Only match story-level `name` (Property → ObjectExpression → VariableDeclarator)
|
|
256
|
+
// to avoid false positives on argTypes/args/controls nested `name` fields
|
|
257
|
+
const parent = node.parent;
|
|
258
|
+
if (
|
|
259
|
+
!parent ||
|
|
260
|
+
parent.type !== 'ObjectExpression' ||
|
|
261
|
+
!parent.parent ||
|
|
262
|
+
parent.parent.type !== 'VariableDeclarator'
|
|
263
|
+
)
|
|
264
|
+
return;
|
|
265
|
+
checkTitleCase(node.value, node.value.value, false);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
'prefer-boolean-property-default-false': {
|
|
272
|
+
meta: {
|
|
273
|
+
type: 'problem',
|
|
274
|
+
docs: {
|
|
275
|
+
description:
|
|
276
|
+
'Prefer boolean property default values of false or set Attribute: false on the property decorator',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
create(context) {
|
|
280
|
+
return {
|
|
281
|
+
PropertyDefinition(node) {
|
|
282
|
+
if (node.type !== 'PropertyDefinition') return;
|
|
283
|
+
if (node.value === undefined || node.value === null) return;
|
|
284
|
+
if (node.value.type !== 'Literal') return;
|
|
285
|
+
if (node.value.value !== true) return;
|
|
286
|
+
// Only apply to @property()-decorated fields (plain class fields are exempt)
|
|
287
|
+
const propertyDecorator = node.decorators?.find(
|
|
288
|
+
(d) =>
|
|
289
|
+
d.expression?.type === 'CallExpression' &&
|
|
290
|
+
d.expression.callee?.type === 'Identifier' &&
|
|
291
|
+
d.expression.callee.name === 'property'
|
|
292
|
+
);
|
|
293
|
+
if (!propertyDecorator) return;
|
|
294
|
+
// Check if the property decorator has attribute: false — that's the
|
|
295
|
+
// accepted escape hatch for true-default booleans (see AGENTS.md § 2).
|
|
296
|
+
const property =
|
|
297
|
+
propertyDecorator.expression?.arguments[0]?.properties?.find(
|
|
298
|
+
(p) =>
|
|
299
|
+
p.type === 'Property' &&
|
|
300
|
+
!p.computed &&
|
|
301
|
+
p.key.type === 'Identifier' &&
|
|
302
|
+
p.key.name === 'attribute'
|
|
303
|
+
);
|
|
304
|
+
const isBuildingBlock =
|
|
305
|
+
typeof context.filename === 'string' &&
|
|
306
|
+
context.filename.includes('building-blocks');
|
|
307
|
+
if (property?.value?.value === false) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
let message = 'Prefer boolean property default values of false';
|
|
311
|
+
if (isBuildingBlock) {
|
|
312
|
+
message += ' or set Attribute: false on the property decorator';
|
|
313
|
+
}
|
|
314
|
+
context.report({
|
|
315
|
+
node,
|
|
316
|
+
message,
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
'prefer-enum-over-string-literal-union': {
|
|
323
|
+
meta: {
|
|
324
|
+
type: 'problem',
|
|
325
|
+
docs: {
|
|
326
|
+
description:
|
|
327
|
+
'Disallow string-literal unions on class fields (prefer enums instead)',
|
|
328
|
+
},
|
|
329
|
+
schema: [],
|
|
330
|
+
},
|
|
331
|
+
create(context) {
|
|
332
|
+
function isStringLiteralUnion(typeNode) {
|
|
333
|
+
if (!typeNode || typeNode.type !== 'TSUnionType') return false;
|
|
334
|
+
if (typeNode.types.length < 2) return false;
|
|
335
|
+
|
|
336
|
+
return typeNode.types.every(
|
|
337
|
+
(t) =>
|
|
338
|
+
t.type === 'TSLiteralType' &&
|
|
339
|
+
t.literal &&
|
|
340
|
+
t.literal.type === 'Literal' &&
|
|
341
|
+
typeof t.literal.value === 'string'
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isStringLiteralInitializer(valueNode) {
|
|
346
|
+
return (
|
|
347
|
+
valueNode &&
|
|
348
|
+
valueNode.type === 'Literal' &&
|
|
349
|
+
typeof valueNode.value === 'string'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
// Type aliases: `export type Foo = 'a' | 'b'`
|
|
355
|
+
TSTypeAliasDeclaration(node) {
|
|
356
|
+
if (!isStringLiteralUnion(node.typeAnnotation)) return;
|
|
357
|
+
|
|
358
|
+
context.report({
|
|
359
|
+
node,
|
|
360
|
+
message: 'Avoid string-literal union types; use an enum instead.',
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
// Class fields: `foo: 'a' | 'b' = 'a'`
|
|
365
|
+
PropertyDefinition(node) {
|
|
366
|
+
// Limit scope to Lit properties declared as `@property({type: String})`
|
|
367
|
+
const hasLitStringPropertyDecorator = (node.decorators ?? []).some(
|
|
368
|
+
(decorator) => {
|
|
369
|
+
const expr = decorator.expression;
|
|
370
|
+
if (!expr || expr.type !== 'CallExpression') return false;
|
|
371
|
+
if (!expr.callee || expr.callee.type !== 'Identifier')
|
|
372
|
+
return false;
|
|
373
|
+
if (expr.callee.name !== 'property') return false;
|
|
374
|
+
|
|
375
|
+
const arg0 = expr.arguments?.[0];
|
|
376
|
+
if (!arg0 || arg0.type !== 'ObjectExpression') return false;
|
|
377
|
+
|
|
378
|
+
const typeProp = arg0.properties.find((p) => {
|
|
379
|
+
if (!p || p.type !== 'Property') return false;
|
|
380
|
+
if (p.key.type !== 'Identifier') return false;
|
|
381
|
+
if (p.key.name !== 'type') return false;
|
|
382
|
+
return (
|
|
383
|
+
p.value &&
|
|
384
|
+
p.value.type === 'Identifier' &&
|
|
385
|
+
p.value.name === 'String'
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return Boolean(typeProp);
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
if (!hasLitStringPropertyDecorator) return;
|
|
393
|
+
|
|
394
|
+
const typeAnn = node.typeAnnotation?.typeAnnotation;
|
|
395
|
+
if (!isStringLiteralUnion(typeAnn)) return;
|
|
396
|
+
if (!isStringLiteralInitializer(node.value)) return;
|
|
397
|
+
|
|
398
|
+
context.report({
|
|
399
|
+
node,
|
|
400
|
+
message:
|
|
401
|
+
'Avoid string-literal unions in `@property({type: String})` fields; use an enum type instead.',
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
'prefer-array-property-type-and-item-interface': {
|
|
408
|
+
meta: {
|
|
409
|
+
type: 'problem',
|
|
410
|
+
docs: {
|
|
411
|
+
description:
|
|
412
|
+
'Require @property array fields to declare {type: Array} and avoid inline item object types',
|
|
413
|
+
},
|
|
414
|
+
schema: [],
|
|
415
|
+
},
|
|
416
|
+
create(context) {
|
|
417
|
+
function getPropertyDecorator(node) {
|
|
418
|
+
return (node.decorators ?? []).find((decorator) => {
|
|
419
|
+
const expr = decorator.expression;
|
|
420
|
+
return (
|
|
421
|
+
expr &&
|
|
422
|
+
expr.type === 'CallExpression' &&
|
|
423
|
+
expr.callee &&
|
|
424
|
+
expr.callee.type === 'Identifier' &&
|
|
425
|
+
expr.callee.name === 'property'
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function getArrayElementType(typeNode) {
|
|
431
|
+
if (!typeNode) return null;
|
|
432
|
+
if (typeNode.type === 'TSArrayType') {
|
|
433
|
+
return typeNode.elementType;
|
|
434
|
+
}
|
|
435
|
+
if (
|
|
436
|
+
typeNode.type === 'TSTypeReference' &&
|
|
437
|
+
typeNode.typeName &&
|
|
438
|
+
typeNode.typeName.type === 'Identifier' &&
|
|
439
|
+
typeNode.typeName.name === 'Array'
|
|
440
|
+
) {
|
|
441
|
+
return typeNode.typeArguments?.params?.[0] ?? null;
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function hasTypeArrayOption(propertyDecorator) {
|
|
447
|
+
const options = propertyDecorator.expression.arguments?.[0];
|
|
448
|
+
if (!options || options.type !== 'ObjectExpression') return false;
|
|
449
|
+
return options.properties.some((prop) => {
|
|
450
|
+
if (!prop || prop.type !== 'Property') return false;
|
|
451
|
+
if (prop.key.type !== 'Identifier' || prop.key.name !== 'type')
|
|
452
|
+
return false;
|
|
453
|
+
return (
|
|
454
|
+
prop.value &&
|
|
455
|
+
prop.value.type === 'Identifier' &&
|
|
456
|
+
prop.value.name === 'Array'
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function containsInlineObjectType(typeNode) {
|
|
462
|
+
if (!typeNode) return false;
|
|
463
|
+
if (typeNode.type === 'TSTypeLiteral') return true;
|
|
464
|
+
if (typeNode.type === 'TSUnionType') {
|
|
465
|
+
return typeNode.types.some((t) => containsInlineObjectType(t));
|
|
466
|
+
}
|
|
467
|
+
if (typeNode.type === 'TSIntersectionType') {
|
|
468
|
+
return typeNode.types.some((t) => containsInlineObjectType(t));
|
|
469
|
+
}
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
PropertyDefinition(node) {
|
|
475
|
+
const propertyDecorator = getPropertyDecorator(node);
|
|
476
|
+
if (!propertyDecorator) return;
|
|
477
|
+
|
|
478
|
+
const typeAnnotation = node.typeAnnotation?.typeAnnotation;
|
|
479
|
+
const arrayElementType = getArrayElementType(typeAnnotation);
|
|
480
|
+
if (!arrayElementType) return;
|
|
481
|
+
|
|
482
|
+
if (!hasTypeArrayOption(propertyDecorator)) {
|
|
483
|
+
context.report({
|
|
484
|
+
node: propertyDecorator,
|
|
485
|
+
message:
|
|
486
|
+
'Array properties decorated with @property must include {type: Array}.',
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (containsInlineObjectType(arrayElementType)) {
|
|
491
|
+
context.report({
|
|
492
|
+
node: arrayElementType,
|
|
493
|
+
message:
|
|
494
|
+
'Array item type must be a named interface/type (avoid inline object types like Array<{...}>).',
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
export default [
|
|
505
|
+
...compat.extends(
|
|
506
|
+
'eslint:recommended',
|
|
507
|
+
'plugin:@typescript-eslint/eslint-recommended',
|
|
508
|
+
'plugin:@typescript-eslint/recommended',
|
|
509
|
+
'plugin:storybook/recommended'
|
|
510
|
+
),
|
|
511
|
+
{
|
|
512
|
+
plugins: {
|
|
513
|
+
'@typescript-eslint': typescriptEslint,
|
|
514
|
+
'custom-element': customElementPlugin,
|
|
515
|
+
openbridge: openbridgePlugin,
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
languageOptions: {
|
|
519
|
+
globals: {
|
|
520
|
+
...globals.browser,
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
parser: tsParser,
|
|
524
|
+
ecmaVersion: 2020,
|
|
525
|
+
sourceType: 'module',
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
rules: {
|
|
529
|
+
'no-prototype-builtins': 'off',
|
|
530
|
+
'@typescript-eslint/ban-types': 'off',
|
|
531
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
532
|
+
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
533
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
534
|
+
'@typescript-eslint/no-empty-function': 'off',
|
|
535
|
+
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
536
|
+
|
|
537
|
+
'@typescript-eslint/no-unused-vars': [
|
|
538
|
+
'warn',
|
|
539
|
+
{
|
|
540
|
+
argsIgnorePattern: '^_',
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
'custom-element/prefer-local-decorator': 'error',
|
|
544
|
+
'openbridge/prefer-enum-over-string-literal-union': 'error',
|
|
545
|
+
'openbridge/prefer-boolean-property-default-false': 'error',
|
|
546
|
+
'openbridge/prefer-array-property-type-and-item-interface': 'error',
|
|
547
|
+
'openbridge/storybook-title-case': 'off',
|
|
548
|
+
// Disabled because eslint-plugin-file-extension-in-import-ts is not yet
|
|
549
|
+
// compatible with ESLint v10 (it still uses deprecated context methods).
|
|
550
|
+
'file-extension-in-import-ts/file-extension-in-import-ts': 'off',
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
files: ['**/rollup.config.js', '**/web-test-runner.config.js'],
|
|
555
|
+
|
|
556
|
+
languageOptions: {
|
|
557
|
+
globals: {
|
|
558
|
+
...globals.node,
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
files: [
|
|
564
|
+
'**/*_test.ts',
|
|
565
|
+
'**/custom_typings/*.ts',
|
|
566
|
+
'packages/labs/ssr/src/test/integration/tests/**',
|
|
567
|
+
'packages/labs/ssr/src/lib/util/parse5-utils.ts',
|
|
568
|
+
],
|
|
569
|
+
|
|
570
|
+
rules: {
|
|
571
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
// Generated locale files use string-literal unions from lit-localize
|
|
576
|
+
files: ['**/generated/locales/*.ts'],
|
|
577
|
+
|
|
578
|
+
rules: {
|
|
579
|
+
'openbridge/prefer-enum-over-string-literal-union': 'off',
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
files: ['**/*.stories.ts'],
|
|
584
|
+
|
|
585
|
+
rules: {
|
|
586
|
+
'openbridge/storybook-title-case': 'error',
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
];
|