@spiracss/stylelint-plugin 0.1.11 → 0.1.12

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Akihiro Hoshiyama
3
+ Copyright (c) 2025 SpiraCSS
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
1
  # @spiracss/stylelint-plugin
2
2
 
3
- Do not use. This package is experimental and internal only.
3
+ Experimental package under active development. Breaking changes may occur frequently.
@@ -25,6 +25,7 @@ const messages = stylelint_1.default.utils.ruleMessages(exports.ruleName, {
25
25
  disallowedModifier: () => 'Modifier classes are disabled by selectorPolicy. Use data-variant/data-state or enable class mode.',
26
26
  invalidDataAttribute: (attr) => `Attribute "${attr}" is not allowed by selectorPolicy.`,
27
27
  invalidDataValue: (attr, value) => `Attribute "${attr}" value "${value}" does not match selectorPolicy valueNaming.`,
28
+ rootSelectorMissingBlock: (block, selector) => `Root selector "${selector}" must include the root Block ".${block}".`,
28
29
  fileNameMismatch: (block, expected, actual) => `Root Block ".${block}" must be defined in "${expected}.scss" (found "${actual}.scss").`
29
30
  });
30
31
  const defaultValueNaming = {
@@ -341,6 +342,24 @@ const collectRootBlockNames = (selector, options, patterns) => {
341
342
  }).processSync(selector);
342
343
  return [...names];
343
344
  };
345
+ const analyzeRootSelector = (sel, rootBlockName, options, patterns) => {
346
+ let hasSpiraClass = false;
347
+ let hasRootBlock = false;
348
+ let hasOtherBlock = false;
349
+ sel.walk((node) => {
350
+ if (node.type !== 'class')
351
+ return;
352
+ const name = node.value;
353
+ const kind = classify(name, options, patterns);
354
+ if (kind !== 'external')
355
+ hasSpiraClass = true;
356
+ if (name === rootBlockName)
357
+ hasRootBlock = true;
358
+ if (kind === 'block' && name !== rootBlockName)
359
+ hasOtherBlock = true;
360
+ });
361
+ return { hasSpiraClass, hasRootBlock, hasOtherBlock };
362
+ };
344
363
  const buildValuePattern = (naming) => {
345
364
  const maxWords = naming.maxWords;
346
365
  switch (naming.case) {
@@ -771,6 +790,7 @@ const spiracssClassStructure = stylelint_1.default.createPlugin(exports.ruleName
771
790
  const interactionRules = markInteractionRules(root, commentPatterns);
772
791
  let firstRule = null;
773
792
  let rootBlockName = null;
793
+ const topLevelRules = [];
774
794
  const filePath = ((_a = result === null || result === void 0 ? void 0 : result.opts) === null || _a === void 0 ? void 0 : _a.from) || '';
775
795
  const normalizedPath = filePath ? path_1.default.normalize(filePath) : '';
776
796
  const pathSegments = normalizedPath ? normalizedPath.split(path_1.default.sep).filter(Boolean) : [];
@@ -799,6 +819,7 @@ const spiracssClassStructure = stylelint_1.default.createPlugin(exports.ruleName
799
819
  });
800
820
  };
801
821
  if (!parentRule) {
822
+ topLevelRules.push(rule);
802
823
  const rootBlocks = collectRootBlockNames(rule.selector, options, patterns);
803
824
  if (rootBlocks.length > 0) {
804
825
  const rootName = rootBlockName || rootBlocks[0];
@@ -826,6 +847,28 @@ const spiracssClassStructure = stylelint_1.default.createPlugin(exports.ruleName
826
847
  }));
827
848
  }).processSync(rule.selector);
828
849
  });
850
+ if (options.enforceSingleRootBlock && rootBlockName) {
851
+ const resolvedRootBlockName = rootBlockName;
852
+ topLevelRules.forEach((rule) => {
853
+ if (typeof rule.selector !== 'string')
854
+ return;
855
+ if (rule.selector.includes(':global'))
856
+ return;
857
+ (0, postcss_selector_parser_1.default)((selectors) => {
858
+ selectors.each((sel) => {
859
+ const { hasSpiraClass, hasRootBlock, hasOtherBlock } = analyzeRootSelector(sel, resolvedRootBlockName, options, patterns);
860
+ if (!hasSpiraClass || hasRootBlock || hasOtherBlock)
861
+ return;
862
+ stylelint_1.default.utils.report({
863
+ ruleName: exports.ruleName,
864
+ result,
865
+ node: rule,
866
+ message: messages.rootSelectorMissingBlock(resolvedRootBlockName, sel.toString().trim())
867
+ });
868
+ });
869
+ }).processSync(rule.selector);
870
+ });
871
+ }
829
872
  if (options.enforceRootFileName && rootBlockName && fileBase) {
830
873
  const isComponentsLayer = options.componentsDirs.some((dir) => {
831
874
  const normalizedDir = path_1.default.normalize(dir);
@@ -19,6 +19,7 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
19
19
  disallowedModifier: () => 'Modifier classes are disabled by selectorPolicy. Use data-variant/data-state or enable class mode.',
20
20
  invalidDataAttribute: (attr) => `Attribute "${attr}" is not allowed by selectorPolicy.`,
21
21
  invalidDataValue: (attr, value) => `Attribute "${attr}" value "${value}" does not match selectorPolicy valueNaming.`,
22
+ rootSelectorMissingBlock: (block, selector) => `Root selector "${selector}" must include the root Block ".${block}".`,
22
23
  fileNameMismatch: (block, expected, actual) => `Root Block ".${block}" must be defined in "${expected}.scss" (found "${actual}.scss").`
23
24
  });
24
25
  const defaultValueNaming = {
@@ -335,6 +336,24 @@ const collectRootBlockNames = (selector, options, patterns) => {
335
336
  }).processSync(selector);
336
337
  return [...names];
337
338
  };
339
+ const analyzeRootSelector = (sel, rootBlockName, options, patterns) => {
340
+ let hasSpiraClass = false;
341
+ let hasRootBlock = false;
342
+ let hasOtherBlock = false;
343
+ sel.walk((node) => {
344
+ if (node.type !== 'class')
345
+ return;
346
+ const name = node.value;
347
+ const kind = classify(name, options, patterns);
348
+ if (kind !== 'external')
349
+ hasSpiraClass = true;
350
+ if (name === rootBlockName)
351
+ hasRootBlock = true;
352
+ if (kind === 'block' && name !== rootBlockName)
353
+ hasOtherBlock = true;
354
+ });
355
+ return { hasSpiraClass, hasRootBlock, hasOtherBlock };
356
+ };
338
357
  const buildValuePattern = (naming) => {
339
358
  const maxWords = naming.maxWords;
340
359
  switch (naming.case) {
@@ -765,6 +784,7 @@ const spiracssClassStructure = stylelint.createPlugin(ruleName, (primaryOption,
765
784
  const interactionRules = markInteractionRules(root, commentPatterns);
766
785
  let firstRule = null;
767
786
  let rootBlockName = null;
787
+ const topLevelRules = [];
768
788
  const filePath = ((_a = result === null || result === void 0 ? void 0 : result.opts) === null || _a === void 0 ? void 0 : _a.from) || '';
769
789
  const normalizedPath = filePath ? path.normalize(filePath) : '';
770
790
  const pathSegments = normalizedPath ? normalizedPath.split(path.sep).filter(Boolean) : [];
@@ -793,6 +813,7 @@ const spiracssClassStructure = stylelint.createPlugin(ruleName, (primaryOption,
793
813
  });
794
814
  };
795
815
  if (!parentRule) {
816
+ topLevelRules.push(rule);
796
817
  const rootBlocks = collectRootBlockNames(rule.selector, options, patterns);
797
818
  if (rootBlocks.length > 0) {
798
819
  const rootName = rootBlockName || rootBlocks[0];
@@ -820,6 +841,28 @@ const spiracssClassStructure = stylelint.createPlugin(ruleName, (primaryOption,
820
841
  }));
821
842
  }).processSync(rule.selector);
822
843
  });
844
+ if (options.enforceSingleRootBlock && rootBlockName) {
845
+ const resolvedRootBlockName = rootBlockName;
846
+ topLevelRules.forEach((rule) => {
847
+ if (typeof rule.selector !== 'string')
848
+ return;
849
+ if (rule.selector.includes(':global'))
850
+ return;
851
+ selectorParser((selectors) => {
852
+ selectors.each((sel) => {
853
+ const { hasSpiraClass, hasRootBlock, hasOtherBlock } = analyzeRootSelector(sel, resolvedRootBlockName, options, patterns);
854
+ if (!hasSpiraClass || hasRootBlock || hasOtherBlock)
855
+ return;
856
+ stylelint.utils.report({
857
+ ruleName,
858
+ result,
859
+ node: rule,
860
+ message: messages.rootSelectorMissingBlock(resolvedRootBlockName, sel.toString().trim())
861
+ });
862
+ });
863
+ }).processSync(rule.selector);
864
+ });
865
+ }
823
866
  if (options.enforceRootFileName && rootBlockName && fileBase) {
824
867
  const isComponentsLayer = options.componentsDirs.some((dir) => {
825
868
  const normalizedDir = path.normalize(dir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spiracss/stylelint-plugin",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "SpiraCSS development utilities",
5
5
  "keywords": [
6
6
  "spiracss",
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  "publishConfig": {
15
15
  "access": "public",
16
- "tag": "beta"
16
+ "tag": "latest"
17
17
  },
18
18
  "license": "MIT",
19
19
  "engines": {