Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(react): refactored types for styled function (fixes #872) #887

Merged
merged 1 commit into from
Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -181,21 +181,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