Skip to content

Commit 0bc4e0a

Browse files
authored
Validate tokens match corresponding theme contracts (#84)
1 parent 84f3635 commit 0bc4e0a

File tree

8 files changed

+227
-7
lines changed

8 files changed

+227
-7
lines changed

.changeset/spotty-crabs-rule.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@vanilla-extract/css': patch
3+
'@vanilla-extract/private': patch
4+
---
5+
6+
Validate tokens match corresponding theme contracts

packages/css/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@
3333
"dependencies": {
3434
"@emotion/hash": "^0.8.0",
3535
"@vanilla-extract/private": "^0.1.1",
36+
"chalk": "^4.1.1",
3637
"css-selector-parser": "^1.4.1",
3738
"cssesc": "^3.0.0",
3839
"csstype": "^3.0.7",
39-
"dedent": "^0.7.0"
40+
"dedent": "^0.7.0",
41+
"deep-object-diff": "^1.1.0"
4042
},
4143
"devDependencies": {
4244
"@types/cssesc": "^3.0.0",
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { validateContract } from './validateContract';
2+
3+
describe('validateContract', () => {
4+
it('should return valid when tokens match contract', () => {
5+
const contract = {
6+
colors: {
7+
red: '',
8+
green: '',
9+
blue: '',
10+
},
11+
space: {
12+
1: '',
13+
2: '',
14+
3: '',
15+
},
16+
};
17+
18+
expect(validateContract(contract, contract).valid).toBe(true);
19+
});
20+
21+
it('should show nice diff for added properties', () => {
22+
const contract = {
23+
colors: {
24+
red: '',
25+
green: '',
26+
blue: '',
27+
},
28+
space: {
29+
1: '',
30+
2: '',
31+
3: '',
32+
},
33+
};
34+
35+
const { valid, diffString } = validateContract(contract, {
36+
...contract,
37+
fontWeight: { 300: '300' },
38+
});
39+
40+
expect(valid).toBe(false);
41+
expect(diffString).toMatchInlineSnapshot(`
42+
" {
43+
+ fontWeight: ...,
44+
}"
45+
`);
46+
});
47+
48+
it('should show nice diff for removed properties', () => {
49+
const contract = {
50+
colors: {
51+
red: '',
52+
green: '',
53+
blue: '',
54+
},
55+
space: {
56+
1: '',
57+
2: '',
58+
3: '',
59+
},
60+
};
61+
62+
const { valid, diffString } = validateContract(contract, {
63+
...contract,
64+
colors: undefined,
65+
});
66+
67+
expect(valid).toBe(false);
68+
expect(diffString).toMatchInlineSnapshot(`
69+
" {
70+
- colors: ...,
71+
}"
72+
`);
73+
});
74+
75+
it('should show nice diff for mixed properties', () => {
76+
const contract = {
77+
colors: {
78+
red: '',
79+
green: '',
80+
blue: '',
81+
},
82+
space: {
83+
1: '',
84+
2: '',
85+
3: '',
86+
},
87+
};
88+
89+
const { valid, diffString } = validateContract(contract, {
90+
...contract,
91+
colors: undefined,
92+
fontWeight: { 300: '300' },
93+
});
94+
95+
expect(valid).toBe(false);
96+
expect(diffString).toMatchInlineSnapshot(`
97+
" {
98+
- colors: ...,
99+
+ fontWeight: ...,
100+
}"
101+
`);
102+
});
103+
104+
it('should show nice diff for missing nested properties', () => {
105+
const contract = {
106+
colors: {
107+
red: '',
108+
green: '',
109+
blue: '',
110+
},
111+
space: {
112+
1: '',
113+
2: '',
114+
3: '',
115+
},
116+
};
117+
118+
const { valid, diffString } = validateContract(contract, {
119+
...contract,
120+
colors: {
121+
red: '',
122+
blue: '',
123+
},
124+
});
125+
126+
expect(valid).toBe(false);
127+
expect(diffString).toMatchInlineSnapshot(`
128+
" {
129+
colors: {
130+
- green: ...,
131+
}
132+
}"
133+
`);
134+
});
135+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Contract, walkObject } from '@vanilla-extract/private';
2+
import { diff } from 'deep-object-diff';
3+
import chalk from 'chalk';
4+
5+
const normaliseObject = (obj: Contract) => walkObject(obj, () => '');
6+
7+
export function validateContract(contract: any, tokens: any) {
8+
const theDiff = diff(normaliseObject(contract), normaliseObject(tokens));
9+
const valid = Object.keys(theDiff).length === 0;
10+
11+
return {
12+
valid,
13+
diffString: valid ? '' : renderDiff(contract, theDiff),
14+
};
15+
}
16+
17+
function diffLine(value: string, nesting: number, type?: '+' | '-') {
18+
const whitespace = [...Array(nesting).keys()].map(() => ' ').join('');
19+
const line = `${type ? type : ' '}${whitespace}${value}`;
20+
21+
if (process.env.NODE_ENV !== 'test') {
22+
if (type === '-') {
23+
return chalk.red(line);
24+
}
25+
26+
if (type === '+') {
27+
return chalk.green(line);
28+
}
29+
}
30+
31+
return line;
32+
}
33+
34+
function renderDiff(orig: any, diff: any, nesting: number = 0): string {
35+
const lines = [];
36+
37+
if (nesting === 0) {
38+
lines.push(diffLine('{', 0));
39+
}
40+
41+
const innerNesting = nesting + 1;
42+
43+
const keys = Object.keys(diff).sort();
44+
45+
for (const key of keys) {
46+
const value = diff[key];
47+
48+
if (!(key in orig)) {
49+
lines.push(diffLine(`${key}: ...,`, innerNesting, '+'));
50+
} else if (typeof value === 'object') {
51+
lines.push(diffLine(`${key}: {`, innerNesting));
52+
53+
lines.push(renderDiff(orig[key], diff[key], innerNesting));
54+
55+
lines.push(diffLine('}', innerNesting));
56+
} else {
57+
lines.push(diffLine(`${key}: ...,`, innerNesting, '-'));
58+
}
59+
}
60+
61+
if (nesting === 0) {
62+
lines.push(diffLine('}', 0));
63+
}
64+
return lines.join('\n');
65+
}

packages/css/src/vars.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import cssesc from 'cssesc';
99

1010
import { CSSVarFunction, ThemeVars } from './types';
1111
import { getAndIncrementRefCounter, getFileScope } from './fileScope';
12+
import { validateContract } from './validateContract';
1213

1314
export function createVar(debugId?: string): CSSVarFunction {
1415
// Convert ref count to base 36 for optimal hash lengths
@@ -58,11 +59,12 @@ export function assignVars<VarContract extends Contract>(
5859
tokens: MapLeafNodes<VarContract, string>,
5960
): Record<CSSVarFunction, string> {
6061
const varSetters: { [cssVarName: string]: string } = {};
62+
const { valid, diffString } = validateContract(varContract, tokens);
63+
64+
if (!valid) {
65+
throw new Error(`Tokens don't match contract.\n${diffString}`);
66+
}
6167

62-
/* TODO
63-
- validate new variables arn't set
64-
- validate arrays have the same length as contract
65-
*/
6668
walkObject(tokens, (value, path) => {
6769
varSetters[get(varContract, path)] = String(value);
6870
});

packages/private/src/get.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ export function get(obj: any, path: Array<string>) {
22
let result = obj;
33

44
for (const key of path) {
5+
if (!(key in result)) {
6+
throw new Error(`Path ${path.join(' -> ')} does not exist in object`);
7+
}
58
result = result[key];
69
}
710

packages/webpack-plugin/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
"webpack": "^4.30.0 || ^5.20.2"
3030
},
3131
"dependencies": {
32-
"@vanilla-extract/css": "^0.4.0",
3332
"@vanilla-extract/integration": "^0.1.0",
3433
"chalk": "^4.1.1",
3534
"debug": "^4.3.1",

yarn.lock

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2816,10 +2816,12 @@ __metadata:
28162816
"@types/cssesc": ^3.0.0
28172817
"@types/dedent": ^0.7.0
28182818
"@vanilla-extract/private": ^0.1.1
2819+
chalk: ^4.1.1
28192820
css-selector-parser: ^1.4.1
28202821
cssesc: ^3.0.0
28212822
csstype: ^3.0.7
28222823
dedent: ^0.7.0
2824+
deep-object-diff: ^1.1.0
28232825
languageName: unknown
28242826
linkType: soft
28252827

@@ -2887,7 +2889,6 @@ __metadata:
28872889
resolution: "@vanilla-extract/webpack-plugin@workspace:packages/webpack-plugin"
28882890
dependencies:
28892891
"@types/debug": ^4.1.5
2890-
"@vanilla-extract/css": ^0.4.0
28912892
"@vanilla-extract/integration": ^0.1.0
28922893
chalk: ^4.1.1
28932894
debug: ^4.3.1
@@ -4994,6 +4995,13 @@ __metadata:
49944995
languageName: node
49954996
linkType: hard
49964997

4998+
"deep-object-diff@npm:^1.1.0":
4999+
version: 1.1.0
5000+
resolution: "deep-object-diff@npm:1.1.0"
5001+
checksum: 2667c43932d14c908d03f813cd2846a626bda1d320f8f1dee3c7bd00ff8058713bb8f5d995fabd617e09229cf64bf16e53579233e29da870fd27bde23d5f5b20
5002+
languageName: node
5003+
linkType: hard
5004+
49975005
"deepmerge@npm:^4.2.2":
49985006
version: 4.2.2
49995007
resolution: "deepmerge@npm:4.2.2"

0 commit comments

Comments
 (0)