Skip to content

Commit

Permalink
fix(react): refactored types for styled function (fixes #872) (#887)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anber authored Dec 20, 2021
1 parent ee656dd commit 7b8b129
Show file tree
Hide file tree
Showing 14 changed files with 138 additions and 89 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"react": ">=16",
"release-it": "^14.2.1",
"release-it-lerna-changelog": "^3.1.0",
"typescript": "^3.9.7"
"typescript": "^4.2.3"
},
"resolutions": {
"@typescript-eslint/experimental-utils": "^4.28.0",
Expand Down
18 changes: 11 additions & 7 deletions packages/babel/src/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,18 @@ export default function extract(
state.dependencies.push(...evaluation.dependencies);
lazyValues = evaluation.value.__linariaPreval || [];
debug('lazy-deps:values', evaluation.value.__linariaPreval);
} catch (e) {
} catch (e: unknown) {
error('lazy-deps:evaluate:error', code);
throw new Error(
'An unexpected runtime error occurred during dependencies evaluation: \n' +
e.stack +
'\n\nIt may happen when your code or third party module is invalid or uses identifiers not available in Node environment, eg. window. \n' +
'Note that line numbers in above stack trace will most likely not match, because Linaria needed to transform your code a bit.\n'
);
if (e instanceof Error) {
throw new Error(
'An unexpected runtime error occurred during dependencies evaluation: \n' +
e.stack +
'\n\nIt may happen when your code or third party module is invalid or uses identifiers not available in Node environment, eg. window. \n' +
'Note that line numbers in above stack trace will most likely not match, because Linaria needed to transform your code a bit.\n'
);
} else {
throw e;
}
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/babel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export * from './types';
export type { PluginOptions } from './utils/loadOptions';
export { default as isNode } from './utils/isNode';
export { default as getVisitorKeys } from './utils/getVisitorKeys';
export type { VisitorKeys } from './utils/getVisitorKeys';
export { default as peek } from './utils/peek';
export { default as CollectDependencies } from './visitors/CollectDependencies';
export { default as DetectStyledImportName } from './visitors/DetectStyledImportName';
Expand Down
20 changes: 1 addition & 19 deletions packages/babel/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Node, Expression, TaggedTemplateExpression } from '@babel/types';
import type { Expression, TaggedTemplateExpression } from '@babel/types';
import type { TransformOptions } from '@babel/core';
import type { NodePath } from '@babel/traverse';
import type { StyledMeta } from '@linaria/core';
Expand Down Expand Up @@ -184,21 +184,3 @@ export type Options = {

export type PreprocessorFn = (selector: string, cssText: string) => string;
export type Preprocessor = 'none' | 'stylis' | PreprocessorFn | void;

type AllNodes = { [T in Node['type']]: Extract<Node, { type: T }> };

declare module '@babel/types' {
type VisitorKeys = {
[T in keyof AllNodes]: Extract<
keyof AllNodes[T],
{
[Key in keyof AllNodes[T]]: AllNodes[T][Key] extends
| Node
| Node[]
| null
? Key
: never;
}[keyof AllNodes[T]]
>;
};
}
13 changes: 9 additions & 4 deletions packages/babel/src/utils/getVisitorKeys.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { types as t } from '@babel/core';
import type { Node, VisitorKeys } from '@babel/types';
import type { Node } from '@babel/types';

type Keys<T extends Node> = (VisitorKeys[T['type']] & keyof T)[];
export type VisitorKeys<T extends Node> = {
[K in keyof T]: Exclude<T[K], undefined> extends Node | Node[] | null
? K
: never;
}[keyof T] &
string;

export default function getVisitorKeys<TNode extends Node>(
node: TNode
): Keys<TNode> {
return t.VISITOR_KEYS[node.type] as Keys<TNode>;
): VisitorKeys<TNode>[] {
return t.VISITOR_KEYS[node.type] as VisitorKeys<TNode>[];
}
37 changes: 37 additions & 0 deletions packages/react/__dtslint__/styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,40 @@ styled.a`
// $ExpectType Validator<string> | undefined
NewWrapper.propTypes!.prop2;
})();

((/* Issue #844 */) => {
type GridProps = { container?: false } | { container: true; spacing: number };

const Grid: React.FC<GridProps & { className?: string }> = () => null;

// Type 'false' is not assignable to type 'true'
// $ExpectError
React.createElement(Grid, { container: false, spacing: 8 });

React.createElement(Grid, { container: true, spacing: 8 });

styled(Grid)``;
})();

((/* Issue #872 */) => {
interface BaseProps {
className?: string;
style?: React.CSSProperties;
}

interface ComponentProps extends BaseProps {
title: string;
}

const Flow = <TProps extends BaseProps>(Cmp: React.FC<TProps>) =>
styled(Cmp)`
display: flow;
`;

const Component: React.FC<ComponentProps> = (props) =>
React.createElement('div', props);

const Implementation = Flow(Component);

(() => React.createElement(Implementation, { title: 'Title' }))();
})();
79 changes: 44 additions & 35 deletions packages/react/src/styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import type { CSSProperties, StyledMeta } from '@linaria/core';

export type NoInfer<A extends any> = [A][A extends any ? 0 : never];

type Component<TProps> =
| ((props: TProps) => unknown)
| { new (props: TProps): unknown };

type Has<T, TObj> = [T] extends [TObj] ? T : T & TObj;

type Options = {
name: string;
class: string;
Expand All @@ -22,15 +28,12 @@ type Options = {
};
};

interface CustomOmit {
<T extends object, K extends [...(keyof T)[]]>(obj: T, keys: K): {
[K2 in Exclude<keyof T, K[number]>]: T[K2];
};
}

// Workaround for rest operator
export const restOp: CustomOmit = (obj, keys) => {
const res = {} as { [K in keyof typeof obj]: typeof obj[K] };
export const restOp = <T extends object, TKeys extends [...(keyof T)[]]>(
obj: T,
keys: TKeys
) => {
const res = {} as { [K in keyof T]: T[K] };
let key: keyof typeof obj;
for (key in obj) {
if (keys.indexOf(key) === -1) {
Expand Down Expand Up @@ -66,22 +69,28 @@ interface IProps {
[props: string]: unknown;
}

// Property-based interpolation is allowed only if `style` property exists
function styled<
TProps extends Has<TMustHave, { style?: React.CSSProperties }>,
TMustHave extends { style?: React.CSSProperties },
TConstructor extends Component<TProps>
>(
componentWithStyle: TConstructor & Component<TProps>
): ComponentStyledTagWithInterpolation<TProps, TConstructor>;
// If styled wraps custom component, that component should have className property
function styled<TConstructor extends React.ComponentType<any>>(
tag: TConstructor extends React.ComponentType<infer T>
? [T] extends [{ className?: string | undefined }]
? TConstructor
: never
: never
): ComponentStyledTag<TConstructor>;
function styled<T>(
tag: [T] extends [{ className?: string | undefined }]
? React.ComponentType<T>
: never
): ComponentStyledTag<T>;
function styled<
TProps extends Has<TMustHave, { className?: string }>,
TMustHave extends { className?: string },
TConstructor extends Component<TProps>
>(
componentWithoutStyle: TConstructor & Component<TProps>
): ComponentStyledTagWithoutInterpolation<TConstructor>;
function styled<TName extends keyof JSX.IntrinsicElements>(
tag: TName
): HtmlStyledTag<TName>;
function styled(
component: 'The target component should have a className prop'
): never;
function styled(tag: any): any {
return (options: Options) => {
if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -199,23 +208,23 @@ type HtmlStyledTag<TName extends keyof JSX.IntrinsicElements> = <
>
) => StyledComponent<JSX.IntrinsicElements[TName] & TAdditionalProps>;

type ComponentStyledTag<T> = <
OwnProps = {},
TrgProps = [T] extends [React.FunctionComponent<infer TProps>] ? TProps : T
>(
type ComponentStyledTagWithoutInterpolation<TOrigCmp> = (
strings: TemplateStringsArray,
// Expressions can contain functions only if wrapped component has style property
...exprs: TrgProps extends { style?: React.CSSProperties | undefined }
? Array<
| StaticPlaceholder
| ((props: NoInfer<OwnProps & TrgProps>) => string | number)
>
: StaticPlaceholder[]
...exprs: Array<
| StaticPlaceholder
| ((props: 'The target component should have a style prop') => never)
>
) => StyledMeta & TOrigCmp;

type ComponentStyledTagWithInterpolation<TTrgProps, TOrigCmp> = <OwnProps = {}>(
strings: TemplateStringsArray,
...exprs: Array<
| StaticPlaceholder
| ((props: NoInfer<OwnProps & TTrgProps>) => string | number)
>
) => keyof OwnProps extends never
? [T] extends [React.FunctionComponent<any>]
? StyledMeta & T
: StyledComponent<TrgProps>
: StyledComponent<OwnProps & TrgProps>;
? StyledMeta & TOrigCmp
: StyledComponent<OwnProps & TTrgProps>;

type StyledJSXIntrinsics = {
readonly [P in keyof JSX.IntrinsicElements]: HtmlStyledTag<P>;
Expand Down
5 changes: 3 additions & 2 deletions packages/shaker/src/GraphBuilderState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Node, VisitorKeys } from '@babel/types';
import type { Node } from '@babel/types';
import type { VisitorKeys } from '@linaria/babel-preset';
import ScopeManager from './scope';
import DepsGraph from './DepsGraph';
import { VisitorAction } from './types';
Expand Down Expand Up @@ -40,7 +41,7 @@ export default abstract class GraphBuilderState {
abstract visit<TNode extends Node, TParent extends Node>(
node: TNode,
parent: TParent | null,
parentKey: VisitorKeys[TParent['type']] | null,
parentKey: VisitorKeys<TParent> | null,
listIdx?: number | null
): VisitorAction;
}
5 changes: 3 additions & 2 deletions packages/shaker/src/Visitors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { types as t } from '@babel/core';
import type { Identifier, Node, VisitorKeys } from '@babel/types';
import type { Identifier, Node } from '@babel/types';
import { warn } from '@linaria/logger';
import { peek } from '@linaria/babel-preset';
import type { VisitorKeys } from '@linaria/babel-preset';
import GraphBuilderState from './GraphBuilderState';
import identifierHandlers from './identifierHandlers';
import type { Visitor, Visitors } from './types';
Expand All @@ -13,7 +14,7 @@ const visitors: Visitors = {
this: GraphBuilderState,
node: Identifier,
parent: TParent | null,
parentKey: VisitorKeys[TParent['type']] | null,
parentKey: VisitorKeys<TParent> | null,
listIdx: number | null = null
) {
if (!parent || !parentKey) {
Expand Down
17 changes: 12 additions & 5 deletions packages/shaker/src/graphBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { types as t } from '@babel/core';
import type { AssignmentExpression, Node, VisitorKeys } from '@babel/types';
import type { AssignmentExpression, Node } from '@babel/types';
import { isNode, getVisitorKeys } from '@linaria/babel-preset';
import type { VisitorKeys } from '@linaria/babel-preset';
import DepsGraph from './DepsGraph';
import GraphBuilderState from './GraphBuilderState';
import { getVisitors } from './Visitors';
import type { VisitorAction } from './types';
import ScopeManager from './scope';
import { Visitor } from './types';

const isVoid = (node: Node): boolean =>
t.isUnaryExpression(node) && node.operator === 'void';
Expand Down Expand Up @@ -117,7 +119,7 @@ class GraphBuilder extends GraphBuilderState {
visit<TNode extends Node, TParent extends Node>(
node: TNode,
parent: TParent | null,
parentKey: VisitorKeys[TParent['type']] | null,
parentKey: VisitorKeys<TParent> | null,
listIdx: number | null = null
): VisitorAction {
if (parent) {
Expand All @@ -143,7 +145,11 @@ class GraphBuilder extends GraphBuilderState {
// Batch export is a very particular case.
// Each property of the assigned object is independent named export.
// We also need to specify all dependencies and call `visit` for every value.
this.visit(node.left, node, 'left');
this.visit(
node.left,
node,
'left' as VisitorKeys<TNode & AssignmentExpression>
);
node.right.properties.forEach((prop) => {
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
this.visit(prop.value, prop, 'value');
Expand Down Expand Up @@ -186,9 +192,10 @@ class GraphBuilder extends GraphBuilderState {
const visitors = getVisitors(node);
let action: VisitorAction;
if (visitors.length > 0) {
let visitor;
let visitor: Visitor<TNode> | undefined;
while (!action && (visitor = visitors.shift())) {
action = visitor.call(this, node, parent, parentKey, listIdx);
const method: Visitor<TNode> = visitor.bind(this);
action = method(node, parent, parentKey, listIdx);
}
} else {
this.baseVisit(node);
Expand Down
5 changes: 3 additions & 2 deletions packages/shaker/src/identifierHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { types as t } from '@babel/core';
import type { Aliases, Identifier, Node, VisitorKeys } from '@babel/types';
import type { Aliases, Identifier, Node } from '@babel/types';
import { peek } from '@linaria/babel-preset';
import type { VisitorKeys } from '@linaria/babel-preset';
import GraphBuilderState from './GraphBuilderState';
import type { IdentifierHandlerType, NodeType } from './types';
import { identifierHandlers as core } from './langs/core';
Expand All @@ -10,7 +11,7 @@ type HandlerFn = <TParent extends Node = Node>(
builder: GraphBuilderState,
node: Identifier,
parent: TParent,
parentKey: VisitorKeys[TParent['type']],
parentKey: VisitorKeys<TParent>,
listIdx: number | null
) => void;

Expand Down
5 changes: 3 additions & 2 deletions packages/shaker/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Aliases, Node, VisitorKeys } from '@babel/types';
import type { Aliases, Node } from '@babel/types';
import type { VisitorKeys } from '@linaria/babel-preset';

export type NodeOfType<T> = Extract<Node, { type: T }>;

Expand All @@ -9,7 +10,7 @@ export type VisitorAction = 'ignore' | void;
export type Visitor<TNode extends Node> = <TParent extends Node>(
node: TNode,
parent: TParent | null,
parentKey: VisitorKeys[TParent['type']] | null,
parentKey: VisitorKeys<TParent> | null,
listIdx: number | null
) => VisitorAction;

Expand Down
4 changes: 2 additions & 2 deletions packages/stylelint/src/preprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ function preprocessor() {
cache[filename] = undefined;
errors[filename] = undefined;
offsets[filename] = [];
} catch (e) {
} catch (e: unknown) {
cache[filename] = undefined;
offsets[filename] = undefined;
errors[filename] = e;
errors[filename] = e as Error;

// Ignore parse errors here
// We handle it separately
Expand Down
Loading

0 comments on commit 7b8b129

Please sign in to comment.