Skip to content

Commit d360fef

Browse files
committed
Add 'merge' style function to web
Optimize the classname merging function.
1 parent 165b2db commit d360fef

12 files changed

Lines changed: 417 additions & 35 deletions

File tree

apps/expo-app/app.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"expo": {
3-
"name": "example-expo",
4-
"slug": "example-expo",
3+
"name": "expo-app",
4+
"slug": "expo-app",
55
"version": "1.0.0",
66
"orientation": "portrait",
77
"icon": "./assets/icon.png",

apps/platform-tests/app.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"expo": {
3-
"name": "example-expo",
4-
"slug": "example-expo",
3+
"name": "platform-tests",
4+
"slug": "platform-tests",
55
"version": "1.0.0",
66
"orientation": "portrait",
77
"icon": "./assets/icon.png",

flow-typed/npm/styleq.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
*/
9+
10+
type InlineStyle = {
11+
[key: string]: string | number
12+
};
13+
14+
type StylesArray<+T> = T | $ReadOnlyArray<StylesArray<T>>;
15+
type Styles = StylesArray<?{ ... } | boolean>;
16+
type Style<+T = { ... }> = StylesArray<boolean | ?T>;
17+
18+
/*
19+
type CompiledStyle = {
20+
$$css: boolean,
21+
[key: string]: string,
22+
};
23+
type EitherStyle = CompiledStyle | InlineStyle;
24+
25+
type Styles = StylesArray<EitherStyle | false | void>;
26+
type Style<+T = EitherStyle> = StylesArray<false | ?T>;
27+
*/
28+
29+
type StyleqOptions = {
30+
disableCache?: boolean,
31+
disableMix?: boolean,
32+
transform?: (mixed) => { ... }
33+
// transform?: (EitherStyle) => EitherStyle,
34+
};
35+
36+
type StyleqResult = [
37+
className: string,
38+
inlineStyle: InlineStyle | null,
39+
dataStyleSrc: string | null
40+
];
41+
42+
type Styleq = (styles: Styles) => StyleqResult;
43+
44+
type IStyleq = {
45+
(...styles: $ReadOnlyArray<Styles>): StyleqResult,
46+
factory: (options?: StyleqOptions) => Styleq
47+
};
48+
49+
declare module 'styleq' {
50+
declare module.exports: {
51+
styleq: IStyleq
52+
};
53+
}

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-strict-dom/babel/plugin.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ function reactStrictPlugin({ types: t }, options = {}) {
5353
);
5454
styleResolverImportIdentifier = addNamed(
5555
path,
56-
'resolveStyle',
56+
'merge',
5757
packageRuntime,
58-
{ nameHint: '_rsdResolveStyle' }
58+
{ nameHint: '_rsdMerge' }
5959
);
6060
// No need to rename as addNamed already creates unique identifiers
6161
}

packages/react-strict-dom/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"@stylexjs/babel-plugin": "^0.14.1",
4040
"@stylexjs/stylex": "^0.14.1",
4141
"@stylexjs/postcss-plugin": "^0.14.1",
42-
"postcss-value-parser": "^4.1.0"
42+
"postcss-value-parser": "^4.1.0",
43+
"styleq": "^0.2.1"
4344
},
4445
"devDependencies": {
4546
"@rollup/plugin-alias": "^5.1.0",
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
'use strict';
11+
12+
import { merge } from '../merge';
13+
14+
/**
15+
* Based on https://github.com/necolas/styleq/blob/main/test/styleq.test.js
16+
*
17+
* Copyright (c) Nicolas Gallagher
18+
*
19+
* This source code is licensed under the MIT license found in the
20+
* LICENSE file in the root directory of this source tree.
21+
*/
22+
23+
function stringifyInlineStyle(inlineStyle) {
24+
let str = '';
25+
Object.keys(inlineStyle).forEach((prop) => {
26+
const value = inlineStyle[prop];
27+
str += `${prop}:${value};`;
28+
});
29+
return str;
30+
}
31+
32+
describe('merge()', () => {
33+
describe('invalid values', () => {
34+
beforeAll(() => {
35+
jest.spyOn(global.console, 'error').mockImplementation((msg) => {
36+
throw new Error(msg);
37+
});
38+
});
39+
afterAll(() => {
40+
global.console.error.mockRestore();
41+
});
42+
43+
test('error if extracted property values are not strings or null', () => {
44+
expect(() => merge({ $$css: true, a: 1 })).toThrow();
45+
expect(() => merge({ $$css: true, a: undefined })).toThrow();
46+
expect(() => merge({ $$css: true, a: false })).toThrow();
47+
expect(() => merge({ $$css: true, a: true })).toThrow();
48+
expect(() => merge({ $$css: true, a: {} })).toThrow();
49+
expect(() => merge({ $$css: true, a: [] })).toThrow();
50+
expect(() => merge({ $$css: true, a: new Date() })).toThrow();
51+
});
52+
});
53+
54+
test('combines different class names', () => {
55+
const style = { $$css: true, a: 'aaa', b: 'bbb' };
56+
expect(merge(style).className).toBe('aaa bbb');
57+
});
58+
59+
test('combines different class names in order', () => {
60+
const a = { $$css: true, a: 'a', ':focus$aa': 'focus$aa' };
61+
const b = { $$css: true, b: 'b' };
62+
const c = { $$css: true, c: 'c', ':focus$cc': 'focus$cc' };
63+
expect(merge([a, b, c]).className).toBe('a focus$aa b c focus$cc');
64+
});
65+
66+
test('dedupes class names for the same key', () => {
67+
const a = { $$css: true, backgroundColor: 'backgroundColor-a' };
68+
const b = { $$css: true, backgroundColor: 'backgoundColor-b' };
69+
const c = { $$css: true, backgroundColor: 'backgoundColor-c' };
70+
expect(merge([a, b]).className).toEqual('backgoundColor-b');
71+
// Tests memoized result of [a,b] is correct
72+
expect(merge([c, a, b]).className).toEqual('backgoundColor-b');
73+
});
74+
75+
test('dedupes class names with "null" value', () => {
76+
const a = { $$css: true, backgroundColor: 'backgroundColor-a' };
77+
const b = { $$css: true, backgroundColor: null };
78+
expect(merge([a, b]).className).toEqual(undefined);
79+
});
80+
81+
test('dedupes class names in complex merges', () => {
82+
const styles = {
83+
a: {
84+
$$css: true,
85+
backgroundColor: 'backgroundColor-a',
86+
borderColor: 'borderColor-a',
87+
borderStyle: 'borderStyle-a',
88+
borderWidth: 'borderWidth-a',
89+
boxSizing: 'boxSizing-a',
90+
display: 'display-a',
91+
listStyle: 'listStyle-a',
92+
marginTop: 'marginTop-a',
93+
marginEnd: 'marginEnd-a',
94+
marginBottom: 'marginBottom-a',
95+
marginStart: 'marginStart-a',
96+
paddingTop: 'paddingTop-a',
97+
paddingEnd: 'paddingEnd-a',
98+
paddingBottom: 'paddingBottom-a',
99+
paddingStart: 'paddingStart-a',
100+
textAlign: 'textAlign-a',
101+
textDecoration: 'textDecoration-a',
102+
whiteSpace: 'whiteSpace-a',
103+
wordWrap: 'wordWrap-a',
104+
zIndex: 'zIndex-a'
105+
},
106+
b: {
107+
$$css: true,
108+
cursor: 'cursor-b',
109+
touchAction: 'touchAction-b'
110+
},
111+
c: {
112+
$$css: true,
113+
outline: 'outline-c'
114+
},
115+
d: {
116+
$$css: true,
117+
cursor: 'cursor-d',
118+
touchAction: 'touchAction-d'
119+
},
120+
e: {
121+
$$css: true,
122+
textDecoration: 'textDecoration-e',
123+
':focus$textDecoration': 'focus$textDecoration-e'
124+
},
125+
f: {
126+
$$css: true,
127+
backgroundColor: 'backgroundColor-f',
128+
color: 'color-f',
129+
cursor: 'cursor-f',
130+
display: 'display-f',
131+
marginEnd: 'marginEnd-f',
132+
marginStart: 'marginStart-f',
133+
textAlign: 'textAlign-f',
134+
textDecoration: 'textDecoration-f',
135+
':focus$color': 'focus$color-f',
136+
':focus$textDecoration': 'focus$textDecoration-f',
137+
':active$transform': 'active$transform-f',
138+
':active$transition': 'active$transition-f'
139+
},
140+
g: {
141+
$$css: true,
142+
display: 'display-g',
143+
width: 'width-g'
144+
},
145+
h: {
146+
$$css: true,
147+
':active$transform': 'active$transform-h'
148+
}
149+
};
150+
151+
// This tests that repeat results are the same, and that memoization
152+
// works correctly when reusing data from earlier merges.
153+
154+
// ONE
155+
const one = [
156+
styles.a,
157+
false,
158+
[
159+
styles.b,
160+
false,
161+
styles.c,
162+
[styles.d, false, styles.e, false, [styles.f, styles.g], [styles.h]]
163+
]
164+
];
165+
const oneValue = merge(one).className;
166+
const oneRepeat = merge(one).className;
167+
// Check the memoized result is correct
168+
expect(oneValue).toEqual(oneRepeat);
169+
expect(oneValue).toMatchInlineSnapshot(
170+
// eslint-disable-next-line
171+
`"borderColor-a borderStyle-a borderWidth-a boxSizing-a listStyle-a marginTop-a marginBottom-a paddingTop-a paddingEnd-a paddingBottom-a paddingStart-a whiteSpace-a wordWrap-a zIndex-a outline-c touchAction-d backgroundColor-f color-f cursor-f marginEnd-f marginStart-f textAlign-f textDecoration-f focus$color-f focus$textDecoration-f active$transition-f display-g width-g active$transform-h"`
172+
);
173+
174+
// TWO
175+
const two = [
176+
styles.d,
177+
false,
178+
[
179+
styles.c,
180+
false,
181+
styles.b,
182+
[styles.a, false, styles.e, false, [styles.f, styles.g], [styles.h]]
183+
]
184+
];
185+
const twoValue = merge(two).className;
186+
const twoRepeat = merge(two).className;
187+
// Check the memoized result is correct
188+
expect(twoValue).toEqual(twoRepeat);
189+
expect(twoValue).toMatchInlineSnapshot(
190+
// eslint-disable-next-line
191+
`"outline-c touchAction-b borderColor-a borderStyle-a borderWidth-a boxSizing-a listStyle-a marginTop-a marginBottom-a paddingTop-a paddingEnd-a paddingBottom-a paddingStart-a whiteSpace-a wordWrap-a zIndex-a backgroundColor-f color-f cursor-f marginEnd-f marginStart-f textAlign-f textDecoration-f focus$color-f focus$textDecoration-f active$transition-f display-g width-g active$transform-h"`
192+
);
193+
});
194+
195+
test('dedupes inline styles', () => {
196+
const { style } = merge([{ '--a': 'a' }, { '--a': 'aa' }]);
197+
expect(style).toEqual({ '--a': 'aa' });
198+
const { style: style2 } = merge([{ a: 'a' }, { a: null }]);
199+
expect(style2).toEqual({ a: null });
200+
});
201+
202+
test('preserves order of stringified inline style', () => {
203+
const { style } = merge([{ font: 'inherit', fontSize: 12 }]);
204+
const str = stringifyInlineStyle(style);
205+
// eslint-disable-next-line
206+
expect(str).toMatchInlineSnapshot(`"font:inherit;fontSize:12;"`);
207+
208+
const { style: style2 } = merge([{ font: 'inherit' }, { fontSize: 12 }]);
209+
const str2 = stringifyInlineStyle(style2);
210+
// eslint-disable-next-line
211+
expect(str2).toMatchInlineSnapshot(`"font:inherit;fontSize:12;"`);
212+
});
213+
214+
test('does not dedupe class names and inline styles', () => {
215+
const a = { $$css: true, a: 'a', ':focus$a': 'focus$a' };
216+
const b = { $$css: true, b: 'b' };
217+
const binline = { b: 'b', bb: null };
218+
// Both should produce: [ 'a hover$a b', { b: 'b' } ]
219+
expect(merge([a, b, binline])).toEqual(merge([a, binline, b]));
220+
});
221+
222+
test('supports generating debug strings', () => {
223+
const a = { $$css: 'path/to/a:1', a: 'aaa' };
224+
const b1 = { $$css: 'path/to/b:22', b: 'bbb' };
225+
const b2 = { $$css: 'path/to/b:33', b: 'bbb' };
226+
const b3 = { $$css: 'path/to/b:44', b: 'bbb' };
227+
const c = { $$css: 'path/to/c:3', b: 'ccc' };
228+
const { 'data-style-src': dataStyleSrc } = merge([a]);
229+
expect(dataStyleSrc).toBe('path/to/a:1');
230+
const { 'data-style-src': dataStyleSrc2 } = merge([a, [b1, c, b2], b3]);
231+
expect(dataStyleSrc2).toBe('path/to/a:1; path/to/b:22,33,44; path/to/c:3');
232+
});
233+
});

0 commit comments

Comments
 (0)