Skip to content

Commit 4b2e5ee

Browse files
authoredMar 10, 2024··
Add media query typing to XCSS prop / createStrictAPI (#1634)
* feat: support top-level media queries * chore: changeset * feat: add media query support * chore: test loose api * fix: type union exhaustion * fix: remove required pseudo inside media query * chore: fix tests * fix: flatten only objects that are exact matches * chore: remove required props from media queries * chore: add test * chore: add tests * chore: remove unused * chore: correct comment * fix: block all loose media queries from the strict api * chore: fix test * chore: move to object
1 parent 20528e9 commit 4b2e5ee

File tree

12 files changed

+396
-27
lines changed

12 files changed

+396
-27
lines changed
 

‎.changeset/fair-dolphins-tie.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@compiled/babel-plugin': patch
3+
---
4+
5+
The CSS map transform now allows top level at rules to be defined.

‎.changeset/modern-eels-tie.md

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@compiled/react': patch
3+
---
4+
5+
- The CSS map API now allows defining top level media queries. Previously you had to define them inside a `@media` object, this restriction has now been removed bringing it inline with the CSS function API.
6+
- The XCSS prop and strict API types now allow defining and using media queries.
7+
8+
**XCSS prop**
9+
10+
The XCSS prop now takes top level media queries. Nested media queries is not allowed.
11+
12+
```jsx
13+
import { cssMap, css } from '@compiled/react';
14+
15+
const styles = cssMap({
16+
valid: { '@media (min-width: 30rem)': { color: 'green' } },
17+
invalid: { '@media': { '(min-width: 30rem)': { color: 'red' } } },
18+
});
19+
20+
<Component xcss={styles.valid} />;
21+
```
22+
23+
**createStrictAPI**
24+
25+
Now takes an optional second generic to define what media queries are supported:
26+
27+
```diff
28+
createStrictAPI<
29+
{ color: 'var(--text)' }
30+
+ { media: '(min-width: 30rem)' | '(min-width: 48rem)' }
31+
>();
32+
```
33+
34+
Which is then flushed to all output APIs.

‎packages/babel-plugin/src/css-map/process-selectors.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
errorIfNotValidObjectProperty,
99
getKeyValue,
1010
hasExtendedSelectorsKey as propertyHasExtendedSelectorsKey,
11-
isAtRule,
11+
isAtRuleObject,
1212
objectKeyIsLiteralValue,
1313
isPlainSelector,
1414
} from '../utils/css-map';
@@ -150,7 +150,7 @@ export const mergeExtendedSelectorsIntoProperties = (
150150
// variable, so we can skip it now
151151
if (propertyHasExtendedSelectorsKey(property)) continue;
152152

153-
if (isAtRule(propertyKey)) {
153+
if (isAtRuleObject(propertyKey)) {
154154
const atRuleType = getKeyValue(propertyKey);
155155
const atRules = collapseAtRule(property, atRuleType, meta);
156156

@@ -162,6 +162,7 @@ export const mergeExtendedSelectorsIntoProperties = (
162162
meta.parentPath
163163
);
164164
}
165+
165166
mergedProperties.push(atRuleValue);
166167
addedSelectors.add(atRuleName);
167168
}

‎packages/babel-plugin/src/utils/css-map.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
import * as t from '@babel/types';
2+
import type { AtRules } from 'csstype';
23

34
import type { Metadata } from '../types';
45
import { buildCodeFrameError } from '../utils/ast';
56

67
export const EXTENDED_SELECTORS_KEY = 'selectors';
78

9+
const atRules: Record<AtRules, boolean> = {
10+
'@charset': true,
11+
'@counter-style': true,
12+
'@document': true,
13+
'@font-face': true,
14+
'@font-feature-values': true,
15+
'@font-palette-values': true,
16+
'@import': true,
17+
'@keyframes': true,
18+
'@layer': true,
19+
'@media': true,
20+
'@namespace': true,
21+
'@page': true,
22+
'@property': true,
23+
'@scroll-timeline': true,
24+
'@supports': true,
25+
'@viewport': true,
26+
};
27+
828
type ObjectKeyWithLiteralValue = t.Identifier | t.StringLiteral;
929

1030
export const objectKeyIsLiteralValue = (
@@ -17,8 +37,12 @@ export const getKeyValue = (key: ObjectKeyWithLiteralValue): string => {
1737
throw new Error(`Expected an identifier or a string literal, got type ${(key as any).type}`);
1838
};
1939

20-
export const isAtRule = (key: ObjectKeyWithLiteralValue): key is ObjectKeyWithLiteralValue =>
21-
getKeyValue(key).startsWith('@');
40+
export const isAtRuleObject = (
41+
key: ObjectKeyWithLiteralValue
42+
): key is ObjectKeyWithLiteralValue => {
43+
const keyValue = getKeyValue(key);
44+
return keyValue in atRules;
45+
};
2246

2347
export const isPlainSelector = (selector: string): boolean => selector.startsWith(':');
2448

‎packages/react/src/create-strict-api/__tests__/__fixtures__/strict-api.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ interface CSSPropertiesSchema {
1010
bkgrnd: 'red' | 'green';
1111
}
1212

13-
const { css, XCSSProp, cssMap, cx } = createStrictAPI<CSSPropertiesSchema>();
13+
type MediaQuery =
14+
| '(min-width: 30rem)'
15+
| '(min-width: 48rem)'
16+
| '(min-width: 64rem)'
17+
| '(min-width: 90rem)'
18+
| '(min-width: 110rem)'
19+
| '(prefers-color-scheme: dark)'
20+
| '(prefers-color-scheme: light)';
21+
22+
const { css, XCSSProp, cssMap, cx } = createStrictAPI<CSSPropertiesSchema, { media: MediaQuery }>();
1423

1524
export { css, XCSSProp, cssMap, cx };

‎packages/react/src/create-strict-api/index.ts

+29-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import { type CompiledStyles, cx, type Internal$XCSSProp } from '../xcss-prop';
44

55
import type { AllowedStyles, ApplySchema, ApplySchemaMap, CompiledSchemaShape } from './types';
66

7-
export interface CompiledAPI<TSchema extends CompiledSchemaShape> {
7+
export interface StrictOptions {
8+
media: string;
9+
}
10+
11+
export interface CompiledAPI<
12+
TSchema extends CompiledSchemaShape,
13+
TAllowedMediaQueries extends string
14+
> {
815
/**
916
* ## CSS
1017
*
@@ -23,7 +30,7 @@ export interface CompiledAPI<TSchema extends CompiledSchemaShape> {
2330
* ```
2431
*/
2532
css<TStyles extends ApplySchema<TStyles, TSchema>>(
26-
styles: AllowedStyles & TStyles
33+
styles: AllowedStyles<TAllowedMediaQueries> & TStyles
2734
// NOTE: This return type is deliberately not using ReadOnly<CompiledStyles<TStyles>>
2835
// So it type errors when used with XCSS prop. When we update the compiler to work with
2936
// it we can update the return type so it stops being a type violation.
@@ -45,10 +52,10 @@ export interface CompiledAPI<TSchema extends CompiledSchemaShape> {
4552
* ```
4653
*/
4754
cssMap<
48-
TObject extends Record<string, AllowedStyles>,
55+
TObject extends Record<string, AllowedStyles<TAllowedMediaQueries>>,
4956
TStylesMap extends ApplySchemaMap<TObject, TSchema>
5057
>(
51-
styles: Record<string, AllowedStyles> & TStylesMap
58+
styles: Record<string, AllowedStyles<TAllowedMediaQueries>> & TStylesMap
5259
): {
5360
readonly [P in keyof TStylesMap]: CompiledStyles<TStylesMap[P]>;
5461
};
@@ -137,7 +144,14 @@ export interface CompiledAPI<TSchema extends CompiledSchemaShape> {
137144
requiredProperties: TAllowedProperties;
138145
requiredPseudos: TAllowedPseudos;
139146
} = never
140-
>(): Internal$XCSSProp<TAllowedProperties, TAllowedPseudos, TSchema, TRequiredProperties>;
147+
>(): Internal$XCSSProp<
148+
TAllowedProperties,
149+
TAllowedPseudos,
150+
TAllowedMediaQueries,
151+
TSchema,
152+
TRequiredProperties,
153+
'strict'
154+
>;
141155
}
142156

143157
/**
@@ -154,17 +168,19 @@ export interface CompiledAPI<TSchema extends CompiledSchemaShape> {
154168
*
155169
* To set up:
156170
*
157-
* 1. Declare the API in a module (either local or in a package):
171+
* 1. Declare the API in a module (either local or in a package), optionally declaring accepted media queries.
158172
*
159173
* @example
160174
* ```tsx
161175
* // ./foo.ts
162-
* const { css } = createStrictAPI<{
176+
* interface Schema {
163177
* color: 'var(--ds-text)',
164178
* '&:hover': { color: 'var(--ds-text-hover)' }
165-
* }>();
179+
* }
180+
*
181+
* const { css, cssMap, XCSSProp, cx } = createStrictAPI<Schema, { media: '(min-width: 30rem)' }>();
166182
*
167-
* export { css };
183+
* export { css, cssMap, XCSSProp, cx };
168184
* ```
169185
*
170186
* 2. Configure Compiled to pick up this module:
@@ -188,7 +204,10 @@ export interface CompiledAPI<TSchema extends CompiledSchemaShape> {
188204
* <div css={styles} />
189205
* ```
190206
*/
191-
export function createStrictAPI<TSchema extends CompiledSchemaShape>(): CompiledAPI<TSchema> {
207+
export function createStrictAPI<
208+
TSchema extends CompiledSchemaShape,
209+
TCreateStrictAPIOptions extends StrictOptions = never
210+
>(): CompiledAPI<TSchema, TCreateStrictAPIOptions['media']> {
192211
return {
193212
css() {
194213
throw createStrictSetupError();

‎packages/react/src/create-strict-api/types.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ export type CompiledSchemaShape = StrictCSSProperties & {
1616

1717
export type PseudosDeclarations = { [Q in CSSPseudos]?: StrictCSSProperties };
1818

19-
export type AllowedStyles = StrictCSSProperties & PseudosDeclarations;
19+
export type MediaQueries<TMediaQuery extends string> = {
20+
[Q in `@media ${TMediaQuery}`]?: StrictCSSProperties & PseudosDeclarations;
21+
};
22+
23+
export type AllowedStyles<TMediaQuery extends string> = StrictCSSProperties &
24+
PseudosDeclarations &
25+
MediaQueries<TMediaQuery>;
2026

2127
export type ApplySchemaValue<
2228
TSchema,

‎packages/react/src/css-map/index.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,21 @@ type ExtendedSelectors = {
8383
selectors?: ExtendedSelector;
8484
};
8585

86+
type LooseMediaQueries = Record<`@media ${string}`, CSSProperties & AllPseudos>;
87+
88+
/**
89+
* We remap media query keys to `"@media"` so it's blocked inside the strict APIs.
90+
* This is done as it's currently impossible to ensure type safety end-to-end — when
91+
* passing in unknown media queries from the loose API into the strict API you end up
92+
* being also able to pass any styles you want, which makes the whole point of the strict
93+
* API meaningless.
94+
*
95+
* Sorry folks!
96+
*/
97+
type RemapMedia<TStyles> = {
98+
[Q in keyof TStyles as Q extends `@media ${string}` ? '@media [loose]' : Q]: TStyles[Q];
99+
};
100+
86101
/**
87102
* ## CSS Map
88103
*
@@ -100,11 +115,14 @@ type ExtendedSelectors = {
100115
* ```
101116
*/
102117
export default function cssMap<
103-
TStyles extends Record<string, CSSProperties & WhitelistedSelector & ExtendedSelectors>
118+
TStyles extends Record<
119+
string,
120+
CSSProperties & WhitelistedSelector & ExtendedSelectors & LooseMediaQueries
121+
>
104122
>(
105123
_styles: TStyles
106124
): {
107-
readonly [P in keyof TStyles]: CompiledStyles<TStyles[P]>;
125+
readonly [P in keyof TStyles]: CompiledStyles<RemapMedia<TStyles[P]>>;
108126
} {
109127
throw createSetupError();
110128
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/** @jsxImportSource @compiled/react */
2+
// eslint-disable-next-line import/no-extraneous-dependencies
3+
import { cssMap as cssMapLoose } from '@compiled/react';
4+
import { render } from '@testing-library/react';
5+
6+
import { cssMap } from '../../create-strict-api/__tests__/__fixtures__/strict-api';
7+
import type { XCSSProp } from '../../create-strict-api/__tests__/__fixtures__/strict-api';
8+
import type { XCSSAllProperties, XCSSAllPseudos } from '../index';
9+
10+
function CSSPropComponent({
11+
testId,
12+
xcss,
13+
}: {
14+
testId?: string;
15+
xcss: ReturnType<typeof XCSSProp<XCSSAllProperties, XCSSAllPseudos>>;
16+
}) {
17+
return (
18+
<div data-testid={testId} className={xcss}>
19+
foo
20+
</div>
21+
);
22+
}
23+
24+
const styles = cssMap({
25+
invalidMediaObject: {
26+
// @ts-expect-error — @media at rule object is not allowed in strict cssMap
27+
'@media': {
28+
screen: { color: 'red' },
29+
},
30+
},
31+
invalidMediaQuery: {
32+
// @ts-expect-error — this specific @media is not in our allowed types
33+
'@media (min-width: 100px)': {
34+
color: 'red',
35+
},
36+
},
37+
valid: {
38+
'@media (min-width: 110rem)': {
39+
// @ts-expect-error — color should be a value from the strict schema
40+
color: 'red',
41+
},
42+
},
43+
});
44+
45+
const looseStyles = cssMapLoose({
46+
invalidMediaObject: {
47+
'@media': {
48+
screen: { color: 'var(--ds-text)' },
49+
},
50+
},
51+
invalidMediaQuery: {
52+
'@media (min-width: 100px)': {
53+
color: 'red',
54+
},
55+
},
56+
validMediaQueryInvalidProperty: {
57+
'@media (min-width: 110rem)': {
58+
color: 'red',
59+
},
60+
},
61+
valid: {
62+
'@media (min-width: 110rem)': {
63+
color: 'var(--ds-text)',
64+
},
65+
},
66+
});
67+
68+
describe('xcss prop', () => {
69+
it('should block all usage of loose media queries in strict api as it is unsafe', () => {
70+
const { getByText } = render(
71+
<CSSPropComponent
72+
// @ts-expect-error — Block all media queries in strict xcss prop
73+
xcss={looseStyles.valid}
74+
/>
75+
);
76+
77+
expect(getByText('foo')).toHaveCompiledCss('color', 'var(--ds-text)', {
78+
media: '(min-width: 110rem)',
79+
});
80+
});
81+
82+
it('should type error invalid media queries from loose api', () => {
83+
const { getByTestId } = render(
84+
<>
85+
<CSSPropComponent
86+
// @ts-expect-error — Types of property '"@media"' are incompatible.
87+
xcss={looseStyles.invalidMediaObject}
88+
/>
89+
<CSSPropComponent testId="foobar" xcss={styles.invalidMediaObject} />
90+
</>
91+
);
92+
93+
expect(getByTestId('foobar')).toHaveCompiledCss('color', 'red', { media: 'screen' });
94+
});
95+
96+
it('should allow valid media queries in inline xcss prop', () => {
97+
const { getByText } = render(
98+
<CSSPropComponent
99+
xcss={{
100+
'@media (min-width: 110rem)': {
101+
color: 'var(--ds-text)',
102+
},
103+
}}
104+
/>
105+
);
106+
107+
expect(getByText('foo')).toHaveCompiledCss('color', 'var(--ds-text)', {
108+
media: '(min-width: 110rem)',
109+
});
110+
});
111+
112+
it('should allow valid media queries in inline xcss prop', () => {
113+
const { getByText } = render(
114+
<CSSPropComponent
115+
xcss={{
116+
'@media (min-width: 110rem)': {
117+
// @ts-expect-error — color should be a value from the strict schema
118+
color: 'red',
119+
},
120+
}}
121+
/>
122+
);
123+
124+
expect(getByText('foo')).toHaveCompiledCss('color', 'red', { media: '(min-width: 110rem)' });
125+
});
126+
127+
it('should allow valid psuedo through inline xcss prop', () => {
128+
const { getByText } = render(
129+
<CSSPropComponent
130+
xcss={{ '@media (min-width: 30rem)': { '&:hover': { color: 'var(--ds-text-hover)' } } }}
131+
/>
132+
);
133+
134+
expect(getByText('foo')).toHaveCompiledCss('color', 'var(--ds-text-hover)', {
135+
media: '(min-width: 30rem)',
136+
target: ':hover',
137+
});
138+
});
139+
140+
it('should type error for unexpected media query', () => {
141+
const { getByTestId } = render(
142+
<>
143+
<CSSPropComponent
144+
// NOTE: This doesn't currently error as the output isn't the generic value
145+
// when the cssMap call has type supressions. While not ideal this is acceptable
146+
// for now. Hopefully we can fix this in the future.
147+
xcss={styles.invalidMediaQuery}
148+
/>
149+
<CSSPropComponent
150+
// @ts-expect-error — Passing loose media queries to XCSS prop is not supported.
151+
xcss={looseStyles.invalidMediaQuery}
152+
/>
153+
<CSSPropComponent
154+
// @ts-expect-error — Passing loose media queries to XCSS prop is not supported.
155+
xcss={looseStyles.validMediaQueryInvalidProperty}
156+
/>
157+
<CSSPropComponent
158+
testId="foobar"
159+
xcss={{
160+
// @ts-expect-error — Types of property '"@media"' are incompatible.
161+
'@media (min-width: 100px)': {
162+
color: 'var(--ds-text)',
163+
},
164+
}}
165+
/>
166+
</>
167+
);
168+
169+
expect(getByTestId('foobar')).toHaveCompiledCss('color', 'var(--ds-text)', {
170+
media: '(min-width: 100px)',
171+
});
172+
});
173+
174+
it('should type check top level media query styles from cssMap', () => {
175+
const { getByText } = render(<CSSPropComponent xcss={styles.valid} />);
176+
177+
expect(getByText('foo')).toHaveCompiledCss('color', 'red', { media: '(min-width: 110rem)' });
178+
});
179+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/** @jsxImportSource @compiled/react */
2+
// eslint-disable-next-line import/no-extraneous-dependencies
3+
import { cssMap } from '@compiled/react';
4+
import { render } from '@testing-library/react';
5+
6+
import type { XCSSProp, XCSSAllProperties, XCSSAllPseudos } from '../index';
7+
8+
function CSSPropComponent({ xcss }: { xcss: XCSSProp<XCSSAllProperties, XCSSAllPseudos> }) {
9+
return <div className={xcss}>foo</div>;
10+
}
11+
12+
const styles = cssMap({
13+
invalid: {
14+
'@media': { screen: { color: 'red' } },
15+
},
16+
valid: { '@media screen': { color: 'red' } },
17+
});
18+
19+
describe('xcss prop', () => {
20+
it('should allow valid media queries in inline xcss prop', () => {
21+
const { getByText } = render(<CSSPropComponent xcss={{ '@media screen': { color: 'red' } }} />);
22+
23+
expect(getByText('foo')).toHaveCompiledCss('color', 'red', { media: 'screen' });
24+
});
25+
26+
it('should allow valid psuedo through inline xcss prop', () => {
27+
const { getByText } = render(
28+
<CSSPropComponent xcss={{ '@media screen': { '&:hover': { color: 'green' } } }} />
29+
);
30+
31+
expect(getByText('foo')).toHaveCompiledCss('color', 'green', {
32+
media: 'screen',
33+
target: ':hover',
34+
});
35+
});
36+
37+
it('should type error for disallowed nested media query object from cssMap', () => {
38+
const { getByText } = render(
39+
<CSSPropComponent
40+
// @ts-expect-error — @media object is not allowed in xcss prop
41+
xcss={styles.invalid}
42+
/>
43+
);
44+
45+
expect(getByText('foo')).toHaveCompiledCss('color', 'red', { media: 'screen' });
46+
});
47+
48+
it('should type check top level media query styles from cssMap', () => {
49+
const { getByText } = render(<CSSPropComponent xcss={styles.valid} />);
50+
51+
expect(getByText('foo')).toHaveCompiledCss('color', 'red', { media: 'screen' });
52+
});
53+
});

‎packages/react/src/xcss-prop/__tests__/xcss-prop.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ describe('xcss prop', () => {
177177
).toBeObject();
178178
});
179179

180-
it('should type error when passing in at rules to xcss prop', () => {
180+
it('should type error when passing in @media property to xcss prop', () => {
181181
function CSSPropComponent({ xcss }: { xcss: XCSSProp<'color', '&:hover'> }) {
182182
return <div className={xcss}>foo</div>;
183183
}

‎packages/react/src/xcss-prop/index.ts

+29-8
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,29 @@ type XCSSPseudo<
3030
: never;
3131
};
3232

33+
type XCSSMediaQuery<
34+
TAllowedProperties extends keyof StrictCSSProperties,
35+
TAllowedPseudos extends CSSPseudos,
36+
TAllowedMediaQueries extends string,
37+
TSchema
38+
> = {
39+
[Q in `@media ${TAllowedMediaQueries}`]?:
40+
| XCSSValue<TAllowedProperties, TSchema, ''>
41+
| XCSSPseudo<TAllowedProperties, TAllowedPseudos, never, TSchema>;
42+
};
43+
3344
/**
3445
* These APIs we don't want to allow to be passed through the `xcss` prop but we also
3546
* must declare them so the (lack-of a) excess property check doesn't bite us and allow
3647
* unexpected values through.
3748
*/
38-
type BlockedRules = {
49+
type BlockedRules<TMode extends 'loose' | 'strict'> = {
50+
// To ensure styles that aren't allowed through XCSS prop strict APIs we block any
51+
// loose media queries from being passed through as we can't ensure they're correct.
52+
'@media [loose]'?: TMode extends 'loose' ? any : never;
3953
selectors?: never;
4054
} & {
41-
/**
42-
* We currently block all at rules from xcss prop.
43-
* This needs us to decide on what the final API is across Compiled to be able to set.
44-
*/
55+
// We also block all type level at rule "objects" that are present on cssMap.
4556
[Q in CSS.AtRules]?: never;
4657
};
4758

@@ -143,16 +154,25 @@ export type XCSSProp<
143154
requiredProperties: TAllowedProperties;
144155
requiredPseudos: TAllowedPseudos;
145156
} = never
146-
> = Internal$XCSSProp<TAllowedProperties, TAllowedPseudos, object, TRequiredProperties>;
157+
> = Internal$XCSSProp<
158+
TAllowedProperties,
159+
TAllowedPseudos,
160+
string,
161+
object,
162+
TRequiredProperties,
163+
'loose'
164+
>;
147165

148166
export type Internal$XCSSProp<
149167
TAllowedProperties extends keyof StrictCSSProperties,
150168
TAllowedPseudos extends CSSPseudos,
169+
TAllowedMediaQueries extends string,
151170
TSchema,
152171
TRequiredProperties extends {
153172
requiredProperties: TAllowedProperties;
154173
requiredPseudos: TAllowedPseudos;
155-
}
174+
},
175+
TMode extends 'loose' | 'strict'
156176
> =
157177
| (MarkAsRequired<
158178
XCSSValue<TAllowedProperties, TSchema, ''>,
@@ -162,7 +182,8 @@ export type Internal$XCSSProp<
162182
XCSSPseudo<TAllowedProperties, TAllowedPseudos, TRequiredProperties, TSchema>,
163183
TRequiredProperties['requiredPseudos']
164184
> &
165-
BlockedRules)
185+
XCSSMediaQuery<TAllowedProperties, TAllowedPseudos, TAllowedMediaQueries, TSchema> &
186+
BlockedRules<TMode>)
166187
| false
167188
| null
168189
| undefined;

0 commit comments

Comments
 (0)
Please sign in to comment.