@drupal-canvas/eslint-config 0.2.1 → 0.4.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 (3) hide show
  1. package/README.md +7 -5
  2. package/dist/index.js +229 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,11 +33,13 @@ export default defineConfig([
33
33
  The following custom rules are part of the `required` config and validate Drupal
34
34
  Canvas Code Components:
35
35
 
36
- | Rule | Description |
37
- | ---------------------- | ------------------------------------------------------------------------------------------------------- |
38
- | `component-dir-name` | Validates that `machineName` matches the directory name (index-style) or filename prefix (named-style). |
39
- | `component-exports` | Validates that component has a default export. |
40
- | `component-prop-names` | Validates that component prop IDs match the camelCase version of their titles. |
36
+ | Rule | Description |
37
+ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
38
+ | `component-dir-name` | Validates that `machineName` matches the directory name (index-style) or filename prefix (named-style). |
39
+ | `component-exports` | Validates that component has a default export. |
40
+ | `component-prop-example-value-image-url` | Validates that `canvas.module/image` prop examples use fully qualified image URLs. |
41
+ | `component-prop-example-value-no-empty-string` | Validates that string prop examples do not contain empty string values. |
42
+ | `component-prop-names` | Validates that component prop IDs match the camelCase version of their titles. |
41
43
 
42
44
  ### Deprecated rules
43
45
 
package/dist/index.js CHANGED
@@ -320,9 +320,9 @@ var require_ignore = __commonJS({
320
320
  };
321
321
  }
322
322
  if (checkPattern(pattern.pattern)) {
323
- const rule8 = createRule(pattern, this._ignoreCase);
323
+ const rule10 = createRule(pattern, this._ignoreCase);
324
324
  this._added = true;
325
- this._rules.push(rule8);
325
+ this._rules.push(rule10);
326
326
  }
327
327
  }
328
328
  // @param {Array<string> | string | Ignore} pattern
@@ -344,18 +344,18 @@ var require_ignore = __commonJS({
344
344
  let ignored = false;
345
345
  let unignored = false;
346
346
  let matchedRule;
347
- this._rules.forEach((rule8) => {
348
- const { negative } = rule8;
347
+ this._rules.forEach((rule10) => {
348
+ const { negative } = rule10;
349
349
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
350
350
  return;
351
351
  }
352
- const matched = rule8[mode].test(path2);
352
+ const matched = rule10[mode].test(path2);
353
353
  if (!matched) {
354
354
  return;
355
355
  }
356
356
  ignored = !negative;
357
357
  unignored = negative;
358
- matchedRule = negative ? UNDEFINED : rule8;
358
+ matchedRule = negative ? UNDEFINED : rule10;
359
359
  });
360
360
  const ret = {
361
361
  ignored,
@@ -577,6 +577,15 @@ function getYAMLStringValue(node) {
577
577
  }
578
578
  return null;
579
579
  }
580
+ function isYAMLMapping(node) {
581
+ return node?.type === "YAMLMapping";
582
+ }
583
+ function isYAMLSequence(node) {
584
+ return node?.type === "YAMLSequence";
585
+ }
586
+ function getYAMLMappingPair(mapping, key) {
587
+ return mapping.pairs.find((pair) => getYAMLStringValue(pair.key) === key);
588
+ }
580
589
 
581
590
  // src/rules/component-dir-name.ts
582
591
  var NAMED_SUFFIX2 = ".component.yml";
@@ -4599,31 +4608,217 @@ var rule3 = {
4599
4608
  }
4600
4609
  };
4601
4610
  var component_imports_default = rule3;
4602
- function extractProps(propsNode) {
4603
- if (!propsNode.value || propsNode.value.type !== "YAMLMapping") {
4611
+
4612
+ // src/rules/component-prop-example-value-image-url.ts
4613
+ var IMAGE_REF = "json-schema-definitions://canvas.module/image";
4614
+ function hasImageRef(propMapping) {
4615
+ const ref = getYAMLStringValue(
4616
+ getYAMLMappingPair(propMapping, "$ref")?.value ?? null
4617
+ );
4618
+ if (ref === IMAGE_REF) {
4619
+ return true;
4620
+ }
4621
+ const itemsValue = getYAMLMappingPair(propMapping, "items")?.value;
4622
+ if (!isYAMLMapping(itemsValue)) {
4623
+ return false;
4624
+ }
4625
+ return getYAMLStringValue(
4626
+ getYAMLMappingPair(itemsValue, "$ref")?.value ?? null
4627
+ ) === IMAGE_REF;
4628
+ }
4629
+ function isFullyQualifiedUrl(value) {
4630
+ try {
4631
+ const parsed = new URL(value);
4632
+ return parsed.protocol.length > 0 && parsed.hostname.length > 0;
4633
+ } catch {
4634
+ return false;
4635
+ }
4636
+ }
4637
+ function getInvalidImageExampleNodes(examplesValue) {
4638
+ if (!isYAMLSequence(examplesValue)) {
4604
4639
  return [];
4605
4640
  }
4606
- const propsMapping = propsNode.value;
4607
- const propertiesPair = propsMapping.pairs.find(
4608
- (p) => getYAMLStringValue(p.key) === "properties"
4641
+ const invalidNodes = [];
4642
+ for (const example of examplesValue.entries) {
4643
+ const imageEntries = isYAMLSequence(example) ? example.entries : [example];
4644
+ for (const imageEntry of imageEntries) {
4645
+ if (!isYAMLMapping(imageEntry)) {
4646
+ continue;
4647
+ }
4648
+ const srcNode = getYAMLMappingPair(imageEntry, "src")?.value;
4649
+ if (srcNode?.type === "YAMLScalar" && typeof srcNode.value === "string" && !isFullyQualifiedUrl(srcNode.value)) {
4650
+ invalidNodes.push(srcNode);
4651
+ }
4652
+ }
4653
+ }
4654
+ return invalidNodes;
4655
+ }
4656
+ var rule4 = {
4657
+ meta: {
4658
+ type: "problem",
4659
+ docs: {
4660
+ description: "Validates that default examples for image props use fully-qualified URLs"
4661
+ }
4662
+ },
4663
+ create(context) {
4664
+ if (!isComponentYmlFile(context.filename)) {
4665
+ return {};
4666
+ }
4667
+ return {
4668
+ YAMLPair(node) {
4669
+ const keyName = getYAMLStringValue(node.key);
4670
+ if (keyName !== "props" || !isYAMLMapping(node.value)) {
4671
+ return;
4672
+ }
4673
+ const propertiesValue = getYAMLMappingPair(
4674
+ node.value,
4675
+ "properties"
4676
+ )?.value;
4677
+ if (!isYAMLMapping(propertiesValue)) {
4678
+ return;
4679
+ }
4680
+ for (const propPair of propertiesValue.pairs) {
4681
+ const propId = getYAMLStringValue(propPair.key);
4682
+ if (!propId || !isYAMLMapping(propPair.value)) {
4683
+ continue;
4684
+ }
4685
+ if (!hasImageRef(propPair.value)) {
4686
+ continue;
4687
+ }
4688
+ const examplesValue = getYAMLMappingPair(
4689
+ propPair.value,
4690
+ "examples"
4691
+ )?.value;
4692
+ const invalidNodes = getInvalidImageExampleNodes(examplesValue);
4693
+ for (const invalidNode of invalidNodes) {
4694
+ context.report({
4695
+ node: invalidNode,
4696
+ message: `Image prop "${propId}" example src must be a fully-qualified URL with both scheme and host. Use a placeholder URL such as https://placehold.co/600x400.`
4697
+ });
4698
+ }
4699
+ }
4700
+ }
4701
+ };
4702
+ }
4703
+ };
4704
+ var component_prop_example_value_image_url_default = rule4;
4705
+
4706
+ // src/rules/component-prop-example-value-no-empty-string.ts
4707
+ function isEmptyStringScalar(node) {
4708
+ return node?.type === "YAMLScalar" && node.value === "";
4709
+ }
4710
+ function getStringExampleMode(propMapping) {
4711
+ const type2 = getYAMLStringValue(
4712
+ getYAMLMappingPair(propMapping, "type")?.value ?? null
4609
4713
  );
4610
- if (!propertiesPair || !propertiesPair.value || propertiesPair.value.type !== "YAMLMapping") {
4714
+ if (type2 === "string") {
4715
+ return "string";
4716
+ }
4717
+ if (type2 !== "array") {
4718
+ return null;
4719
+ }
4720
+ const itemsValue = getYAMLMappingPair(propMapping, "items")?.value;
4721
+ if (!isYAMLMapping(itemsValue)) {
4722
+ return null;
4723
+ }
4724
+ return getYAMLStringValue(
4725
+ getYAMLMappingPair(itemsValue, "type")?.value ?? null
4726
+ ) === "string" ? "string-array" : null;
4727
+ }
4728
+ function getInvalidExampleNodes(examplesValue, mode) {
4729
+ if (!isYAMLSequence(examplesValue)) {
4730
+ return [];
4731
+ }
4732
+ if (mode === "string") {
4733
+ const invalidNodes2 = [];
4734
+ for (const example of examplesValue.entries) {
4735
+ if (isEmptyStringScalar(example)) {
4736
+ invalidNodes2.push(example);
4737
+ }
4738
+ }
4739
+ return invalidNodes2;
4740
+ }
4741
+ const invalidNodes = [];
4742
+ for (const example of examplesValue.entries) {
4743
+ if (!isYAMLSequence(example)) {
4744
+ continue;
4745
+ }
4746
+ for (const item of example.entries) {
4747
+ if (isEmptyStringScalar(item)) {
4748
+ invalidNodes.push(item);
4749
+ }
4750
+ }
4751
+ }
4752
+ return invalidNodes;
4753
+ }
4754
+ var rule5 = {
4755
+ meta: {
4756
+ type: "problem",
4757
+ docs: {
4758
+ description: "Validates that string prop examples do not contain empty string values"
4759
+ }
4760
+ },
4761
+ create(context) {
4762
+ if (!isComponentYmlFile(context.filename)) {
4763
+ return {};
4764
+ }
4765
+ return {
4766
+ YAMLPair(node) {
4767
+ const keyName = getYAMLStringValue(node.key);
4768
+ if (keyName !== "props" || !isYAMLMapping(node.value)) {
4769
+ return;
4770
+ }
4771
+ const propertiesValue = getYAMLMappingPair(
4772
+ node.value,
4773
+ "properties"
4774
+ )?.value;
4775
+ if (!isYAMLMapping(propertiesValue)) {
4776
+ return;
4777
+ }
4778
+ for (const propPair of propertiesValue.pairs) {
4779
+ const propId = getYAMLStringValue(propPair.key);
4780
+ if (!propId || !isYAMLMapping(propPair.value)) {
4781
+ continue;
4782
+ }
4783
+ const mode = getStringExampleMode(propPair.value);
4784
+ if (!mode) {
4785
+ continue;
4786
+ }
4787
+ const examplesValue = getYAMLMappingPair(
4788
+ propPair.value,
4789
+ "examples"
4790
+ )?.value;
4791
+ const invalidNodes = getInvalidExampleNodes(examplesValue, mode);
4792
+ for (const invalidNode of invalidNodes) {
4793
+ context.report({
4794
+ node: invalidNode,
4795
+ message: `Prop "${propId}" example values must not be empty strings. Remove the empty example or use a non-empty placeholder value.`
4796
+ });
4797
+ }
4798
+ }
4799
+ }
4800
+ };
4801
+ }
4802
+ };
4803
+ var component_prop_example_value_no_empty_string_default = rule5;
4804
+ function extractProps(propsNode) {
4805
+ if (!isYAMLMapping(propsNode.value)) {
4806
+ return [];
4807
+ }
4808
+ const propertiesValue = getYAMLMappingPair(
4809
+ propsNode.value,
4810
+ "properties"
4811
+ )?.value;
4812
+ if (!isYAMLMapping(propertiesValue)) {
4611
4813
  return [];
4612
4814
  }
4613
- const propertiesMapping = propertiesPair.value;
4614
4815
  const props = [];
4615
- for (const pair of propertiesMapping.pairs) {
4816
+ for (const pair of propertiesValue.pairs) {
4616
4817
  const propId = getYAMLStringValue(pair.key);
4617
4818
  if (!propId) continue;
4618
- if (!pair.value || pair.value.type !== "YAMLMapping") continue;
4619
- const propMapping = pair.value;
4620
- const titlePair = propMapping.pairs.find(
4621
- (p) => getYAMLStringValue(p.key) === "title"
4622
- );
4819
+ if (!isYAMLMapping(pair.value)) continue;
4623
4820
  let title = null;
4624
- if (titlePair) {
4625
- title = getYAMLStringValue(titlePair.value);
4626
- }
4821
+ title = getYAMLStringValue(getYAMLMappingPair(pair.value, "title")?.value);
4627
4822
  props.push({
4628
4823
  id: propId,
4629
4824
  title,
@@ -4632,7 +4827,7 @@ function extractProps(propsNode) {
4632
4827
  }
4633
4828
  return props;
4634
4829
  }
4635
- var rule4 = {
4830
+ var rule6 = {
4636
4831
  meta: {
4637
4832
  type: "problem",
4638
4833
  docs: {
@@ -4673,7 +4868,7 @@ var rule4 = {
4673
4868
  };
4674
4869
  }
4675
4870
  };
4676
- var component_prop_names_default = rule4;
4871
+ var component_prop_names_default = rule6;
4677
4872
 
4678
4873
  // src/configs/required.ts
4679
4874
  var required = defineConfig([
@@ -4697,6 +4892,8 @@ var required = defineConfig([
4697
4892
  plugins: {
4698
4893
  "drupal-canvas": {
4699
4894
  rules: {
4895
+ "component-prop-example-value-image-url": component_prop_example_value_image_url_default,
4896
+ "component-prop-example-value-no-empty-string": component_prop_example_value_no_empty_string_default,
4700
4897
  "component-dir-name": component_dir_name_default,
4701
4898
  "component-exports": component_exports_default,
4702
4899
  "component-imports": component_imports_default,
@@ -4705,6 +4902,8 @@ var required = defineConfig([
4705
4902
  }
4706
4903
  },
4707
4904
  rules: {
4905
+ "drupal-canvas/component-prop-example-value-image-url": "error",
4906
+ "drupal-canvas/component-prop-example-value-no-empty-string": "error",
4708
4907
  "drupal-canvas/component-dir-name": "error",
4709
4908
  "drupal-canvas/component-exports": "error",
4710
4909
  "drupal-canvas/component-imports": "error",
@@ -4754,7 +4953,7 @@ var IGNORED_FILES = [
4754
4953
  function isFileAllowed(fileName, allowedFiles) {
4755
4954
  return allowedFiles.some((allowedFile) => allowedFile === fileName);
4756
4955
  }
4757
- var rule5 = {
4956
+ var rule7 = {
4758
4957
  meta: {
4759
4958
  type: "problem",
4760
4959
  docs: {
@@ -4793,7 +4992,7 @@ var rule5 = {
4793
4992
  };
4794
4993
  }
4795
4994
  };
4796
- var component_files_default = rule5;
4995
+ var component_files_default = rule7;
4797
4996
  function checkImportSource2(context, node, source) {
4798
4997
  if (source.startsWith("./") || source.startsWith("../")) {
4799
4998
  context.report({
@@ -4922,7 +5121,7 @@ function checkImportSource2(context, node, source) {
4922
5121
  message: `Importing "${source}" is not supported. If this is a local import via a path alias, use the "@/components/" alias instead. If you are importing a third-party package, see the list of supported packages at https://project.pages.drupalcode.org/canvas/code-components/packages. (The status of supporting any third-party package can be tracked at https://drupal.org/i/3560197.)`
4923
5122
  });
4924
5123
  }
4925
- var rule6 = {
5124
+ var rule8 = {
4926
5125
  meta: {
4927
5126
  type: "problem",
4928
5127
  docs: {
@@ -4949,7 +5148,7 @@ var rule6 = {
4949
5148
  };
4950
5149
  }
4951
5150
  };
4952
- var component_imports_deprecated_default = rule6;
5151
+ var component_imports_deprecated_default = rule8;
4953
5152
  function findTopmostComponentsParentDir(currentParentDir, rootDir) {
4954
5153
  if (currentParentDir === rootDir) {
4955
5154
  return currentParentDir;
@@ -4970,7 +5169,7 @@ function hasComponentSubdirectories(dirPath) {
4970
5169
  }
4971
5170
  return false;
4972
5171
  }
4973
- var rule7 = {
5172
+ var rule9 = {
4974
5173
  meta: {
4975
5174
  type: "problem",
4976
5175
  docs: {
@@ -5005,7 +5204,7 @@ var rule7 = {
5005
5204
  };
5006
5205
  }
5007
5206
  };
5008
- var component_no_hierarchy_default = rule7;
5207
+ var component_no_hierarchy_default = rule9;
5009
5208
 
5010
5209
  // src/configs/requiredDeprecated.ts
5011
5210
  var required2 = defineConfig([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drupal-canvas/eslint-config",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "ESLint config for validating Drupal Canvas Code Components",
5
5
  "license": "MIT",
6
6
  "repository": {