@lvce-editor/eslint-plugin-virtual-dom 18.10.0 → 18.12.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.
Files changed (2) hide show
  1. package/dist/index.js +265 -52
  2. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import json from '@eslint/json';
2
+ import { validRange, subset } from 'semver';
3
+
1
4
  const isPropertyNode = node => {
2
5
  return typeof node === 'object' && node !== null && 'type' in node && node.type === 'Property' && 'key' in node && 'value' in node && 'computed' in node;
3
6
  };
@@ -93,7 +96,7 @@ const controlsAllowingChildren = new Set(['A', 'Button']);
93
96
  const hasDirectName = node => {
94
97
  return Boolean(getProperty(node, 'ariaLabel') || getProperty(node, 'ariaLabelledBy') || getProperty(node, 'title'));
95
98
  };
96
- const meta$u = {
99
+ const meta$w = {
97
100
  docs: {
98
101
  description: 'Require a statically determinable accessible name for virtual-dom controls'
99
102
  },
@@ -102,7 +105,7 @@ const meta$u = {
102
105
  },
103
106
  type: 'problem'
104
107
  };
105
- const create$u = context => {
108
+ const create$w = context => {
106
109
  return {
107
110
  ObjectExpression(node) {
108
111
  if (!isVirtualDomNode(node) || hasDirectName(node)) {
@@ -144,11 +147,11 @@ const create$u = context => {
144
147
 
145
148
  const accessibleControlName = {
146
149
  __proto__: null,
147
- create: create$u,
148
- meta: meta$u
150
+ create: create$w,
151
+ meta: meta$w
149
152
  };
150
153
 
151
- const meta$t = {
154
+ const meta$v = {
152
155
  docs: {
153
156
  description: 'Require an explicit role on clickable virtual-dom div nodes'
154
157
  },
@@ -157,7 +160,7 @@ const meta$t = {
157
160
  },
158
161
  type: 'problem'
159
162
  };
160
- const create$t = context => {
163
+ const create$v = context => {
161
164
  return {
162
165
  ObjectExpression(node) {
163
166
  if (!isVirtualDomNode(node)) {
@@ -179,11 +182,113 @@ const create$t = context => {
179
182
 
180
183
  const clickableDivNeedsRole = {
181
184
  __proto__: null,
182
- create: create$t,
183
- meta: meta$t
185
+ create: create$v,
186
+ meta: meta$v
184
187
  };
185
188
 
186
- const meta$s = {
189
+ const meta$u = {
190
+ docs: {
191
+ description: 'Require static mergeClassNames calls inside functions to be hoisted to module scope'
192
+ },
193
+ messages: {
194
+ hoistClassNames: 'Hoist this static mergeClassNames call to module scope.'
195
+ },
196
+ type: 'suggestion'
197
+ };
198
+ const findVariable$1 = (sourceCode, node) => {
199
+ let scope = sourceCode.getScope(node);
200
+ while (scope) {
201
+ const variable = scope.set.get(node.name);
202
+ if (variable) {
203
+ return variable;
204
+ }
205
+ scope = scope.upper;
206
+ }
207
+ return undefined;
208
+ };
209
+ const isMergeClassNames = (sourceCode, node) => {
210
+ if (node.type === 'Identifier') {
211
+ return Boolean(findVariable$1(sourceCode, node)?.defs.some(definition => {
212
+ return definition.type === 'ImportBinding' && definition.node.type === 'ImportSpecifier' && (definition.node.imported.type === 'Identifier' ? definition.node.imported.name : definition.node.imported.value) === 'mergeClassNames';
213
+ }));
214
+ }
215
+ if (node.type !== 'MemberExpression' || node.computed || node.optional || node.object.type !== 'Identifier' || node.property.type !== 'Identifier' || node.property.name !== 'mergeClassNames') {
216
+ return false;
217
+ }
218
+ return Boolean(findVariable$1(sourceCode, node.object)?.defs.some(definition => {
219
+ return definition.type === 'ImportBinding' && definition.node.type === 'ImportNamespaceSpecifier';
220
+ }));
221
+ };
222
+ const isStatic = (sourceCode, node, seen = new Set()) => {
223
+ switch (node.type) {
224
+ case 'BinaryExpression':
225
+ return node.operator === '+' && isStatic(sourceCode, node.left, seen) && isStatic(sourceCode, node.right, seen);
226
+ case 'Identifier':
227
+ {
228
+ const variable = findVariable$1(sourceCode, node);
229
+ if (!variable || variable.scope.type !== 'module' || seen.has(variable)) {
230
+ return false;
231
+ }
232
+ const definition = variable.defs[0];
233
+ if (definition?.type !== 'Variable' || definition.parent.kind !== 'const' || !definition.node.init) {
234
+ return false;
235
+ }
236
+ return isStatic(sourceCode, definition.node.init, new Set([...seen, variable]));
237
+ }
238
+ case 'Literal':
239
+ return typeof node.value === 'string';
240
+ case 'MemberExpression':
241
+ {
242
+ // Class-name modules export constants. Do not assume arbitrary object properties are immutable.
243
+ if (node.computed || node.optional || node.object.type !== 'Identifier') {
244
+ return false;
245
+ }
246
+ const variable = findVariable$1(sourceCode, node.object);
247
+ return Boolean(variable?.defs.some(definition => {
248
+ if (definition.type !== 'ImportBinding') {
249
+ return false;
250
+ }
251
+ if (definition.node.type === 'ImportNamespaceSpecifier') {
252
+ return /(?:^|\/)ClassNames\.(?:js|ts)$/.test(String(definition.parent.source.value));
253
+ }
254
+ return definition.node.type === 'ImportSpecifier' && definition.node.imported.type === 'Identifier' && definition.node.imported.name === 'ClassNames' && definition.parent.source.value === '@lvce-editor/virtual-dom-worker';
255
+ }));
256
+ }
257
+ case 'TemplateLiteral':
258
+ return node.expressions.every(expression => isStatic(sourceCode, expression, seen));
259
+ default:
260
+ return false;
261
+ }
262
+ };
263
+ const create$u = context => {
264
+ return {
265
+ CallExpression(node) {
266
+ if (node.type !== 'CallExpression' || node.optional || !isMergeClassNames(context.sourceCode, node.callee)) {
267
+ return;
268
+ }
269
+ if (context.sourceCode.getAncestors(node).every(ancestor => {
270
+ return !['ArrowFunctionExpression', 'FunctionDeclaration', 'FunctionExpression'].includes(ancestor.type);
271
+ })) {
272
+ return;
273
+ }
274
+ if (node.arguments.some(argument => !isStatic(context.sourceCode, argument))) {
275
+ return;
276
+ }
277
+ context.report({
278
+ messageId: 'hoistClassNames',
279
+ node
280
+ });
281
+ }
282
+ };
283
+ };
284
+
285
+ const hoistClassNames = {
286
+ __proto__: null,
287
+ create: create$u,
288
+ meta: meta$u
289
+ };
290
+
291
+ const meta$t = {
187
292
  docs: {
188
293
  description: 'Require static virtual-dom nodes created inside functions to be hoisted to module scope'
189
294
  },
@@ -318,7 +423,7 @@ const isStaticExpression = (sourceCode, node) => {
318
423
  return false;
319
424
  }
320
425
  };
321
- const create$s = context => {
426
+ const create$t = context => {
322
427
  return {
323
428
  ObjectExpression(node) {
324
429
  if (!isVirtualDomElement(node)) {
@@ -343,11 +448,11 @@ const create$s = context => {
343
448
 
344
449
  const hoistStaticNodes = {
345
450
  __proto__: null,
346
- create: create$s,
347
- meta: meta$s
451
+ create: create$t,
452
+ meta: meta$t
348
453
  };
349
454
 
350
- const meta$r = {
455
+ const meta$s = {
351
456
  docs: {
352
457
  description: 'Disallow conditional spreads in virtual-dom arrays'
353
458
  },
@@ -365,7 +470,7 @@ const isVirtualDomArray$1 = node => {
365
470
  const addsVirtualDomChildren = spread => {
366
471
  return isVirtualDomArray$1(spread.argument.consequent) || isVirtualDomArray$1(spread.argument.alternate);
367
472
  };
368
- const create$r = context => {
473
+ const create$s = context => {
369
474
  return {
370
475
  ArrayExpression(node) {
371
476
  const conditionalSpreads = node.elements.filter(isConditionalSpread);
@@ -384,11 +489,11 @@ const create$r = context => {
384
489
 
385
490
  const noConditionalSpread = {
386
491
  __proto__: null,
387
- create: create$r,
388
- meta: meta$r
492
+ create: create$s,
493
+ meta: meta$s
389
494
  };
390
495
 
391
- const meta$q = {
496
+ const meta$r = {
392
497
  docs: {
393
498
  description: 'Disallow element helpers for creating virtual DOM nodes'
394
499
  },
@@ -397,7 +502,7 @@ const meta$q = {
397
502
  },
398
503
  type: 'problem'
399
504
  };
400
- const create$q = context => {
505
+ const create$r = context => {
401
506
  return {
402
507
  CallExpression(node) {
403
508
  if (!isIdentifierNode(node.callee) || node.callee.name !== 'element' || !getVirtualDomElementName(node.arguments[0])) {
@@ -413,11 +518,11 @@ const create$q = context => {
413
518
 
414
519
  const noElementHelper = {
415
520
  __proto__: null,
416
- create: create$q,
417
- meta: meta$q
521
+ create: create$r,
522
+ meta: meta$r
418
523
  };
419
524
 
420
- const meta$p = {
525
+ const meta$q = {
421
526
  docs: {
422
527
  description: 'Disallow empty ARIA values in virtual-dom nodes'
423
528
  },
@@ -429,7 +534,7 @@ const meta$p = {
429
534
  const isAriaProperty = name => {
430
535
  return /^aria[A-Z]/.test(name);
431
536
  };
432
- const create$p = context => {
537
+ const create$q = context => {
433
538
  return {
434
539
  ObjectExpression(node) {
435
540
  if (!isVirtualDomNode(node)) {
@@ -454,11 +559,11 @@ const create$p = context => {
454
559
 
455
560
  const noEmptyAria = {
456
561
  __proto__: null,
457
- create: create$p,
458
- meta: meta$p
562
+ create: create$q,
563
+ meta: meta$q
459
564
  };
460
565
 
461
- const meta$o = {
566
+ const meta$p = {
462
567
  docs: {
463
568
  description: 'Disallow empty className values in virtual-dom nodes'
464
569
  },
@@ -467,7 +572,7 @@ const meta$o = {
467
572
  },
468
573
  type: 'problem'
469
574
  };
470
- const create$o = context => {
575
+ const create$p = context => {
471
576
  return {
472
577
  ObjectExpression(node) {
473
578
  if (!isVirtualDomNode(node)) {
@@ -487,11 +592,11 @@ const create$o = context => {
487
592
 
488
593
  const noEmptyClassName = {
489
594
  __proto__: null,
490
- create: create$o,
491
- meta: meta$o
595
+ create: create$p,
596
+ meta: meta$p
492
597
  };
493
598
 
494
- const meta$n = {
599
+ const meta$o = {
495
600
  docs: {
496
601
  description: 'Disallow inline event handlers in virtual-dom nodes'
497
602
  },
@@ -506,7 +611,7 @@ const isEventPropertyName = name => {
506
611
  const isInlineEventHandler = node => {
507
612
  return isStringLiteral(node) || isTemplateLiteralNode(node) || isArrowFunctionExpressionNode(node) || isFunctionExpressionNode(node);
508
613
  };
509
- const create$n = context => {
614
+ const create$o = context => {
510
615
  return {
511
616
  ObjectExpression(node) {
512
617
  if (!isVirtualDomNode(node)) {
@@ -531,11 +636,11 @@ const create$n = context => {
531
636
 
532
637
  const noInlineEventHandlers = {
533
638
  __proto__: null,
534
- create: create$n,
535
- meta: meta$n
639
+ create: create$o,
640
+ meta: meta$o
536
641
  };
537
642
 
538
- const meta$m = {
643
+ const meta$n = {
539
644
  docs: {
540
645
  description: 'Disallow inline style attributes in virtual-dom nodes'
541
646
  },
@@ -544,7 +649,7 @@ const meta$m = {
544
649
  },
545
650
  type: 'problem'
546
651
  };
547
- const create$m = context => {
652
+ const create$n = context => {
548
653
  return {
549
654
  ObjectExpression(node) {
550
655
  if (!isVirtualDomNode(node)) {
@@ -564,8 +669,8 @@ const create$m = context => {
564
669
 
565
670
  const noInlineStyle = {
566
671
  __proto__: null,
567
- create: create$m,
568
- meta: meta$m
672
+ create: create$n,
673
+ meta: meta$n
569
674
  };
570
675
 
571
676
  const ignoredProperties$1 = new Set(['childCount', 'type']);
@@ -575,7 +680,7 @@ const isOptionalExpression = node => {
575
680
  const isNullish = node => {
576
681
  return isLiteralNode(node) && node.value === null || isIdentifierNode(node) && node.name === 'undefined' || isOptionalExpression(node);
577
682
  };
578
- const meta$l = {
683
+ const meta$m = {
579
684
  docs: {
580
685
  description: 'Disallow nullish virtual-dom attribute values'
581
686
  },
@@ -584,7 +689,7 @@ const meta$l = {
584
689
  },
585
690
  type: 'problem'
586
691
  };
587
- const create$l = context => {
692
+ const create$m = context => {
588
693
  return {
589
694
  ObjectExpression(node) {
590
695
  if (!isVirtualDomNode(node)) {
@@ -612,11 +717,11 @@ const create$l = context => {
612
717
 
613
718
  const noNullishAttributeValues = {
614
719
  __proto__: null,
615
- create: create$l,
616
- meta: meta$l
720
+ create: create$m,
721
+ meta: meta$m
617
722
  };
618
723
 
619
- const meta$k = {
724
+ const meta$l = {
620
725
  docs: {
621
726
  description: 'Disallow object-like attribute values in virtual-dom nodes'
622
727
  },
@@ -629,7 +734,7 @@ const ignoredProperties = new Set(['childCount', 'type']);
629
734
  const isObjectLikeAttributeValue = node => {
630
735
  return isObjectExpressionNode(node) || isArrayExpressionNode(node) || isArrowFunctionExpressionNode(node) || isFunctionExpressionNode(node);
631
736
  };
632
- const create$k = context => {
737
+ const create$l = context => {
633
738
  return {
634
739
  ObjectExpression(node) {
635
740
  if (!isVirtualDomNode(node)) {
@@ -654,11 +759,11 @@ const create$k = context => {
654
759
 
655
760
  const noObjectAttributeValues = {
656
761
  __proto__: null,
657
- create: create$k,
658
- meta: meta$k
762
+ create: create$l,
763
+ meta: meta$l
659
764
  };
660
765
 
661
- const meta$j = {
766
+ const meta$k = {
662
767
  docs: {
663
768
  description: 'Disallow positive virtual-dom tabIndex values'
664
769
  },
@@ -667,7 +772,7 @@ const meta$j = {
667
772
  },
668
773
  type: 'problem'
669
774
  };
670
- const create$j = context => {
775
+ const create$k = context => {
671
776
  return {
672
777
  ObjectExpression(node) {
673
778
  if (!isVirtualDomNode(node)) {
@@ -688,11 +793,11 @@ const create$j = context => {
688
793
 
689
794
  const noPositiveTabIndex = {
690
795
  __proto__: null,
691
- create: create$j,
692
- meta: meta$j
796
+ create: create$k,
797
+ meta: meta$k
693
798
  };
694
799
 
695
- const meta$i = {
800
+ const meta$j = {
696
801
  docs: {
697
802
  description: 'Disallow raw string children in virtual-dom arrays'
698
803
  },
@@ -704,7 +809,7 @@ const meta$i = {
704
809
  const isVirtualDomArray = node => {
705
810
  return isArrayExpressionNode(node) && node.elements.some(element => isVirtualDomNode(element) || isTextCall(element));
706
811
  };
707
- const create$i = context => {
812
+ const create$j = context => {
708
813
  return {
709
814
  ArrayExpression(node) {
710
815
  if (!isVirtualDomArray(node)) {
@@ -724,6 +829,98 @@ const create$i = context => {
724
829
  };
725
830
 
726
831
  const noRawTextChildren = {
832
+ __proto__: null,
833
+ create: create$j,
834
+ meta: meta$j
835
+ };
836
+
837
+ const defaultRestrictions = {
838
+ eslint: '<10.10.0-0',
839
+ typescript: '>=6.0.0-0 <7.0.0-0'
840
+ };
841
+ const dependencySections = new Set(['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']);
842
+ const meta$i = {
843
+ docs: {
844
+ description: 'Disallow dependencies and version ranges outside the supported versions'
845
+ },
846
+ messages: {
847
+ forbidden: 'Dependency "{{name}}" is not allowed.',
848
+ unsupported: 'Dependency "{{name}}" must use a version range within "{{allowed}}"; received "{{version}}".'
849
+ },
850
+ schema: [{
851
+ additionalProperties: false,
852
+ properties: {
853
+ restrictions: {
854
+ additionalProperties: {
855
+ anyOf: [{
856
+ minLength: 1,
857
+ type: 'string'
858
+ }, {
859
+ enum: [false]
860
+ }]
861
+ },
862
+ type: 'object'
863
+ }
864
+ },
865
+ type: 'object'
866
+ }],
867
+ type: 'problem'
868
+ };
869
+ const create$i = context => {
870
+ const restrictions = {
871
+ ...defaultRestrictions,
872
+ ...context.options[0]?.restrictions
873
+ };
874
+ for (const [name, allowed] of Object.entries(restrictions)) {
875
+ if (allowed !== false && !validRange(allowed)) {
876
+ throw new Error(`Invalid allowed version range for dependency "${name}": ${allowed}`);
877
+ }
878
+ }
879
+ return {
880
+ Document(node) {
881
+ if (node.body?.type !== 'Object') {
882
+ return;
883
+ }
884
+ for (const section of node.body.members) {
885
+ if (!dependencySections.has(section.name.value) || section.value.type !== 'Object') {
886
+ continue;
887
+ }
888
+ for (const dependency of section.value.members) {
889
+ const name = dependency.name.value;
890
+ if (Object.hasOwn(restrictions, name)) {
891
+ const allowed = restrictions[name];
892
+ if (allowed === false) {
893
+ context.report({
894
+ data: {
895
+ name
896
+ },
897
+ loc: dependency.name.loc,
898
+ messageId: 'forbidden'
899
+ });
900
+ } else {
901
+ const version = dependency.value.value;
902
+ if (typeof version !== 'string' || !validRange(version) || !subset(version, allowed, {
903
+ includePrerelease: true
904
+ })) {
905
+ context.report({
906
+ data: {
907
+ allowed,
908
+ name,
909
+ version: String(version)
910
+ },
911
+ loc: dependency.value.loc,
912
+ messageId: 'unsupported'
913
+ });
914
+ }
915
+ }
916
+ }
917
+ }
918
+ }
919
+ }
920
+ };
921
+ };
922
+
923
+ const noRestrictedDependencies = {
727
924
  __proto__: null,
728
925
  create: create$i,
729
926
  meta: meta$i
@@ -1664,6 +1861,7 @@ const plugin = {
1664
1861
  rules: {
1665
1862
  'accessible-control-name': accessibleControlName,
1666
1863
  'clickable-div-needs-role': clickableDivNeedsRole,
1864
+ 'hoist-class-names': hoistClassNames,
1667
1865
  'hoist-static-nodes': hoistStaticNodes,
1668
1866
  'no-conditional-spread': noConditionalSpread,
1669
1867
  'no-element-helper': noElementHelper,
@@ -1675,6 +1873,7 @@ const plugin = {
1675
1873
  'no-object-attribute-values': noObjectAttributeValues,
1676
1874
  'no-positive-tab-index': noPositiveTabIndex,
1677
1875
  'no-raw-text-children': noRawTextChildren,
1876
+ 'no-restricted-dependencies': noRestrictedDependencies,
1678
1877
  'no-shared-event-listener-handlers': noSharedEventListenerHandlers,
1679
1878
  'no-text-helper': noTextHelper,
1680
1879
  'no-tree-helper': noTreeHelper,
@@ -1695,13 +1894,26 @@ const plugin = {
1695
1894
  'valid-node-shape': validNodeShape
1696
1895
  }
1697
1896
  };
1698
- const recommended = [{
1897
+ const recommendedDependencies = [{
1898
+ files: ['**/package.json'],
1899
+ language: 'json/json',
1900
+ plugins: {
1901
+ // @ts-ignore
1902
+ json,
1903
+ 'virtual-dom': plugin
1904
+ },
1905
+ rules: {
1906
+ 'virtual-dom/no-restricted-dependencies': 'error'
1907
+ }
1908
+ }];
1909
+ const recommended = [...recommendedDependencies, {
1699
1910
  files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
1700
1911
  plugins: {
1701
1912
  'virtual-dom': plugin
1702
1913
  },
1703
1914
  rules: {
1704
1915
  'virtual-dom/clickable-div-needs-role': 'error',
1916
+ 'virtual-dom/hoist-class-names': 'error',
1705
1917
  'virtual-dom/hoist-static-nodes': 'error',
1706
1918
  'virtual-dom/no-conditional-spread': 'error',
1707
1919
  'virtual-dom/no-element-helper': 'error',
@@ -1727,6 +1939,7 @@ const recommended = [{
1727
1939
  }, {
1728
1940
  files: ['**/test/**/*.{js,mjs,cjs,ts,mts,cts}', '**/tests/**/*.{js,mjs,cjs,ts,mts,cts}', '**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts}'],
1729
1941
  rules: {
1942
+ 'virtual-dom/hoist-class-names': 'off',
1730
1943
  'virtual-dom/hoist-static-nodes': 'off',
1731
1944
  'virtual-dom/prefer-merge-class-names': 'off'
1732
1945
  }
@@ -1751,4 +1964,4 @@ const strict = [...recommended, {
1751
1964
  }
1752
1965
  }];
1753
1966
 
1754
- export { recommended as default, strict };
1967
+ export { recommended as default, recommendedDependencies, strict };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/eslint-plugin-virtual-dom",
3
- "version": "18.10.0",
3
+ "version": "18.12.0",
4
4
  "main": "dist/index.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,5 +8,8 @@
8
8
  },
9
9
  "type": "module",
10
10
  "license": "MIT",
11
- "description": "ESLint rules for @lvce-editor/virtual-dom usage."
11
+ "description": "ESLint rules for @lvce-editor/virtual-dom usage.",
12
+ "dependencies": {
13
+ "semver": "^7.7.2"
14
+ }
12
15
  }