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

[zero] Add useThemeProps processor #40648

Merged
merged 12 commits into from
Jan 24, 2024
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
13 changes: 12 additions & 1 deletion apps/zero-runtime-next-app/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
const { withZeroPlugin } = require('@mui/zero-next-plugin');
const { experimental_extendTheme: extendTheme } = require('@mui/material/styles');

const theme = extendTheme({ cssVarPrefix: 'app' });
const theme = extendTheme({
cssVarPrefix: 'app',
components: {
MuiBadge: {
defaultProps: {
color: 'error',
},
},
},
});
theme.getColorSchemeSelector = (targetColorScheme) =>
`[data-mui-color-scheme="${targetColorScheme}"] &`;

/**
* @typedef {import('@mui/zero-next-plugin').ZeroPluginConfig} ZeroPluginConfig
Expand Down
4 changes: 4 additions & 0 deletions apps/zero-runtime-next-app/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Image from 'next/image';
import { styled } from '@mui/zero-runtime';
import Badge from '@mui/material/Badge';
import styles from './page.module.css';

const Main = styled.main({
Expand Down Expand Up @@ -77,6 +78,9 @@ const Description = styled.div({
export default function Home() {
return (
<Main>
<Badge variant="dot">
<div>Hey</div>
</Badge>
<Description>
<p>
Get started by editing&nbsp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ const rule = {

return {
CallExpression(node) {
const isCreateUseThemePropsCall = node.callee.name === 'createUseThemeProps';
if (isCreateUseThemePropsCall) {
if (!node.arguments.length) {
context.report({ node, messageId: 'noNameValue' });
} else if (node.arguments[0].type !== 'Literal' || !node.arguments[0].value) {
context.report({ node: node.arguments[0], messageId: 'noNameValue' });
}
}

let nameLiteral = null;
const isUseThemePropsCall = node.callee.name === 'useThemeProps';
if (isUseThemePropsCall) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ ruleTester.run('mui-name-matches-component-name', rule, {
useThemeProps: (inProps) => useThemeProps({ props: inProps, name: 'MuiGrid2' }),
}) as OverridableComponent<Grid2TypeMap>;
`,
`
const useThemeProps = createUseThemeProps('MuiBadge');
`,
{
code: `
const StaticDateRangePicker = React.forwardRef(function StaticDateRangePicker<TDate>(
Expand Down Expand Up @@ -142,5 +145,37 @@ ruleTester.run('mui-name-matches-component-name', rule, {
},
],
},
{
code: `
const useThemeProps = createUseThemeProps();

const Badge = React.forwardRef(function Badge(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiBadge' });
});
`,
errors: [
{
message:
'Unable to resolve `name`. Please hardcode the `name` i.e. use a string literal.',
type: 'CallExpression',
},
],
},
{
code: `
const useThemeProps = createUseThemeProps({ name: 'MuiBadge' });

const Badge = React.forwardRef(function Badge(inProps, ref) {
const props = useThemeProps({ props: inProps, name: 'MuiBadge' });
});
`,
errors: [
{
message:
'Unable to resolve `name`. Please hardcode the `name` i.e. use a string literal.',
type: 'ObjectExpression',
},
],
},
],
});
5 changes: 3 additions & 2 deletions packages/mui-material/src/Badge/Badge.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { usePreviousProps } from '@mui/utils';
import { unstable_composeClasses as composeClasses } from '@mui/base/composeClasses';
import { useBadge } from '@mui/base/useBadge';
import { useSlotProps } from '@mui/base';
import { styled } from '../zero-styled';
import useThemeProps from '../styles/useThemeProps';
import { styled, createUseThemeProps } from '../zero-styled';
import capitalize from '../utils/capitalize';
import badgeClasses, { getBadgeUtilityClass } from './badgeClasses';

const RADIUS_STANDARD = 10;
const RADIUS_DOT = 4;

const useThemeProps = createUseThemeProps('MuiBadge');

const useUtilityClasses = (ownerState) => {
const { color, anchorOrigin, invisible, overlap, variant, classes = {} } = ownerState;

Expand Down
2 changes: 0 additions & 2 deletions packages/mui-material/src/zero-styled/index.d.ts

This file was deleted.

2 changes: 0 additions & 2 deletions packages/mui-material/src/zero-styled/index.js

This file was deleted.

8 changes: 8 additions & 0 deletions packages/mui-material/src/zero-styled/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import useThemeProps from '../styles/useThemeProps';

export { default as styled } from '../styles/styled';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function createUseThemeProps(name: string) {
return useThemeProps;
}
5 changes: 5 additions & 0 deletions packages/zero-runtime/exports/createUseThemeProps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Object.defineProperty(exports, '__esModule', {
value: true,
});

exports.default = require('../processors/createUseThemeProps').CreateUseThemePropsProcessor;
6 changes: 5 additions & 1 deletion packages/zero-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
"sx": "./exports/sx.js",
"keyframes": "./exports/keyframes.js",
"generateAtomics": "./exports/generateAtomics.js",
"css": "./exports/css.js"
"css": "./exports/css.js",
"createUseThemeProps": "./exports/createUseThemeProps.js"
}
},
"files": [
Expand Down Expand Up @@ -105,6 +106,9 @@
},
"./exports/sx": {
"default": "./exports/sx.js"
},
"./exports/createUseThemeProps": {
"default": "./exports/createUseThemeProps.js"
}
}
}
5 changes: 5 additions & 0 deletions packages/zero-runtime/src/createUseThemeProps.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface UseThemeProps {
<Props>(params: { theme: Record<string, any>; props: Props; name: string }): Props;
}

export default function createUseThemeProps(theme: any): UseThemeProps;
52 changes: 52 additions & 0 deletions packages/zero-runtime/src/createUseThemeProps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { internal_resolveProps as resolveProps } from '@mui/utils';

/**
* Runtime function for creating `useThemeProps`.
* In the codebase, the first argument will be a string that represent the component slug (should match one of the `theme.components.*`).
* Then, the transformation will replace the first argument with the `defaultProps` object if provided.
*/
export default function createUseThemeProps(nameOrDefaultProps) {
return function useThemeProps({ props }) {
if (typeof nameOrDefaultProps === 'string') {
// if no default props provided in the theme, return the props as is.
return props;
}
const defaultProps = nameOrDefaultProps;
// The same logic as in packages/mui-utils/src/resolveProps.ts
// TODO: consider reusing the logic from the utils package
const output = { ...props };

Object.keys(defaultProps).forEach((propName) => {
if (propName.toString().match(/^(components|slots)$/)) {
output[propName] = {
...defaultProps[propName],
...output[propName],
};
} else if (propName.toString().match(/^(componentsProps|slotProps)$/)) {
const defaultSlotProps = defaultProps[propName] || {};
const slotProps = props[propName];
output[propName] = {};

if (!slotProps || !Object.keys(slotProps)) {
// Reduce the iteration if the slot props is empty
output[propName] = defaultSlotProps;
} else if (!defaultSlotProps || !Object.keys(defaultSlotProps)) {
// Reduce the iteration if the default slot props is empty
output[propName] = slotProps;
} else {
output[propName] = { ...slotProps };
Object.keys(defaultSlotProps).forEach((slotPropName) => {
output[propName][slotPropName] = resolveProps(
defaultSlotProps[slotPropName],
slotProps[slotPropName],
);
});
}
} else if (output[propName] === undefined) {
output[propName] = defaultProps[propName];
}
});

return output;
};
}
1 change: 1 addition & 0 deletions packages/zero-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as sx } from './sx';
export { default as keyframes } from './keyframes';
export { generateAtomics, atomics } from './generateAtomics';
export { default as css } from './css';
export { default as createUseThemeProps } from './createUseThemeProps';
68 changes: 68 additions & 0 deletions packages/zero-runtime/src/processors/createUseThemeProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { validateParams, IOptions as IBaseOptions } from '@linaria/tags';
import type { Expression, Params, TailProcessorParams } from '@linaria/tags';
import BaseProcessor from './base-processor';
import { valueToLiteral } from '../utils/valueToLiteral';

type IOptions = IBaseOptions & {
themeArgs: {
theme: { components?: Record<string, { defaultProps?: Record<string, unknown> }> };
};
};

export class CreateUseThemePropsProcessor extends BaseProcessor {
componentName: string;

constructor(params: Params, ...args: TailProcessorParams) {
super(params, ...args);
if (params.length > 2) {
// no need to do any processing if it is an already transformed call or just a reference.
throw BaseProcessor.SKIP;
}
validateParams(params, ['callee', 'call'], `Invalid use of ${this.tagSource.imported} tag.`);
const [, callParam] = params;
const [, callArg] = callParam;
if (!callArg || callArg.ex.type !== 'StringLiteral') {
throw new Error(
`Invalid usage of \`createUseThemeProps\` tag, expected one string literal argument but got ${callArg?.ex.type}.`,
);
}
this.componentName = callArg.ex.value;
}

// eslint-disable-next-line class-methods-use-this
build(): void {}

doEvaltimeReplacement(): void {
this.replacer(this.value, false);
}

get value(): Expression {
return this.astService.nullLiteral();
}

doRuntimeReplacement(): void {
const t = this.astService;

const { themeArgs: { theme } = {} } = this.options as IOptions;
if (!theme?.components?.[this.componentName]?.defaultProps) {
return;
}

const useThemePropsImportIdentifier = t.addNamedImport(
this.tagSource.imported,
process.env.PACKAGE_NAME as string,
);

this.replacer(
t.callExpression(useThemePropsImportIdentifier, [
valueToLiteral(theme.components[this.componentName].defaultProps),
]),
true,
);
}

public override get asSelector(): string {
// For completeness, this is not intended to be used.
return `.${this.className}`;
}
}
12 changes: 0 additions & 12 deletions packages/zero-runtime/src/processors/styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ type ComponentMeta = {
skipSx?: boolean;
};

type DefaultProps = Record<string, string | number | boolean | unknown>;

/**
* Linaria tag processor responsible for converting complex `styled()()` calls
* at build-time to simple `styled` calls supported by runtime.
Expand Down Expand Up @@ -115,8 +113,6 @@ export class StyledProcessor extends BaseProcessor {

originalLocation: SourceLocation | null = null;

defaultProps: DefaultProps = {};

constructor(params: Params, ...args: TailProcessorParams) {
super(params, ...args);
if (params.length <= 2) {
Expand Down Expand Up @@ -333,11 +329,6 @@ export class StyledProcessor extends BaseProcessor {
componentMetaExpression = parsedMeta as ObjectExpression;
}
}
if (this.defaultProps && Object.keys(this.defaultProps).length > 0) {
argProperties.push(
t.objectProperty(t.identifier('defaultProps'), valueToLiteral(this.defaultProps)),
);
}

const styledImportIdentifier = t.addNamedImport(
this.tagSource.imported,
Expand Down Expand Up @@ -419,9 +410,6 @@ export class StyledProcessor extends BaseProcessor {
if ('variants' in componentData && componentData.variants) {
variantsAccumulator.push(...(componentData.variants as unknown as VariantData[]));
}
if ('defaultProps' in componentData && componentData.defaultProps) {
this.defaultProps = componentData.defaultProps as DefaultProps;
}
}

/**
Expand Down
9 changes: 1 addition & 8 deletions packages/zero-runtime/src/styled.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,7 @@ function defaultShouldForwardProp(propKey) {
* @param {Object} componentMeta.defaultProps Default props object copied over and inlined from theme object
*/
export default function styled(tag, componentMeta = {}) {
const {
name,
slot,
defaultProps = {},
shouldForwardProp = defaultShouldForwardProp,
} = componentMeta;
const { name, slot, shouldForwardProp = defaultShouldForwardProp } = componentMeta;
/**
* @TODO - Filter props and only pass necessary props to children
*
Expand All @@ -64,7 +59,6 @@ export default function styled(tag, componentMeta = {}) {
* @param {string} options.name
* @param {string} options.slot
* @param {ShouldForwardProp} options.shouldForwardProp
* @param {Object} options.defaultProps Default props object copied over and inlined from theme object
*/
function scopedStyledWithOptions(options = {}) {
const { displayName, classes = [], vars: cssVars = {}, variants = [] } = options;
Expand Down Expand Up @@ -163,7 +157,6 @@ export default function styled(tag, componentMeta = {}) {
});

StyledComponent.displayName = `Styled(${componentName})`;
StyledComponent.defaultProps = defaultProps;
// eslint-disable-next-line no-underscore-dangle
StyledComponent.__isStyled = true;

Expand Down
2 changes: 1 addition & 1 deletion packages/zero-runtime/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Options, defineConfig } from 'tsup';
import config from '../../tsup.config';
import packageJson from './package.json';

const processors = ['styled', 'sx', 'keyframes', 'generateAtomics', 'css'];
const processors = ['styled', 'sx', 'keyframes', 'generateAtomics', 'css', 'createUseThemeProps'];
const external = ['react', 'react-is', 'prop-types'];

const baseConfig: Options = {
Expand Down
2 changes: 1 addition & 1 deletion packages/zero-vite-plugin/src/zero-vite-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export default function zeroVitePlugin({
if (tagResult) {
return tagResult;
}
if (source.endsWith('/zero-styled')) {
if (source.endsWith('/zero-styled') || source.endsWith('/zero-useThemeProps')) {
return `${process.env.RUNTIME_PACKAGE_NAME}/exports/${tag}`;
}
return null;
Expand Down
Loading