Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit6f87d27

Browse files
authored
feat: Better BEM block definition in all BEM rules (#22)
1 parent069b86e commit6f87d27

File tree

13 files changed

+301
-96
lines changed

13 files changed

+301
-96
lines changed

‎src/create-define-rules.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ type PluginConfig = {
1616
rules?:Partial<RulesSchema>;
1717
};
1818

19+
//@keep-sorted
1920
construlesWithSeparators=[
2021
'@morev/bem/block-variable',
22+
'@morev/bem/match-file-name',
2123
'@morev/bem/no-block-properties',
2224
'@morev/bem/no-chained-entities',
25+
'@morev/bem/no-side-effects',
2326
'@morev/bem/selector-pattern',
2427
];
2528

Lines changed: 100 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,121 @@
11
import{parse,Rule}from'postcss';
2+
import{DEFAULT_SEPARATORS}from'#modules/bem/constants';
23
import{getBemBlock}from'./get-bem-block';
34

45
describe(getBemBlock,()=>{
5-
it('Returns null when no rules present',()=>{
6-
constroot=parse(``);
6+
describe('Negative scenarios',()=>{
7+
it('Returns `null` when no rules present',()=>{
8+
constroot=parse(``);
79

8-
expect(getBemBlock(root)).toBeNull();
9-
});
10-
11-
it('Returns null when no class selectors present',()=>{
12-
constroot=parse(`body { color: red; }`);
13-
14-
expect(getBemBlock(root)).toBeNull();
15-
});
10+
expect(getBemBlock(root,DEFAULT_SEPARATORS)).toBeNull();
11+
});
1612

17-
it('Returns null forruleswithmultiple selectors',()=>{
18-
constroot=parse(`.a, .b { color: red; }`);
13+
it('Returns`null` forclass selectorwithonly "."',()=>{
14+
constroot=parse(`. { color: red; }`);
1915

20-
expect(getBemBlock(root)).toBeNull();
21-
});
22-
23-
it('Returns null for class selector with only "."',()=>{
24-
constroot=parse(`. { color: red; }`);
25-
26-
expect(getBemBlock(root)).toBeNull();
27-
});
16+
expect(getBemBlock(root,DEFAULT_SEPARATORS)).toBeNull();
17+
});
2818

29-
it('Returns correct block for simple class selector',()=>{
30-
constroot=parse(`.block { color: red; }`);
31-
constresult=getBemBlock(root);
19+
it('Returns `null` when no class selectors present',()=>{
20+
constroot=parse(`body { color: red; }`);
3221

33-
expect(result?.blockName).toBe('block');
34-
expect(result?.selector).toBe('.block');
35-
expect(result?.rule).toBeInstanceOf(Rule);
36-
});
22+
expect(getBemBlock(root,DEFAULT_SEPARATORS)).toBeNull();
23+
});
3724

38-
it('Returns correct block for selector like "html .block"',()=>{
39-
constroot=parse(`html .block { color: red; }`);
40-
constresult=getBemBlock(root);
25+
it('Returns `null` for rules that contain more than just classes',()=>{
26+
constroot=parse(`#foo, .foo-component { color: red; }`);
4127

42-
expect(result?.blockName).toBe('block');
43-
expect(result?.selector).toBe('.block');
44-
expect(result?.rule).toBeInstanceOf(Rule);
45-
});
28+
expect(getBemBlock(root,DEFAULT_SEPARATORS)).toBeNull();
29+
});
4630

47-
it('Skips rules inside disallowed at-rules',()=>{
48-
constroot=parse(`
49-
@supports (display: grid) {
50-
.block { color: red; }
51-
}
52-
`);
31+
it('Returns `null` for multiple classes if they belong to different bem blocks',()=>{
32+
constroot=parse(`.a, .b { color: red; }`);
5333

54-
expect(getBemBlock(root)).toBeNull();
55-
});
34+
expect(getBemBlock(root,DEFAULT_SEPARATORS)).toBeNull();
35+
});
5636

57-
it('Allows rules inside @layer',()=>{
58-
constroot=parse(`
59-
@layer components {
60-
.block { color: red; }
61-
}
62-
`);
63-
constresult=getBemBlock(root);
37+
it('Skips rules inside disallowed at-rules',()=>{
38+
constroot=parse(`
39+
@supports (display: grid) {
40+
.block { color: red; }
41+
}
42+
`);
6443

65-
expect(result?.blockName).toBe('block');
66-
expect(result?.selector).toBe('.block');
67-
expect(result?.rule).toBeInstanceOf(Rule);
44+
expect(getBemBlock(root,DEFAULT_SEPARATORS)).toBeNull();
45+
});
6846
});
6947

70-
it('Allows rules inside @media',()=>{
71-
constroot=parse(`
72-
@media (width >= 768px) {
48+
describe('Positive scenarios',()=>{
49+
it('Returns correct block for simple class selector',()=>{
50+
constroot=parse(`.block { color: red; }`);
51+
constresult=getBemBlock(root,DEFAULT_SEPARATORS);
52+
53+
expect(result?.blockName).toBe('block');
54+
expect(result?.selector).toBe('.block');
55+
expect(result?.rule).toBeInstanceOf(Rule);
56+
});
57+
58+
it('Returns correct block for a BEM element',()=>{
59+
constroot=parse(`.block__element { color: red; }`);
60+
constresult=getBemBlock(root,DEFAULT_SEPARATORS);
61+
62+
expect(result?.blockName).toBe('block');
63+
expect(result?.selector).toBe('.block');
64+
expect(result?.rule).toBeInstanceOf(Rule);
65+
});
66+
67+
it('Returns correct block for multiple selectors of the same block',()=>{
68+
constroot=parse(`.block--modifier, .block { color: red; }`);
69+
constresult=getBemBlock(root,DEFAULT_SEPARATORS);
70+
71+
expect(result?.blockName).toBe('block');
72+
expect(result?.selector).toBe('.block');
73+
expect(result?.rule).toBeInstanceOf(Rule);
74+
});
75+
76+
it('Returns correct block for a selector like "html .block"',()=>{
77+
constroot=parse(`html .block { color: red; }`);
78+
constresult=getBemBlock(root,DEFAULT_SEPARATORS);
79+
80+
expect(result?.blockName).toBe('block');
81+
expect(result?.selector).toBe('.block');
82+
expect(result?.rule).toBeInstanceOf(Rule);
83+
});
84+
85+
it('Allows rules inside `@layer`',()=>{
86+
constroot=parse(`
87+
@layer components {
88+
.block { color: red; }
89+
}
90+
`);
91+
constresult=getBemBlock(root,DEFAULT_SEPARATORS);
92+
93+
expect(result?.blockName).toBe('block');
94+
expect(result?.selector).toBe('.block');
95+
expect(result?.rule).toBeInstanceOf(Rule);
96+
});
97+
98+
it('Allows rules inside `@media`',()=>{
99+
constroot=parse(`
100+
@media (width >= 768px) {
101+
.block { color: red; }
102+
}
103+
`);
104+
constresult=getBemBlock(root,DEFAULT_SEPARATORS);
105+
106+
expect(result?.blockName).toBe('block');
107+
expect(result?.selector).toBe('.block');
108+
expect(result?.rule).toBeInstanceOf(Rule);
109+
});
110+
111+
it('Returns first valid block and ignores others',()=>{
112+
constroot=parse(`
73113
.block { color: red; }
74-
}
75-
`);
76-
constresult=getBemBlock(root);
77-
78-
expect(result?.blockName).toBe('block');
79-
expect(result?.selector).toBe('.block');
80-
expect(result?.rule).toBeInstanceOf(Rule);
81-
});
82-
83-
it('Returns first valid block and ignores others',()=>{
84-
constroot=parse(`
85-
.block { color: red; }
86-
.another { color: blue; }
87-
`);
88-
constresult=getBemBlock(root);
114+
.another { color: blue; }
115+
`);
116+
constresult=getBemBlock(root,DEFAULT_SEPARATORS);
89117

90-
expect(result?.blockName).toBe('block');
118+
expect(result?.blockName).toBe('block');
119+
});
91120
});
92121
});

‎src/modules/bem/utils/get-bem-block/get-bem-block.ts‎

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import{resolveBemEntities}from'#modules/bem/utils/resolve-bem-entities/resolve-bem-entities';
2+
import{isAtRule}from'#modules/postcss';
13
import{parseSelectors}from'#modules/selectors';
2-
importtype{AtRule,Root,Rule}from'postcss';
4+
importtype{Root,Rule}from'postcss';
5+
importtype{Separators}from'#modules/shared';
36

47
typeBemBlock={
58
rule:Rule;
@@ -16,41 +19,57 @@ type BemBlock = {
1619
* * Rule is not nested inside any at-rule other than `@layer` or `@media`.
1720
* * Selectors like `html .the-component` are allowed; only the last part matters.
1821
*
19-
*@param root PostCSS `Root` object.
22+
*@param root PostCSS `Root` object.
23+
*@param separators BEM separators used in the current config.
2024
*
21-
*@returns The first BEM block definition if found, otherwise `null`.
25+
*@returnsThe first BEM block definition if found, otherwise `null`.
2226
*/
23-
exportconstgetBemBlock=(root:Root):BemBlock|null=>{
27+
exportconstgetBemBlock=(
28+
root:Root,
29+
separators:Separators,
30+
):BemBlock|null=>{
2431
letresult:BemBlock|null=null;
2532

2633
root.walkRules((rule)=>{
2734
// PostCSS does not provide a way to break `walkRules`,
2835
// so we rely on early return.
2936
if(result)return;
3037

31-
// Skip rules with multiple selectors (e.g., `.the-component, .foo`) -
32-
// definitely not what we're looking for.
33-
if(rule.selectors.length>1)return;
34-
35-
// Skip rules inside disallowed at-rules
38+
// Skip rules inside disallowed at-rules.
3639
if(
37-
rule.parent?.type==='atrule'
38-
&&(rule.parentasAtRule).name!=='layer'
39-
&&(rule.parentasAtRule).name!=='media'
40+
isAtRule(rule.parent)
41+
&&rule.parent.name!=='layer'
42+
&&rule.parent.name!=='media'
4043
)return;
4144

42-
constlastSelector=parseSelectors(rule.selector)[0]?.at(-1)!.toString();
45+
constblockCandidates=rule.selectors.map((selector)=>{
46+
constlastNodeString=parseSelectors(selector)[0]?.at(-1)!.toString();
47+
// Only class selectors allowed.
48+
if(!lastNodeString.startsWith('.'))return;
49+
// Do not validate while writing the selector `.|`.
50+
if(lastNodeString.length===1)return;
51+
52+
returnlastNodeString;
53+
});
54+
55+
// A scenario like `.foo-component, #bar`
56+
if(blockCandidates.includes(undefined))return;
57+
58+
constallBemEntities=blockCandidates
59+
.filter(Boolean)
60+
.map((source)=>resolveBemEntities({ source, separators})[0]);
4361

44-
// Only class selectors allowed
45-
if(!lastSelector.startsWith('.'))return;
62+
// `.foo-component, .foo-component--modifier {}`
63+
constallSame=allBemEntities
64+
.every((bemEntity)=>bemEntity.block.value===allBemEntities[0].block.value);
4665

47-
// Do not validate while writing the selector.
48-
if(lastSelector.length===1)return;
66+
if(!allSame)return;
4967

68+
constbemBlock=allBemEntities[0].block;
5069
result={
5170
rule,
52-
blockName:lastSelector.slice(1),
53-
selector:lastSelector,
71+
blockName:bemBlock.value,
72+
selector:bemBlock.selector,
5473
};
5574
});
5675

‎src/rules/bem/block-variable/block-variable.docs.md‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ export default {
7777
interpolation:'always',
7878
firstChild:true,
7979
replaceBlockName:true,
80+
separators: {
81+
element:'__',
82+
modifier:'--',
83+
modifierValue:'--',
84+
},
8085
messages: {
8186
missingVariable: (validName)=>
8287
`Missing block reference variable "${validName}".`
@@ -121,6 +126,33 @@ type BlockVariableOptions = {
121126
*/
122127
replaceBlockName?:boolean;
123128

129+
/**
130+
* Object that defines BEM separators used to distinguish blocks, elements, modifiers, and modifier values. \
131+
* This allows the rule to work correctly with non-standard BEM naming conventions.
132+
*/
133+
separators?: {
134+
/**
135+
* String used as the BEM element separator.
136+
*
137+
*@default'__'
138+
*/
139+
element?:string;
140+
141+
/**
142+
* String used as the BEM modifier separator.
143+
*
144+
*@default'--'
145+
*/
146+
modifier?:string;
147+
148+
/**
149+
* String used as the BEM modifier value separator.
150+
*
151+
*@default'--'
152+
*/
153+
modifierValue?:string;
154+
}
155+
124156
/**
125157
* Custom message functions for rule violations.
126158
* If provided, they override the default error messages.
@@ -443,6 +475,12 @@ Ambiguous cases (e.g. root-level selectors without `&`, where both `&--mod` and
443475

444476
---
445477

478+
###`separators`
479+
480+
<!-- @include: @/docs/_parts/separators.md#header-->
481+
482+
---
483+
446484
###`messages`
447485

448486
<!-- @include: @/docs/_parts/custom-messages.md#header-->

‎src/rules/bem/block-variable/block-variable.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@ export default createRule({
103103
// The rule only applicable to `scss` files.
104104
if(isCssFile(root))return;
105105

106-
constbemBlock=getBemBlock(root);
106+
constseparators=extractSeparators(secondary.separators);
107+
constbemBlock=getBemBlock(root,separators);
107108
if(!bemBlock)return;
108109

109110
constmessages=mergeMessages(ruleMessages,secondary.messages);
110-
constseparators=extractSeparators(secondary.separators);
111111

112112
constVARIABLE_NAME=`$${secondary.name.replace(/^\$/,'')}`;
113113
constVALID_VALUES=(()=>{

‎src/rules/bem/block-variable/block-variable.types.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
importtype{Separators}from'#modules/shared';
2+
13
/**
24
* Primary option of the rule.
35
*
@@ -36,6 +38,14 @@ export type SecondaryOption = {
3638
*/
3739
replaceBlockName?:boolean;
3840

41+
/**
42+
* Object that defines BEM separators used to distinguish blocks, elements, modifiers, and modifier values. \
43+
* This allows the rule to work correctly with non-standard BEM naming conventions.
44+
*
45+
*@default { element: '__', modifier: '--', modifierValue: '--' }
46+
*/
47+
separators?:Partial<Separators>;
48+
3949
/**
4050
* Custom message functions for rule violations.
4151
* If provided, they override the default error messages.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp