Skip to content

Commit c1bad54

Browse files
committed
fix: Add 'no-duplicate-state-groups' template validator.
- New validator ensures no two states from the same state group may be applied. - Fix bug in JSX state group application to analysis. Respects inheritance now. - Add inheritance aware children getters to Inheritable. - Update webpackLoader to have a default export, as expected by Webpack.
1 parent 5f59ed9 commit c1bad54

File tree

10 files changed

+287
-74
lines changed

10 files changed

+287
-74
lines changed

packages/css-blocks/src/Block/Inheritable.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,22 @@ export abstract class Inheritable<
181181
}
182182

183183
/**
184-
* Returns an array of all children nodes in the order they were added.
184+
* Returns an array of all children nodes in the order they were added for Self.
185185
* @returns The children array.
186186
*/
187187
protected children(): Child[] {
188188
return [...this._children.values()];
189189
}
190190

191+
/**
192+
* Returns an array of all children nodes in the order they were added for
193+
* self and all inherited children.
194+
* @returns The children array.
195+
*/
196+
protected resolveChildren(): Child[] {
197+
return [...this.resolveChildrenMap().values()];
198+
}
199+
191200
/**
192201
* Returns a map of all children nodes at the keys they are stored..
193202
* @returns The children map.
@@ -196,14 +205,41 @@ export abstract class Inheritable<
196205
return new Map(this._children);
197206
}
198207

208+
/**
209+
* Returns a map of all children nodes at the keys they are stored..
210+
* @returns The children map.
211+
*/
212+
protected resolveChildrenMap(): Map<string, Child> {
213+
let inheritance = [...this.resolveInheritance(), this.asSelf()];
214+
let out = new Map();
215+
for (let o of inheritance) {
216+
for (let [key, value] of o._children.entries()) {
217+
out.set(key, value);
218+
}
219+
}
220+
return out;
221+
}
222+
199223
/**
200224
* Returns a hash of all children nodes at the keys they are stored..
201225
* TODO: Cache this maybe? Convert entire model to only use hash?...
202226
* @returns The children hash.
203227
*/
204228
protected childrenHash(): ObjectDictionary<Child> {
205229
let out = {};
206-
for (let [key, value] of this._children.entries()) {
230+
for (let [key, value] of this._children) {
231+
out[key] = value;
232+
}
233+
return out;
234+
}
235+
236+
/**
237+
* Returns a map of all children nodes at the keys they are stored..
238+
* @returns The children map.
239+
*/
240+
protected resolveChildrenHash(): ObjectDictionary<Child> {
241+
let out = {};
242+
for (let [key, value] of this.resolveChildrenMap()) {
207243
out[key] = value;
208244
}
209245
return out;

packages/css-blocks/src/Block/StateGroup.ts

+2
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ export class StateGroup extends Inheritable<StateGroup, Block, BlockClass, State
5454
* @returns A hash of all `State`s contained in this `StateGroup`.
5555
**/
5656
statesHash(): ObjectDictionary<State> { return this.childrenHash(); }
57+
resolveStatesHash(): ObjectDictionary<State> { return this.resolveChildrenHash(); }
5758

5859
/**
5960
* @returns An Map of all `State`s contained in this `StateGroup`.
6061
**/
6162
statesMap(): Map<string, State> { return this.childrenMap(); }
63+
resolveStatesMap(): Map<string, State> { return this.resolveChildrenMap(); }
6264

6365
/**
6466
* Ensures that a state of name `name` exists in this State group. If no

packages/css-blocks/src/TemplateAnalysis/ElementAnalysis.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,7 @@ export class ElementAnalysis<BooleanExpression, StringExpression, TernaryExpress
449449
this.assertSealed(false);
450450
this.addedStyles.push({
451451
container,
452-
group: group.statesHash(),
452+
group: group.resolveStatesHash(),
453453
stringExpression,
454454
disallowFalsy,
455455
});

packages/css-blocks/src/TemplateAnalysis/validations/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Validator } from "./Validator";
77
import { classPairsValidator } from "./class-pairs-validator";
88
import { propertyConflictValidator } from "./property-conflict-validator";
99
import { rootClassValidator } from "./root-class-validator";
10+
import { stateGroupValidator } from "./state-group-validator";
1011
import { stateParentValidator } from "./state-parent-validator";
1112

1213
export * from "./class-pairs-validator";
@@ -18,6 +19,7 @@ export interface TemplateValidators {
1819
"no-root-classes": Validator;
1920
"no-class-pairs": Validator;
2021
"no-state-orphans": Validator;
22+
"no-duplicate-state-groups": Validator;
2123
"no-required-resolution": Validator;
2224
[name: string]: Validator;
2325
}
@@ -30,13 +32,15 @@ const VALIDATORS: TemplateValidators = {
3032
"no-root-classes": rootClassValidator,
3133
"no-class-pairs": classPairsValidator,
3234
"no-state-orphans": stateParentValidator,
35+
"no-duplicate-state-groups": stateGroupValidator,
3336
"no-required-resolution": propertyConflictValidator,
3437
};
3538

3639
const DEFAULT_VALIDATORS: TemplateValidatorOptions = {
3740
"no-root-classes": true,
3841
"no-class-pairs": true,
3942
"no-state-orphans": true,
43+
"no-duplicate-state-groups": true,
4044
"no-required-resolution": true,
4145
};
4246

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { unionInto } from "@opticss/util";
2+
3+
import { isState, StateGroup } from "../../Block";
4+
import { isBooleanState, isStateGroup } from "../ElementAnalysis";
5+
6+
import { ErrorCallback, Validator } from "./Validator";
7+
8+
/**
9+
* Verify that we are not applying multiple states from a single state group in the same `objstr` call.
10+
*/
11+
function ensureUniqueStateGroup(discovered: Set<StateGroup>, group: StateGroup, err: ErrorCallback, track: boolean): StateGroup[] {
12+
let groups = [...group.resolveInheritance(), group];
13+
for (let g of groups) {
14+
if (discovered.has(g)) {
15+
err(`Can not apply multiple states at the same time from the exclusive state group "${g.asSource()}".`);
16+
}
17+
if (track) { discovered.add(g); }
18+
}
19+
return groups;
20+
}
21+
22+
/**
23+
* Prevent State from being applied to an element without their associated class.
24+
* @param correlations The correlations object for a given element.
25+
* @param err Error callback.
26+
*/
27+
28+
export const stateGroupValidator: Validator = (analysis, _templateAnalysis, err) => {
29+
let discovered: Set<StateGroup> = new Set();
30+
for (let o of analysis.static) {
31+
if (isState(o)) {
32+
ensureUniqueStateGroup(discovered, o.parent, err, true);
33+
}
34+
}
35+
for (let stat of analysis.dynamicStates) {
36+
if (isBooleanState(stat)) {
37+
ensureUniqueStateGroup(discovered, stat.state.parent, err, true);
38+
}
39+
if (isStateGroup(stat)) {
40+
let tmp: Set<StateGroup> = new Set();
41+
for (let key of Object.keys(stat.group)) {
42+
let state = stat.group[key];
43+
let vals = ensureUniqueStateGroup(discovered, state.parent, err, false);
44+
vals.forEach((o) => tmp.add(o));
45+
}
46+
unionInto(discovered, tmp);
47+
}
48+
}
49+
};

packages/css-blocks/test/Block/BlockTree/block-tree-test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ class TestSource extends Inheritable<
5050

5151
childNodeMap: RootNode["childrenMap"] =
5252
() => this.childrenMap()
53+
54+
resolveChildNodes: RootNode["children"] =
55+
() => this.resolveChildren()
56+
57+
resolveChildNodeHash: RootNode["childrenHash"] =
58+
() => this.resolveChildrenHash()
59+
60+
resolveChildNodeMap: RootNode["childrenMap"] =
61+
() => this.resolveChildrenMap()
5362
}
5463

5564
type ContainerNode = Inheritable<TestNode, TestSource, TestSource, TestSink>;
@@ -191,6 +200,13 @@ export class BlockTreeTests {
191200
assert.equal(source.getChildNode("base-only"), null);
192201
assert.equal(source.resolveChildNode("base-only"), baseUnique);
193202
assert.deepEqual(child.resolveInheritance(), [baseChild]);
203+
204+
assert.deepEqual(source.childNodes(), [child]);
205+
assert.deepEqual(source.childNodeHash(), {"child": child});
206+
assert.deepEqual([...source.childNodeMap().values()], [child]);
207+
assert.deepEqual(source.resolveChildNodes(), [child, baseUnique]);
208+
assert.deepEqual(source.resolveChildNodeHash(), { child, "base-only": baseUnique });
209+
assert.deepEqual([...source.resolveChildNodeMap().values()], [child, baseUnique]);
194210
}
195211

196212
@test "setBase creates inheritance tree for grandchildren"() {

packages/css-blocks/test/template-analysis-test.ts

-62
Original file line numberDiff line numberDiff line change
@@ -276,68 +276,6 @@ export class TemplateAnalysisTests {
276276
});
277277
}
278278

279-
@test "adding the same styles more than once doesn't duplicate the styles found"() {
280-
let info = new Template("templates/my-template.hbs");
281-
let analysis = new TemplateAnalysis(info);
282-
let imports = new MockImportRegistry();
283-
284-
let options: PluginOptions = { importer: imports.importer() };
285-
let reader = new OptionsReader(options);
286-
287-
let css = `
288-
[state|color=red] { color: red; }
289-
[state|color=blue] { color: blue; }
290-
[state|bgcolor=red] { color: red; }
291-
[state|bgcolor=blue] { color: blue; }
292-
`;
293-
return this.parseBlock(css, "blocks/foo.block.css", reader).then(([block, _]) => {
294-
analysis.blocks[""] = block;
295-
let element: TestElement = analysis.startElement({ line: 10, column: 32 });
296-
element.addStaticClass(block.rootClass);
297-
element.addDynamicGroup(block.rootClass, block.rootClass.resolveGroup("color") as StateGroup, null);
298-
element.addDynamicGroup(block.rootClass, block.rootClass.resolveGroup("color") as StateGroup, null, true);
299-
analysis.endElement(element);
300-
301-
let result = analysis.serialize();
302-
let expectedResult: SerializedTemplateAnalysis<"Opticss.Template"> = {
303-
blocks: {"": "blocks/foo.block.css"},
304-
template: { type: "Opticss.Template", identifier: "templates/my-template.hbs"},
305-
stylesFound: [
306-
".root",
307-
"[state|color=blue]",
308-
"[state|color=red]",
309-
],
310-
elements: {
311-
"a": {
312-
"sourceLocation": {
313-
"start": { filename: "templates/my-template.hbs", "column": 32, "line": 10 },
314-
},
315-
"staticStyles": [ 0 ],
316-
"dynamicClasses": [],
317-
"dynamicStates": [
318-
{
319-
"stringExpression": true,
320-
"group": {
321-
"blue": 1,
322-
"red": 2,
323-
},
324-
},
325-
{
326-
"stringExpression": true,
327-
"disallowFalsy": true,
328-
"group": {
329-
"blue": 1,
330-
"red": 2,
331-
},
332-
},
333-
],
334-
},
335-
},
336-
};
337-
338-
assert.deepEqual(result, expectedResult);
339-
});
340-
}
341279
@test "multiple exclusive dynamic values added using enumerate correlations correctly in analysis"() {
342280
let info = new Template("templates/my-template.hbs");
343281
let analysis = new TemplateAnalysis(info);

0 commit comments

Comments
 (0)