Skip to content

Commit

Permalink
[zero] Add useThemeProps processor (#40648)
Browse files Browse the repository at this point in the history
  • Loading branch information
siriwatknp committed Jan 24, 2024
1 parent 2a29460 commit 2d39985
Show file tree
Hide file tree
Showing 18 changed files with 210 additions and 30 deletions.
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

0 comments on commit 2d39985

Please sign in to comment.