Skip to content

Commit

Permalink
fix(react): filter props only for known react tags (fixes #968) (#1101)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anber committed Oct 25, 2022
1 parent c26d466 commit 63f56d4
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 129 deletions.
9 changes: 9 additions & 0 deletions .changeset/hip-kids-swim.md
@@ -0,0 +1,9 @@
---
'@linaria/babel-preset': minor
'@linaria/griffel': minor
'@linaria/react': minor
'@linaria/tags': minor
'@linaria/testkit': minor
---

Do not filter properties if an unknown component is passed to `styled`. Fixes support of custom elements #968
5 changes: 3 additions & 2 deletions packages/babel/src/plugins/preeval.ts
Expand Up @@ -6,6 +6,7 @@ import type { BabelFile, NodePath, PluginObj } from '@babel/core';
import type { Identifier } from '@babel/types';

import { createCustomDebug } from '@linaria/logger';
import type { ExpressionValue } from '@linaria/tags';
import type { StrictOptions } from '@linaria/utils';
import {
JSXElementsRemover,
Expand Down Expand Up @@ -181,8 +182,8 @@ export default function preeval(
dependencies: [],
};

const expressions: Identifier[] = this.processors.flatMap((processor) =>
processor.dependencies.map((dependency) => dependency.ex)
const expressions: ExpressionValue['ex'][] = this.processors.flatMap(
(processor) => processor.dependencies.map((dependency) => dependency.ex)
);

const linariaPreval = file.path.scope.getData('__linariaPreval');
Expand Down
18 changes: 15 additions & 3 deletions packages/babel/src/utils/collectTemplateDependencies.ts
Expand Up @@ -11,21 +11,22 @@ import type {
Expression,
ExpressionStatement,
Identifier,
JSXIdentifier,
ObjectExpression,
ObjectProperty,
Program,
SourceLocation,
Statement,
TaggedTemplateExpression,
TemplateElement,
TSType,
VariableDeclaration,
VariableDeclarator,
SourceLocation,
JSXIdentifier,
} from '@babel/types';
import { cloneNode } from '@babel/types';

import { debug } from '@linaria/logger';
import type { ConstValue } from '@linaria/tags';
import { hasMeta } from '@linaria/tags';
import {
findIdentifiers,
Expand Down Expand Up @@ -219,6 +220,17 @@ export function extractExpression(
evaluate = false,
addToExport = true
): Omit<ExpressionValue, 'buildCodeFrameError' | 'source'> {
if (
ex.isLiteral() &&
('value' in ex.node || ex.node.type === 'NullLiteral')
) {
return {
ex: ex.node,
kind: ValueType.CONST,
value: ex.node.type === 'NullLiteral' ? null : ex.node.value,
} as Omit<ConstValue, 'buildCodeFrameError' | 'source'>;
}

const { loc } = ex.node;

const rootScope = ex.scope.getProgramParent();
Expand Down Expand Up @@ -323,7 +335,7 @@ export default function collectTemplateDependencies(
...extracted,
source,
buildCodeFrameError,
};
} as ExpressionValue;
}
);

Expand Down
13 changes: 11 additions & 2 deletions packages/babel/src/utils/getTagProcessor.ts
Expand Up @@ -13,7 +13,12 @@ import type {
import findUp from 'find-up';

import { BaseProcessor } from '@linaria/tags';
import type { Param, Params, IFileContext } from '@linaria/tags';
import type {
Param,
Params,
IFileContext,
ExpressionValue,
} from '@linaria/tags';
import type { IImport, StrictOptions } from '@linaria/utils';
import {
collectExportsAndImports,
Expand Down Expand Up @@ -266,7 +271,11 @@ function getBuilderForIdentifier(
}
const source = getSource(arg);
const extracted = extractExpression(arg, options.evaluate);
return { ...extracted, source, buildCodeFrameError: buildError };
return {
...extracted,
source,
buildCodeFrameError: buildError,
} as ExpressionValue;
})
.filter(isNotNull);

Expand Down
11 changes: 9 additions & 2 deletions packages/griffel/src/processors/makeStyles.ts
Expand Up @@ -15,7 +15,7 @@ export default class MakeStylesProcessor extends BaseProcessor {

#cssRulesByBucket: CSSRulesByBucket | undefined;

readonly #slotsExpName: string;
readonly #slotsExpName: string | number | boolean | null;

public constructor(params: Params, ...args: TailProcessorParams) {
validateParams(
Expand All @@ -27,7 +27,14 @@ export default class MakeStylesProcessor extends BaseProcessor {

super([tag], ...args);

this.#slotsExpName = callParam[1].ex.name;
const { ex } = callParam[1];
if (ex.type === 'Identifier') {
this.#slotsExpName = ex.name;
} else if (ex.type === 'NullLiteral') {
this.#slotsExpName = null;
} else {
this.#slotsExpName = ex.value;
}
}

public override get asSelector(): string {
Expand Down
9 changes: 9 additions & 0 deletions packages/react/__tests__/__snapshots__/styled.test.tsx.snap
Expand Up @@ -31,6 +31,15 @@ exports[`does not filter attributes for kebab cased custom elements 1`] = `
</my-element>
`;

exports[`does not filter attributes for unknown tag 1`] = `
<definitelyunknowntag
className="abcdefg"
hoverClass="voila"
>
This is a test
</definitelyunknowntag>
`;

exports[`does not filter attributes for upper camel cased custom elements 1`] = `
<View
className="abcdefg"
Expand Down
12 changes: 12 additions & 0 deletions packages/react/__tests__/styled.test.tsx
Expand Up @@ -214,6 +214,18 @@ it('does not filter attributes for upper camel cased custom elements', () => {
expect(tree.toJSON()).toMatchSnapshot();
});

it('does not filter attributes for unknown tag', () => {
const Test = styled('definitelyunknowntag')({
name: 'TestComponent',
class: 'abcdefg',
propsAsIs: true,
});

const tree = renderer.create(<Test hoverClass="voila">This is a test</Test>);

expect(tree.toJSON()).toMatchSnapshot();
});

it('does not filter attributes for components', () => {
const Custom: React.FC<{ unknownAttribute: string }> = (props) => (
<div>{props.unknownAttribute}</div>
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Expand Up @@ -8,6 +8,7 @@
"@linaria/core": "workspace:^",
"@linaria/tags": "workspace:^",
"@linaria/utils": "workspace:^",
"react-html-attributes": "^1.4.6",
"ts-invariant": "^0.10.3"
},
"devDependencies": {
Expand Down
14 changes: 13 additions & 1 deletion packages/react/src/processors/styled.ts
Expand Up @@ -5,6 +5,7 @@ import type {
SourceLocation,
StringLiteral,
} from '@babel/types';
import html from 'react-html-attributes';

import type {
Params,
Expand All @@ -26,10 +27,13 @@ import { slugify } from '@linaria/utils';

const isNotNull = <T>(x: T | null): x is T => x !== null;

const allTagsSet = new Set([...html.elements.html, html.elements.svg]);

export interface IProps {
atomic?: boolean;
class?: string;
name: string;
propsAsIs: boolean;
vars?: Record<string, Expression[]>;
}

Expand Down Expand Up @@ -71,6 +75,8 @@ export default class StyledProcessor extends TaggedTemplateProcessor {
const value = tagOp[1];
if (value.kind === ValueType.FUNCTION) {
component = 'FunctionalComponent';
} else if (value.kind === ValueType.CONST) {
component = typeof value.value === 'string' ? value.value : undefined;
} else {
component = {
node: value.ex,
Expand Down Expand Up @@ -240,6 +246,8 @@ export default class StyledProcessor extends TaggedTemplateProcessor {
const propsObj: IProps = {
name: this.displayName,
class: this.className,
propsAsIs:
typeof this.component !== 'string' || !allTagsSet.has(this.component),
};

// If we found any interpolations, also pass them, so they can be applied
Expand All @@ -264,12 +272,16 @@ export default class StyledProcessor extends TaggedTemplateProcessor {

const propExpressions = Object.entries(props)
.map(([key, value]: [key: string, value: IProps[keyof IProps]]) => {
if (!value) {
if (value === undefined) {
return null;
}

const keyNode = t.identifier(key);

if (value === null) {
return t.objectProperty(keyNode, t.nullLiteral());
}

if (typeof value === 'string') {
return t.objectProperty(keyNode, t.stringLiteral(value));
}
Expand Down
16 changes: 16 additions & 0 deletions packages/react/src/react-html-attributes.d.ts
@@ -0,0 +1,16 @@
declare module 'react-html-attributes' {
interface IElements {
html: string[];
svg: string[];
}

interface IAttributes {
[tag: string]: string[];
}

const result: IAttributes & {
elements: IElements;
};

export = result;
}
26 changes: 15 additions & 11 deletions packages/react/src/styled.ts
Expand Up @@ -21,9 +21,10 @@ type Component<TProps> =
type Has<T, TObj> = [T] extends [TObj] ? T : T & TObj;

type Options = {
name: string;
class: string;
atomic?: boolean;
class: string;
name: string;
propsAsIs: boolean;
vars?: {
[key: string]: [
string | number | ((props: unknown) => string | number),
Expand Down Expand Up @@ -53,18 +54,13 @@ export const omit = <T extends Record<string, unknown>, TKeys extends keyof T>(
};

function filterProps<T extends Record<string, unknown>, TKeys extends keyof T>(
component: string | unknown,
asIs: boolean,
props: T,
omitKeys: TKeys[]
): Partial<Omit<T, TKeys>> {
const filteredProps = omit(props, omitKeys) as Partial<T>;

// Check if it's an HTML tag and not a custom element
if (
typeof component === 'string' &&
component.indexOf('-') === -1 &&
!isCapital(component[0])
) {
if (!asIs) {
/**
* A failsafe check for esModule import issues
* if validAttr !== 'function' then it is an object of { default: Fn }
Expand Down Expand Up @@ -144,7 +140,15 @@ function styled(tag: any): any {

const render = (props: any, ref: any) => {
const { as: component = tag, class: className } = props;
const filteredProps: IProps = filterProps(component, props, [
const shouldKeepProps =
options.propsAsIs === undefined
? !(
typeof component === 'string' &&
component.indexOf('-') === -1 &&
!isCapital(component[0])
)
: options.propsAsIs;
const filteredProps: IProps = filterProps(shouldKeepProps, props, [
'as',
'class',
]);
Expand All @@ -157,7 +161,7 @@ function styled(tag: any): any {
const { vars } = options;

if (vars) {
const style: { [key: string]: string } = {};
const style: Record<string, string> = {};

// eslint-disable-next-line guard-for-in,no-restricted-syntax
for (const name in vars) {
Expand Down
25 changes: 23 additions & 2 deletions packages/tags/src/types.ts
Expand Up @@ -3,6 +3,12 @@ import type {
Identifier,
TemplateElement,
MemberExpression,
BigIntLiteral,
BooleanLiteral,
DecimalLiteral,
NullLiteral,
NumericLiteral,
StringLiteral,
} from '@babel/types';

export type StyledMeta = {
Expand Down Expand Up @@ -41,7 +47,7 @@ export type Serializable = JSONValue;

export type Value = (() => void) | StyledMeta | CSSable;

export type ValueCache = Map<string, unknown>;
export type ValueCache = Map<string | number | boolean | null, unknown>;

export type Artifact = [name: string, data: unknown];

Expand Down Expand Up @@ -88,6 +94,7 @@ export type BuildCodeFrameErrorFn = <TError extends Error>(
export enum ValueType {
LAZY,
FUNCTION,
CONST,
}

export type LazyValue = {
Expand All @@ -104,7 +111,21 @@ export type FunctionValue = {
source: string;
};

export type ExpressionValue = LazyValue | FunctionValue;
export type ConstValue = {
buildCodeFrameError: BuildCodeFrameErrorFn;
ex:
| StringLiteral
| NumericLiteral
| NullLiteral
| BooleanLiteral
| BigIntLiteral
| DecimalLiteral;
kind: ValueType.CONST;
source: string;
value: string | number | boolean | null;
};

export type ExpressionValue = LazyValue | FunctionValue | ConstValue;

export type Replacements = Array<{
length: number;
Expand Down
2 changes: 1 addition & 1 deletion packages/tags/src/utils/templateProcessor.ts
Expand Up @@ -67,7 +67,7 @@ export default function templateProcessor(
: { line: end.line, column: end.column + 1 },
};

const value = valueCache.get(ex.name);
const value = 'value' in item ? item.value : valueCache.get(item.ex.name);

throwIfInvalid(
tagProcessor.isValidValue.bind(tagProcessor),
Expand Down

0 comments on commit 63f56d4

Please sign in to comment.