diff --git a/docs/src/content/docs/v3/other/babel-plugin.mdx b/docs/src/content/docs/v3/other/babel-plugin.mdx index ab400087..4d4e135e 100644 --- a/docs/src/content/docs/v3/other/babel-plugin.mdx +++ b/docs/src/content/docs/v3/other/babel-plugin.mdx @@ -64,6 +64,21 @@ const stylesheet = StyleSheet.create((theme, rt) => ({ })) ``` + + ### 2. Attaching unique id to each StyleSheet diff --git a/plugin/__tests__/dependencies.spec.js b/plugin/__tests__/dependencies.spec.js index f30fe4aa..178438bc 100644 --- a/plugin/__tests__/dependencies.spec.js +++ b/plugin/__tests__/dependencies.spec.js @@ -506,7 +506,7 @@ pluginTester({ container: (headerColors, colorMap) => ({ backgroundColor: headerColors[rt.colorScheme], paddingBottom: colorMap[theme.colors.primary], - uni__dependencies: [5, 0] + uni__dependencies: [0, 5] }) }), 664955283 @@ -559,7 +559,7 @@ pluginTester({ translateY: -rt.insets.ime } ], - uni__dependencies: [9] + uni__dependencies: [14] } }), 664955283 @@ -627,7 +627,7 @@ pluginTester({ if (someRandomInt === 5) { return { backgroundColor: theme.colors.background, - uni__dependencies: [0] + uni__dependencies: [0, 9, 11] } } @@ -635,19 +635,19 @@ pluginTester({ return { backgroundColor: theme.colors.barbie, paddingBottom: rt.insets.bottom, - uni__dependencies: [0, 9] + uni__dependencies: [0, 9, 11] } } if (someRandomInt === 15) { return { fontSize: rt.fontScale * 10, - uni__dependencies: [11] + uni__dependencies: [0, 9, 11] } } else { return { backgroundColor: theme.colors.blood, - uni__dependencies: [0] + uni__dependencies: [0, 9, 11] } } } @@ -857,6 +857,105 @@ pluginTester({ 664955283 ) ` + }, + { + title: 'Should correctly detect dependencies in weirdest syntax', + code: ` + import { View, Text } from 'react-native' + import { StyleSheet } from 'react-native-unistyles' + + export const Example = () => { + return ( + + Hello world + + ) + } + + const styles = StyleSheet.create(({ components: { test, other: { nested }} }, { insets: { ime }, screen, statusBar: { width, height } }) => { + const otherVariable = 2 + + return { + container: () => { + if (otherVariable === 2) { + return { + backgroundColor: nested + } + } + + if (otherVariable === 3) { + return { + marginTop: ime, + height: screen.height + } + } + + return nested + }, + container2: () => ({ + paddingBottom: ime, + height, + width + }) + } + }) + `, + output: ` + import { Text } from 'react-native-unistyles/components/native/Text' + import { View } from 'react-native-unistyles/components/native/View' + + import { StyleSheet } from 'react-native-unistyles' + + export const Example = () => { + return ( + + Hello world + + ) + } + + const styles = StyleSheet.create( + ( + { + components: { + test, + other: { nested } + } + }, + { insets: { ime }, screen, statusBar: { width, height } } + ) => { + const otherVariable = 2 + + return { + container: () => { + if (otherVariable === 2) { + return { + backgroundColor: nested, + uni__dependencies: [0, 14, 6] + } + } + + if (otherVariable === 3) { + return { + marginTop: ime, + height: screen.height, + uni__dependencies: [0, 14, 6] + } + } + + return { ...nested, uni__dependencies: [0, 14, 6] } + }, + container2: () => ({ + paddingBottom: ime, + height, + width, + uni__dependencies: [14, 12] + }) + } + }, + 664955283 + ) + ` } ] }) diff --git a/plugin/__tests__/ref.spec.js b/plugin/__tests__/ref.spec.js deleted file mode 100644 index 0939f306..00000000 --- a/plugin/__tests__/ref.spec.js +++ /dev/null @@ -1,471 +0,0 @@ -import { pluginTester } from 'babel-plugin-tester' -import plugin from '../' - -pluginTester({ - plugin, - pluginOptions: { - debug: false - }, - babelOptions: { - plugins: ['@babel/plugin-syntax-jsx'], - generatorOpts: { - retainLines: true - } - }, - tests: [ - { - title: 'Does nothing if there is no import from React Native', - code: ` - import { StyleSheet, View, Text } from 'custom-lib' - - export const Example = () => { - return ( - - Hello world - - ) - } - - const styles = StyleSheet.create({ - container: { - backgroundColor: 'red' - } - }) - `, - output: ` - import { StyleSheet, View, Text } from 'custom-lib' - - export const Example = () => { - return ( - - Hello world - - ) - } - - const styles = StyleSheet.create({ - container: { - backgroundColor: 'red' - } - }) - ` - }, - { - title: 'Preserves user\'s ref as function', - code: ` - import { useRef } from 'react' - import { View, Text } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - const myRef = useRef() - - return ( - { - doSomething(ref) - myRef.current = ref - }} - style={styles.container} - > - Hello world - - ) - } - - const styles = StyleSheet.create({ - container: { - backgroundColor: 'red' - } - }) - `, - output: ` - import { Text } from 'react-native-unistyles/components/native/Text' - import { View } from 'react-native-unistyles/components/native/View' - import { useRef } from 'react' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - const myRef = useRef() - - return ( - { - doSomething(ref) - myRef.current = ref - }} - style={styles.container} - > - Hello world - - ) - } - - const styles = StyleSheet.create( - { - container: { - backgroundColor: 'red' - } - }, - 92366683 - ) - ` - }, - { - title: 'Preserves user\'s ref as function with cleanup', - code: ` - import React from 'react' - import { View, Text } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - const myRef = React.useRef() - - return ( - { - doSomething(ref) - myRef.current = ref - - return () => { - customCleanup() - } - }} - style={styles.container} - > - Hello world - - ) - } - - const styles = StyleSheet.create({ - container: { - backgroundColor: 'red' - } - }) - `, - output: ` - import { Text } from 'react-native-unistyles/components/native/Text' - import { View } from 'react-native-unistyles/components/native/View' - import React from 'react' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - const myRef = React.useRef() - - return ( - { - doSomething(ref) - myRef.current = ref - - return () => { - customCleanup() - } - }} - style={styles.container} - > - Hello world - - ) - } - - const styles = StyleSheet.create( - { - container: { - backgroundColor: 'red' - } - }, - 92366683 - ) - ` - }, - { - title: 'Preserves user\'s ref as assigned arrow function', - code: ` - import React from 'react' - import { View, Text } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - const myRef = React.useRef() - const fn = ref => { - doSomething(ref) - myRef.current = ref - - return () => { - customCleanup2() - } - } - - return ( - - Hello world - - ) - } - - const styles = StyleSheet.create({ - container: { - backgroundColor: 'red' - } - }) - `, - output: ` - import { Text } from 'react-native-unistyles/components/native/Text' - import { View } from 'react-native-unistyles/components/native/View' - import React from 'react' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - const myRef = React.useRef() - const fn = ref => { - doSomething(ref) - myRef.current = ref - - return () => { - customCleanup2() - } - } - - return ( - - Hello world - - ) - } - - const styles = StyleSheet.create( - { - container: { - backgroundColor: 'red' - } - }, - 92366683 - ) - ` - }, - { - title: 'Preserves user\'s ref as assigned function function', - code: ` - import React from 'react' - import { View, Text } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - const myRef = React.useRef() - function fn(ref) { - doSomething(ref) - myRef.current = ref - - return () => { - customCleanup2() - } - } - - return ( - - Hello world - - ) - } - - const styles = StyleSheet.create({ - container: { - backgroundColor: 'red' - } - }) - `, - output: ` - import { Text } from 'react-native-unistyles/components/native/Text' - import { View } from 'react-native-unistyles/components/native/View' - import React from 'react' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - const myRef = React.useRef() - function fn(ref) { - doSomething(ref) - myRef.current = ref - - return () => { - customCleanup2() - } - } - - return ( - - Hello world - - ) - } - - const styles = StyleSheet.create( - { - container: { - backgroundColor: 'red' - } - }, - 92366683 - ) - ` - }, - { - title: 'Should keep order of spreads', - code: ` - import { View } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - return ( - - ) - } - - const styles = StyleSheet.create(theme => ({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.colors.backgroundColor - }, - secondProp: { - marginHorizontal: theme.gap(10), - backgroundColor: 'red' - }, - thirdProp: { - backgroundColor: 'blue' - } - })) - `, - output: ` - import { View } from 'react-native-unistyles/components/native/View' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - return - } - - const styles = StyleSheet.create( - theme => ({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.colors.backgroundColor, - uni__dependencies: [0] - }, - secondProp: { - marginHorizontal: theme.gap(10), - backgroundColor: 'red', - uni__dependencies: [0] - }, - thirdProp: { - backgroundColor: 'blue' - } - }), - 92366683 - ) - ` - }, - { - title: 'Should support nested styles', - code: ` - import { View } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = ({ styles }) => { - return ( - - ) - } - - const styles = StyleSheet.create(theme => ({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.colors.backgroundColor - } - })) - `, - output: ` - import { View } from 'react-native-unistyles/components/native/View' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = ({ styles }) => { - return - } - - const styles = StyleSheet.create( - theme => ({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.colors.backgroundColor, - uni__dependencies: [0] - } - }), - 92366683 - ) - ` - }, - { - title: 'Should support conditional styles', - code: ` - import { View } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = ({ condition }) => { - return ( - - ) - } - - const styles = StyleSheet.create(theme => ({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.colors.backgroundColor - } - })) - `, - output: ` - import { View } from 'react-native-unistyles/components/native/View' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = ({ condition }) => { - return - } - - const styles = StyleSheet.create( - theme => ({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.colors.backgroundColor, - uni__dependencies: [0] - } - }), - 92366683 - ) - ` - } - ] -}) diff --git a/plugin/__tests__/stylesheet.spec.js b/plugin/__tests__/stylesheet.spec.js index a741ed06..767d1e19 100644 --- a/plugin/__tests__/stylesheet.spec.js +++ b/plugin/__tests__/stylesheet.spec.js @@ -283,7 +283,7 @@ pluginTester({ backgroundColor: 'red', variants: {}, paddingTop: rt.insets.top, - uni__dependencies: [4, 9] + uni__dependencies: [9, 4] }) }), 798826616 @@ -332,7 +332,7 @@ pluginTester({ backgroundColor: hhsa.colors.background, variants: {}, paddingTop: dee.colorScheme === 'dark' ? 0 : 10, - uni__dependencies: [0, 4, 5] + uni__dependencies: [0, 5, 4] }) }), 798826616 @@ -383,14 +383,14 @@ pluginTester({ backgroundColor: theme.colors.background, variants: {}, paddingTop: rt.insets.top, - uni__dependencies: [0, 4, 9] + uni__dependencies: [0, 9, 4] }) } }, 798826616) ` }, { - title: 'Should generates two different ids for 2 stylesheets in the same file', + title: 'Should generate two different ids for 2 stylesheets in the same file', code: ` import { View, Text } from 'react-native' import { StyleSheet } from 'react-native-unistyles' @@ -442,7 +442,7 @@ pluginTester({ backgroundColor: theme.colors.background, variants: {}, paddingTop: rt.insets.top, - uni__dependencies: [0, 4, 9] + uni__dependencies: [0, 9, 4] }) } }, 798826616) @@ -452,202 +452,12 @@ pluginTester({ backgroundColor: theme.colors.background, variants: {}, paddingTop: rt.insets.top, - uni__dependencies: [0, 4, 9] + uni__dependencies: [0, 9, 4] }) } }, 798826617) ` }, - { - title: 'Should do nothing if pressable is parameterless arrow function and style is an object', - code: ` - import { View, Pressable, Text } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - return ( - - styles.pressable}> - Hello world - - - ) - } - - const styles = StyleSheet.create((theme, rt) => { - return { - container: () => ({ - backgroundColor: theme.colors.background, - variants: {}, - paddingTop: rt.insets.top - }), - pressable: { - marginRight: arg1 + arg2 - } - } - }) - `, - output: ` - import { Text } from 'react-native-unistyles/components/native/Text' - import { Pressable } from 'react-native-unistyles/components/native/Pressable' - import { View } from 'react-native-unistyles/components/native/View' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = () => { - return ( - - styles.pressable}> - Hello world - - - ) - } - - const styles = StyleSheet.create((theme, rt) => { - return { - container: () => ({ - backgroundColor: theme.colors.background, - variants: {}, - paddingTop: rt.insets.top, - uni__dependencies: [0, 4, 9] - }), - pressable: { - marginRight: arg1 + arg2 - } - } - }, 798826616) - ` - }, - { - title: 'Should handle pressable with arrow function and array of styles', - code: ` - import { View, Pressable, Text } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = ({ height }) => { - return ( - - [styles.sectionItem, styles.other(1), { height }, pressed && styles.pressed]}> - Hello world - - - ) - } - - const styles = StyleSheet.create((theme, rt) => ({ - sectionItem: { - width: 100, - height: 100, - theme: theme.colors.red - }, - pressed: { - marginBottom: rt.insets.bottom - } - })) - `, - output: ` - import { Text } from 'react-native-unistyles/components/native/Text' - import { Pressable } from 'react-native-unistyles/components/native/Pressable' - import { View } from 'react-native-unistyles/components/native/View' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = ({ height }) => { - return ( - - [styles.sectionItem, styles.other(1), { height }, pressed && styles.pressed]}> - Hello world - - - ) - } - - const styles = StyleSheet.create( - (theme, rt) => ({ - sectionItem: { - width: 100, - height: 100, - theme: theme.colors.red, - uni__dependencies: [0] - }, - pressed: { - marginBottom: rt.insets.bottom, - uni__dependencies: [9] - } - }), - 798826616 - ) - ` - }, - { - title: 'Should handle nested functions', - code: ` - import { View, Pressable, Text } from 'react-native' - import { StyleSheet } from 'react-native-unistyles' - - export const Example = ({ height }) => { - return ( - - [styles.sectionItem, { height }, pressed && styles.pressed(pressed), pressed ? styles.pressed : styles.notPressed]}> - Hello world - - - ) - } - - const styles = StyleSheet.create((theme, rt) => ({ - sectionItem: { - width: 100, - height: 100, - theme: theme.colors.red - }, - pressed: pressed => ({ - marginBottom: rt.insets.bottom - }) - })) - `, - output: ` - import { Text } from 'react-native-unistyles/components/native/Text' - import { Pressable } from 'react-native-unistyles/components/native/Pressable' - import { View } from 'react-native-unistyles/components/native/View' - - import { StyleSheet } from 'react-native-unistyles' - - export const Example = ({ height }) => { - return ( - - [ - styles.sectionItem, - { height }, - pressed && styles.pressed(pressed), - pressed ? styles.pressed : styles.notPressed - ]} - > - Hello world - - - ) - } - - const styles = StyleSheet.create( - (theme, rt) => ({ - sectionItem: { - width: 100, - height: 100, - theme: theme.colors.red, - uni__dependencies: [0] - }, - pressed: pressed => ({ - marginBottom: rt.insets.bottom, - uni__dependencies: [9] - }) - }), - 798826616 - ) - ` - }, { title: 'Should use same local name as user name while replacing imports', code: ` diff --git a/plugin/common.js b/plugin/common.js deleted file mode 100644 index 5936da15..00000000 --- a/plugin/common.js +++ /dev/null @@ -1,154 +0,0 @@ -function getIdentifierNameFromExpression(t, memberExpression) { - if (t.isIdentifier(memberExpression)) { - return [memberExpression.name] - } - - if (t.isSpreadElement(memberExpression)) { - return [getIdentifierNameFromExpression(t, memberExpression.argument)].flat() - } - - if (t.isObjectProperty(memberExpression)) { - return [getIdentifierNameFromExpression(t, memberExpression.value)].flat() - } - - if (t.isMemberExpression(memberExpression)) { - if (memberExpression.computed) { - return [ - getIdentifierNameFromExpression(t, memberExpression.property), - getIdentifierNameFromExpression(t, memberExpression.object) - ].flat() - } - - const object = memberExpression.object - - // If the object is an Identifier, return its name - if (t.isIdentifier(object)) { - return [object.name] - } - - // If the object is another MemberExpression, recursively get the identifier - if (t.isMemberExpression(object)) { - return getIdentifierNameFromExpression(t, object).flat() - } - } - - if (t.isBinaryExpression(memberExpression)) { - return [ - getIdentifierNameFromExpression(t, memberExpression.left), - getIdentifierNameFromExpression(t, memberExpression.right) - ].flat() - } - - if (t.isCallExpression(memberExpression)) { - return getIdentifierNameFromExpression(t, memberExpression.callee) - } - - if (t.isConditionalExpression(memberExpression)) { - return [ - getIdentifierNameFromExpression(t, memberExpression.test.left), - getIdentifierNameFromExpression(t, memberExpression.test.right), - getIdentifierNameFromExpression(t, memberExpression.alternate), - getIdentifierNameFromExpression(t, memberExpression.consequent), - getIdentifierNameFromExpression(t, memberExpression.test) - ].flat() - } - - if (t.isArrayExpression(memberExpression)) { - return memberExpression.elements.map(expression => getIdentifierNameFromExpression(t, expression)).flat() - } - - if (t.isArrowFunctionExpression(memberExpression)) { - return memberExpression.body.properties.map(prop => getIdentifierNameFromExpression(t, prop.value)).flat() - } - - if (t.isTemplateLiteral(memberExpression)) { - return memberExpression.expressions.map(expression => getIdentifierNameFromExpression(t, expression)).flat() - } - - if (t.isObjectExpression(memberExpression)) { - return memberExpression.properties - .filter(property => t.isObjectProperty(property)) - .flatMap(property => getIdentifierNameFromExpression(t, property.value)) - } - - if (t.isUnaryExpression(memberExpression)) { - return getIdentifierNameFromExpression(t, memberExpression.argument.object) - } - - return [] -} - -function getSecondPropertyName(t, memberExpression) { - if (t.isUnaryExpression(memberExpression)) { - return getSecondPropertyName(t, memberExpression.argument.object) - } - - if (t.isConditionalExpression(memberExpression)) { - return [ - getSecondPropertyName(t, memberExpression.test.left), - getSecondPropertyName(t, memberExpression.test.right), - getSecondPropertyName(t, memberExpression.alternate), - getSecondPropertyName(t, memberExpression.consequent), - getSecondPropertyName(t, memberExpression.test) - ].flat() - } - - if (t.isTemplateLiteral(memberExpression)) { - return memberExpression.expressions.map(expression => getSecondPropertyName(t, expression)).flat() - } - - if (t.isBinaryExpression(memberExpression)) { - return [ - getSecondPropertyName(t, memberExpression.left), - getSecondPropertyName(t, memberExpression.right) - ].flat() - } - - if (t.isObjectExpression(memberExpression)) { - return memberExpression.properties - .filter(property => t.isObjectProperty(property)) - .flatMap(property => getSecondPropertyName(t, property.value)) - } - - if (t.isArrayExpression(memberExpression)) { - return memberExpression.elements.map(expression => getSecondPropertyName(t, expression)).flat() - } - - if (!t.isMemberExpression(memberExpression)) { - return [] - } - - let current = memberExpression.computed - ? memberExpression.property - : memberExpression - let propertyName = null - - while (t.isMemberExpression(current)) { - propertyName = current.property - current = current.object - } - - // special case for IME - if (propertyName && t.isIdentifier(propertyName) && propertyName.name === 'insets') { - if (t.isIdentifier(memberExpression.property) && memberExpression.property.name === "ime") { - return [memberExpression.property.name] - } - - return [propertyName.name] - } - - if (propertyName && t.isIdentifier(propertyName)) { - return [propertyName.name] - } - - if (propertyName) { - return [propertyName.value] - } - - return [] -} - -module.exports = { - getIdentifierNameFromExpression, - getSecondPropertyName -} diff --git a/plugin/index.js b/plugin/index.js index 9fc34d1e..3d805138 100644 --- a/plugin/index.js +++ b/plugin/index.js @@ -1,6 +1,6 @@ const { addUnistylesImport, isInsideNodeModules } = require('./import') const { hasStringRef } = require('./ref') -const { isUnistylesStyleSheet, analyzeDependencies, addStyleSheetTag, getUnistyles, isKindOfStyleSheet, maybeAddThemeDependencyToMemberExpression, addThemeDependencyToMemberExpression, getStyleSheetLocalNames } = require('./stylesheet') +const { isUnistylesStyleSheet, addStyleSheetTag, isKindOfStyleSheet, getStylesDependenciesFromFunction, addDependencies, getStylesDependenciesFromObject } = require('./stylesheet') const { extractVariants } = require('./variants') const { REACT_NATIVE_COMPONENT_NAMES, REPLACE_WITH_UNISTYLES_PATHS, REPLACE_WITH_UNISTYLES_EXOTIC_PATHS, NATIVE_COMPONENTS_PATHS } = require('./consts') const { handleExoticImport } = require('./exotic') @@ -149,43 +149,38 @@ module.exports = function ({ types: t }) { const arg = path.node.arguments[0] - // Object passed to StyleSheet.create + // Object passed to StyleSheet.create (may contain variants) if (t.isObjectExpression(arg)) { - arg.properties.forEach(property => { - if (t.isObjectProperty(property)) { - const propertyValues = getUnistyles(t, property) + const detectedDependencies = getStylesDependenciesFromObject(t, path) - propertyValues.forEach(propertyValue => { - analyzeDependencies(t, state, property.key.name, propertyValue, [], []) + if (detectedDependencies) { + if (t.isObjectExpression(arg)) { + arg.properties.forEach(property => { + if (detectedDependencies[property.key.name]) { + addDependencies(t, state, property.key.name, property, detectedDependencies[property.key.name]) + } }) } - }) + } } // Function passed to StyleSheet.create (e.g., theme => ({ container: {} })) if (t.isArrowFunctionExpression(arg) || t.isFunctionExpression(arg)) { - const localNames = getStyleSheetLocalNames(t, arg) - const body = t.isBlockStatement(arg.body) - ? arg.body.body.find(statement => t.isReturnStatement(statement)).argument - : arg.body - - // Ensure the function body returns an object - if (t.isObjectExpression(body)) { - body.properties.forEach(property => { - if (t.isObjectProperty(property)) { - const propertyValues = getUnistyles(t, property) - - // special case for non object/function properties - // maybe user used inlined theme? ({ container: theme.components.container }) - if (propertyValues.length === 0 && maybeAddThemeDependencyToMemberExpression(t, property, localNames.theme)) { - addThemeDependencyToMemberExpression(t, property) + const detectedDependencies = getStylesDependenciesFromFunction(t, path) + + if (detectedDependencies) { + const body = t.isBlockStatement(arg.body) + ? arg.body.body.find(statement => t.isReturnStatement(statement)).argument + : arg.body + + // Ensure the function body returns an object + if (t.isObjectExpression(body)) { + body.properties.forEach(property => { + if (detectedDependencies[property.key.name]) { + addDependencies(t, state, property.key.name, property, detectedDependencies[property.key.name]) } - - propertyValues.forEach(propertyValue => { - analyzeDependencies(t, state, property.key.name, propertyValue, localNames.theme, localNames.miniRuntime) - }) - } - }) + }) + } } } } diff --git a/plugin/stylesheet.js b/plugin/stylesheet.js index 4c92fb45..7a0f630a 100644 --- a/plugin/stylesheet.js +++ b/plugin/stylesheet.js @@ -1,5 +1,3 @@ -const { getIdentifierNameFromExpression, getSecondPropertyName } = require('./common') - const UnistyleDependency = { Theme: 0, ThemeName: 1, @@ -19,7 +17,7 @@ const UnistyleDependency = { } function stringToUniqueId(str) { - let hash = 0; + let hash = 0 for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i) @@ -63,268 +61,505 @@ function addStyleSheetTag(t, path, state) { callee.container.arguments.push(t.numericLiteral(uniqueId)) } -function getStyleSheetLocalNames(t, functionArg) { - const params = functionArg.params - const hasTheme = params.length >= 1 - const hasMiniRuntime = params.length === 2 - const getProperty = (property, allowNested) => { - if (t.isIdentifier(property.value)) { - return property.value.name +const getProperty = (t, property) => { + if (!property) { + return undefined + } + + if (t.isIdentifier(property)) { + return { + properties: [property.name] } + } + + if (t.isObjectPattern(property)) { + const matchingProperties = property.properties.flatMap(p => getProperty(t, p)) - if (!t.isObjectPattern(property.value)) { - return undefined + return { + properties: matchingProperties.flatMap(properties => properties.properties) } + } - if (allowNested) { - return property.value.properties.flatMap(getProperty) + if (t.isObjectProperty(property) && t.isIdentifier(property.value)) { + return { + properties: [property.key.name] } + } - // we can force allow nested only for insets - const hasIme = property.value.properties.find(property => property.key.name === 'ime') - const lastKeyValue = property.value.properties.flatMap(getProperty) + if (t.isObjectProperty(property) && t.isObjectPattern(property.value)) { + const matchingProperties = property.value.properties.flatMap(p => getProperty(t, p)) - if (hasIme) { - return lastKeyValue + return { + parent: property.key.name, + properties: matchingProperties.flatMap(properties => properties.properties) } - - return `${property.key.name}.${lastKeyValue}` } - const getLocalNames = (param, allowNested) => { - if (t.isObjectPattern(param)) { - return param.properties - .flatMap(property => getProperty(property, allowNested)) - .filter(Boolean) + + return undefined +} + +function getStylesDependenciesFromObject(t, path) { + const detectedStylesWithVariants = new Set() + const stylesheet = path.node.arguments[0] + + stylesheet.properties.forEach(property => { + if (!t.isIdentifier(property.key)) { + return } + if (t.isObjectProperty(property)) { + if(t.isObjectExpression(property.value)) { + property.value.properties.forEach(innerProp => { + if (t.isIdentifier(innerProp.key) && innerProp.key.name === 'variants') { + detectedStylesWithVariants.add({ + label: 'variants', + key: property.key.name + }) + } + }) - if (t.isIdentifier(param)) { - return [param.name] + } } - return [] + if (t.isArrowFunctionExpression(property.value)) { + if(t.isObjectExpression(property.value.body)) { + property.value.body.properties.forEach(innerProp => { + if (t.isIdentifier(innerProp.key) && innerProp.key.name === 'variants') { + detectedStylesWithVariants.add({ + label: 'variants', + key: property.key.name + }) + } + }) + + } + } + }) + + const variants = Array.from(detectedStylesWithVariants) + + return variants.reduce((acc, { key, label }) => { + if (acc[key]) { + return { + ...acc, + [key]: [ + ...acc[key], + label + ] + } + } + + return { + ...acc, + [key]: [label] + } + }, []) +} + +function getStylesDependenciesFromFunction(t, path) { + const funcPath = path.get('arguments.0') + + if (!funcPath) { + return } - return { - theme: hasTheme ? getLocalNames(params[0], true) : [], - miniRuntime: hasMiniRuntime ? getLocalNames(params[1], false) : [] + const params = funcPath.node.params + const [themeParam, rtParam] = params + + let themeNames = [] + + // destructured theme object + if (themeParam.type === 'ObjectPattern') { + // If destructured, collect all property names + for (const prop of themeParam.properties) { + themeNames.push(getProperty(t, prop)) + } } -} -function maybeAddThemeDependencyToMemberExpression(t, property, themeLocalNames) { - if (t.isIdentifier(property)) { - return themeLocalNames.includes(property.name) + // user used 'theme' without destructuring + if (themeParam.type === 'Identifier') { + themeNames.push({ + properties: [themeParam.name] + }) } - if (t.isObjectProperty(property)) { - return maybeAddThemeDependencyToMemberExpression(t, property.value, themeLocalNames) + let rtNames = [] + + // destructured rt object + if (rtParam && rtParam.type === 'ObjectPattern') { + // If destructured, collect all property names + for (const prop of rtParam.properties) { + rtNames.push(getProperty(t, prop)) + } } - if (t.isMemberExpression(property)) { - return maybeAddThemeDependencyToMemberExpression(t, property.object, themeLocalNames) + // user used 'rt' without destructuring + if (rtParam && rtParam.type === 'Identifier') { + rtNames.push({ + properties: [rtParam.name] + }) } -} + // get returned object or return statement from StyleSheet.create function + let returnedObjectPath = null -/** @param {import('./index').UnistylesPluginPass} state */ -function analyzeDependencies(t, state, name, unistyleObj, themeNames, rtNames) { - const debugMessage = deps => { - if (state.opts.debug) { - const mappedDeps = deps - .map(dep => Object.keys(UnistyleDependency).find(key => UnistyleDependency[key] === dep)) - .join(', ') + if (funcPath.get('body').isObjectExpression()) { + returnedObjectPath = funcPath.get('body') + } else { + funcPath.traverse({ + ReturnStatement(retPath) { + if (!returnedObjectPath && retPath.get('argument').isObjectExpression()) { + returnedObjectPath = retPath.get('argument') + } + } + }) + } - console.log(`${state.filename.replace(`${state.file.opts.root}/`, '')}: styles.${name}: [${mappedDeps}]`) - } + if (!returnedObjectPath) { + // there is no returned object + // abort + + return } - const unistyle = unistyleObj.properties - const dependencies = [] - Object.values(unistyle).forEach(uni => { - const identifiers = getIdentifierNameFromExpression(t, uni) + const detectedStylesWithVariants = new Set() + + // detect variants via Scope + returnedObjectPath.get('properties').forEach(propPath => { + // get style name + const stylePath = propPath.get('key') - if (themeNames.some(name => identifiers.some(id => id === name))) { - dependencies.push(UnistyleDependency.Theme) + if (!stylePath.isIdentifier()) { + return } - const matchingRtNames = rtNames.reduce((acc, name) => { - if (name.includes('.')) { - const key = name.split('.').at(0) + const styleKey = stylePath.node.name - if (identifiers.some(id => name.includes(id))) { - return [ - ...acc, - key - ] - } + const valuePath = propPath.get('value') + + if (valuePath.isObjectExpression()) { + const hasVariants = valuePath.get('properties').some(innerProp => { + const innerKey = innerProp.get('key') + + return innerKey.isIdentifier() && innerKey.node.name === 'variants' + }) - return acc + if (hasVariants) { + detectedStylesWithVariants.add({ + label: 'variants', + key: styleKey + }) } + } + if (valuePath.isArrowFunctionExpression()) { + if(t.isObjectExpression(valuePath.node.body)) { + const hasVariants = valuePath.node.body.properties.some(innerProp => { - if (identifiers.some(id => id === name)) { - return [ - ...acc, - name - ] + return t.isIdentifier(innerProp.key) && innerProp.key.name === 'variants' + }) + + if (hasVariants) { + detectedStylesWithVariants.add({ + label: 'variants', + key: styleKey + }) + } } + } + }) - return acc - }, []) + const detectedStylesWithTheme = new Set() - if (matchingRtNames.length > 0) { - const propertyNames = getSecondPropertyName(t, uni.value) + // detect theme dependencies via Scope + themeNames.forEach(({ properties }) => { + properties.forEach(property => { + const binding = funcPath.scope.getBinding(property) - matchingRtNames - .concat(propertyNames) - .filter(Boolean) - .forEach(propertyName => { - switch (propertyName) { - case 'themeName': { - dependencies.push(UnistyleDependency.ThemeName) + if (!binding) { + return + } - return - } - case 'adaptiveThemes': { - dependencies.push(UnistyleDependency.AdaptiveThemes) + binding.referencePaths.forEach(refPath => { + // find key of the style that we are referring to + const containerProp = refPath + .findParent(parent => parent.isObjectProperty() && parent.parentPath === returnedObjectPath) - return - } - case 'breakpoint': { - dependencies.push(UnistyleDependency.Breakpoints) + if (!containerProp) { + return + } - return - } - case 'colorScheme': { - dependencies.push(UnistyleDependency.ColorScheme) + const keyNode = containerProp.get('key') + const styleKey = keyNode.isIdentifier() + ? keyNode.node.name + : keyNode.isLiteral() + ? keyNode.node.value + : null + + if (styleKey) { + detectedStylesWithTheme.add({ + label: 'theme', + key: styleKey + }) + } + }) + }) + }) - return - } - case 'screen': { - dependencies.push(UnistyleDependency.Dimensions) + const detectedStylesWithRt = new Set() + const localRtName = t.isIdentifier(rtParam) + ? rtParam.name + : undefined - return - } - case 'isPortrait': - case 'isLandscape': { - dependencies.push(UnistyleDependency.Orientation) + // detect rt dependencies via Scope + rtNames.forEach(({ properties, parent }) => { + properties.forEach(property => { + const rtBinding = funcPath.scope.getBinding(property) - return - } - case 'contentSizeCategory': { - dependencies.push(UnistyleDependency.ContentSizeCategory) + if (!rtBinding) { + return + } - return - } - case 'ime': { - dependencies.push(UnistyleDependency.Ime) + const isValidDependency = Boolean(toUnistylesDependency(property)) - return - } - case 'insets': { - dependencies.push(UnistyleDependency.Insets) + let validRtName = property - return - } - case 'pixelRatio': { - dependencies.push(UnistyleDependency.PixelRatio) + // user used nested destructing, find out parent key + if (!isValidDependency && (!localRtName || (localRtName && localRtName !== property))) { + if (!parent) { + return + } - return - } - case 'fontScale': { - dependencies.push(UnistyleDependency.FontScale) + if (!Boolean(toUnistylesDependency(parent))) { + return + } - return - } - case 'statusBar': { - dependencies.push(UnistyleDependency.StatusBar) + validRtName = parent + } + + rtBinding.referencePaths.forEach(refPath => { + // to detect rt dependencies we need to get parameter not rt itself + // eg. rt.screen.width -> screen + // rt.insets.top -> insets + // special case: rt.insets.ime -> ime + + let usedLabel = validRtName - return + if (refPath.parentPath.isMemberExpression() && refPath.parentPath.get('object') === refPath) { + const memberExpr = refPath.parentPath + const propPath = memberExpr.get('property') + + if (propPath.isIdentifier()) { + if (localRtName) { + usedLabel = propPath.node.name } - case 'navigationBar': { - dependencies.push(UnistyleDependency.NavigationBar) - return + if ( + usedLabel === 'insets' && + memberExpr.parentPath.isMemberExpression() && + memberExpr.parentPath.get('object') === memberExpr + ) { + const secondPropPath = memberExpr.parentPath.get('property') + + if (secondPropPath.isIdentifier() && secondPropPath.node.name === 'ime') { + usedLabel = 'ime' + } } } - }) - } + } + + // find key of the style that we are referring to + const containerProp = refPath + .findParent(parent => parent.isObjectProperty() && parent.parentPath === returnedObjectPath) + + if (!containerProp) { + return + } - if (uni.key && uni.key.name === 'variants') { - dependencies.push(UnistyleDependency.Variants) + const keyNode = containerProp.get('key') + const styleKey = keyNode.isIdentifier() + ? keyNode.node.name + : keyNode.isLiteral() + ? keyNode.node.value + : null + + if (styleKey) { + detectedStylesWithRt.add({ + label: usedLabel, + key: styleKey + }) + } + }) + }) + }) + + const variants = Array.from(detectedStylesWithVariants) + const theme = Array.from(detectedStylesWithTheme) + const rt = Array.from(detectedStylesWithRt) + + return theme + .concat(rt) + .concat(variants) + .reduce((acc, { key, label }) => { + if (acc[key]) { + return { + ...acc, + [key]: [ + ...acc[key], + label + ] + } + } + + return { + ...acc, + [key]: [label] + } + }, []) +} + +function toUnistylesDependency(dependency) { + switch (dependency) { + case 'theme': { + return UnistyleDependency.Theme + } + case 'themeName': { + return UnistyleDependency.ThemeName + } + case 'adaptiveThemes': { + return UnistyleDependency.AdaptiveThemes + } + case 'breakpoint': { + return UnistyleDependency.Breakpoints + } + case 'colorScheme': { + return UnistyleDependency.ColorScheme + } + case 'screen': { + return UnistyleDependency.Dimensions + } + case 'isPortrait': + case 'isLandscape': { + return UnistyleDependency.Orientation + } + case 'contentSizeCategory': { + return UnistyleDependency.ContentSizeCategory + } + case 'ime': { + return UnistyleDependency.Ime + } + case 'insets': { + return UnistyleDependency.Insets + } + case 'pixelRatio': { + return UnistyleDependency.PixelRatio + } + case 'fontScale': { + return UnistyleDependency.FontScale + } + case 'statusBar': { + return UnistyleDependency.StatusBar + } + case 'navigationBar': { + return UnistyleDependency.NavigationBar + } + case 'variants': { + return UnistyleDependency.Variants } // breakpoints are too complex and are handled by C++ - }) + } +} - // add dependencies to the unistyle object if any found - if (dependencies.length > 0) { - const uniqueDependencies = Array.from(new Set(dependencies)) +function getReturnStatementsFromBody(t, node, results = []) { + if (t.isReturnStatement(node)) { + results.push(node) + } - debugMessage(uniqueDependencies) + if (t.isBlockStatement(node)) { + node.body.forEach(child => getReturnStatementsFromBody(t, child, results)) + } + + if (t.isIfStatement(node)) { + getReturnStatementsFromBody(t, node.consequent, results) - unistyleObj.properties.push( - t.objectProperty( - t.identifier('uni__dependencies'), - t.arrayExpression(uniqueDependencies.map(dep => t.numericLiteral(dep))) - ) - ) + if (node.alternate) { + getReturnStatementsFromBody(t, node.alternate, results) + } } + + return results } -function getUnistyles(t, property) { - const propertyValue = t.isArrowFunctionExpression(property.value) - ? property.value.body - : property.value +function addDependencies(t, state, styleName, unistyle, detectedDependencies) { + const debugMessage = deps => { + if (state.opts.debug) { + const mappedDeps = deps + .map(dep => Object.keys(UnistyleDependency).find(key => UnistyleDependency[key] === dep)) + .join(', ') - if (t.isObjectExpression(propertyValue)) { - return [propertyValue] + console.log(`${state.filename.replace(`${state.file.opts.root}/`, '')}: styles.${styleName}: [${mappedDeps}]`) + } } - if (t.isBlockStatement(propertyValue)) { - // here we might have single return statement - // or if-else statements with return statements - return propertyValue.body - .flatMap(value => { - if (t.isReturnStatement(value)) { - return [value] - } + const styleDependencies = detectedDependencies.map(toUnistylesDependency) - if (!t.isIfStatement(value)) { - return [] - } + // add metadata about dependencies + if (styleDependencies.length > 0) { + const uniqueDependencies = Array.from(new Set(styleDependencies)) + + debugMessage(uniqueDependencies) + + let targets = [] - return [value.consequent, value.alternate] - .filter(Boolean) - .flatMap(value => { - if (t.isBlockStatement(value)) { - return value.body.filter(t.isReturnStatement) + if (t.isArrowFunctionExpression(unistyle.value) || t.isFunctionExpression(unistyle.value)) { + if (t.isObjectExpression(unistyle.value.body)) { + targets.push(unistyle.value.body) + } + + if (t.isBlockStatement(unistyle.value.body)) { + targets = getReturnStatementsFromBody(t, unistyle.value.body) + .map(node => { + if (t.isIdentifier(node.argument)) { + node.argument = t.objectExpression([ + t.spreadElement(node.argument) + ]) } + + return node.argument }) - }) - .map(value => value.argument) - } + } + } - return [] -} + if (t.isObjectExpression(unistyle.value)) { + targets.push(unistyle.value) + } + + if (t.isMemberExpression(unistyle.value)) { + // convert to object + unistyle.value = t.objectExpression([t.spreadElement(unistyle.value)]) -function addThemeDependencyToMemberExpression(t, path) { - path.value = t.objectExpression([ - t.spreadElement(path.value), - t.objectProperty( - t.identifier('uni__dependencies'), - t.arrayExpression([t.numericLiteral(UnistyleDependency.Theme)]) - ) - ]) + targets.push(unistyle.value) + } + + if (targets.length > 0) { + targets.forEach(target => { + target.properties.push( + t.objectProperty( + t.identifier('uni__dependencies'), + t.arrayExpression(uniqueDependencies.map(dep => t.numericLiteral(dep))) + ) + ) + }) + } + } } module.exports = { isUnistylesStyleSheet, - analyzeDependencies, + addDependencies, addStyleSheetTag, - getUnistyles, - isKindOfStyleSheet, - getStyleSheetLocalNames, - maybeAddThemeDependencyToMemberExpression, - addThemeDependencyToMemberExpression + getStylesDependenciesFromObject, + getStylesDependenciesFromFunction, + isKindOfStyleSheet }