diff --git a/packages/react-strict-dom/babel/migrate.js b/packages/react-strict-dom/babel/migrate.js
new file mode 100644
index 00000000..61a2b03d
--- /dev/null
+++ b/packages/react-strict-dom/babel/migrate.js
@@ -0,0 +1,1043 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+// Mapping of React Native's non-standard logical properties to their
+// W3C standard equivalents, as used by React Strict DOM.
+const LOGICAL_PROPERTY_TRANSFORMS = {
+ borderBottomEndRadius: 'borderEndEndRadius',
+ borderBottomStartRadius: 'borderEndStartRadius',
+ borderEndColor: 'borderInlineEndColor',
+ borderEndStyle: 'borderInlineEndStyle',
+ borderEndWidth: 'borderInlineEndWidth',
+ borderStartColor: 'borderInlineStartColor',
+ borderStartStyle: 'borderInlineStartStyle',
+ borderStartWidth: 'borderInlineStartWidth',
+ borderTopEndRadius: 'borderStartEndRadius',
+ borderTopStartRadius: 'borderStartStartRadius',
+ end: 'insetInlineEnd',
+ marginEnd: 'marginInlineEnd',
+ marginHorizontal: 'marginInline',
+ marginStart: 'marginInlineStart',
+ marginVertical: 'marginBlock',
+ paddingEnd: 'paddingInlineEnd',
+ paddingHorizontal: 'paddingInline',
+ paddingStart: 'paddingInlineStart',
+ paddingVertical: 'paddingBlock',
+ start: 'insetInlineStart'
+};
+
+// Handle style property name transformations
+function transformStyleProperty(t, styleDeclarationPath) {
+ const styleDeclaration = styleDeclarationPath.node;
+ if (styleDeclaration.key?.type !== 'Identifier') {
+ return;
+ }
+ const styleProperty = styleDeclaration.key;
+ const styleValue = styleDeclaration.value;
+
+ const currentPropertyName = styleProperty.name;
+
+ // Transform non-standard logical CSS properties
+ const standardLogicalPropertyName =
+ LOGICAL_PROPERTY_TRANSFORMS[currentPropertyName];
+
+ if (standardLogicalPropertyName) {
+ if (
+ styleValue?.type === 'StringLiteral' ||
+ styleValue?.type === 'NumericLiteral' ||
+ styleValue?.type === 'NullLiteral'
+ ) {
+ // Number values
+ const isValueNumber = styleValue.type === 'NumericLiteral';
+ // String 'auto'
+ const isValueAuto =
+ styleValue.type === 'StringLiteral' && styleValue.value === 'auto';
+ // Strings ending with 'px' or '%'
+ const isValueLength =
+ styleValue.type === 'StringLiteral' &&
+ (styleValue.value.endsWith('px') || styleValue.value.endsWith('%'));
+ // null values
+ const isValidNull = styleValue.type === 'NullLiteral';
+ // Color values (for borderEndColor, borderStartColor, etc.)
+ const isColorProperty =
+ currentPropertyName.includes('Color') &&
+ styleValue.type === 'StringLiteral';
+
+ const isValidValue =
+ isValueNumber ||
+ isValueAuto ||
+ isValueLength ||
+ isValidNull ||
+ isColorProperty;
+
+ if (isValidValue) {
+ styleProperty.name = standardLogicalPropertyName;
+ }
+ }
+ }
+
+ // Transform 'fontVariant'
+ // From array of strings to space-separated string
+ if (
+ currentPropertyName === 'fontVariant' &&
+ styleValue?.type === 'ArrayExpression'
+ ) {
+ const elements = styleValue.elements;
+ const stringValues = [];
+ let allValid = true;
+
+ for (const element of elements) {
+ if (element?.type === 'StringLiteral') {
+ stringValues.push(element.value);
+ } else {
+ allValid = false;
+ break;
+ }
+ }
+
+ if (allValid && stringValues.length > 0) {
+ styleDeclarationPath.node.value = t.stringLiteral(stringValues.join(' '));
+ }
+ }
+
+ // Transform 'textAlignVertical' (Android)
+ if (
+ currentPropertyName === 'textAlignVertical' &&
+ styleValue?.type === 'StringLiteral'
+ ) {
+ const value = styleValue.value;
+ styleDeclarationPath.node.key = t.identifier('verticalAlign');
+ styleDeclarationPath.node.value = t.stringLiteral(
+ value === 'center' ? 'middle' : value
+ );
+ }
+
+ // Transform 'transform'
+ // From React Native transform array to CSS transform string
+ if (
+ currentPropertyName === 'transform' &&
+ styleValue?.type === 'ArrayExpression'
+ ) {
+ const elements = styleValue.elements;
+ const transformFunctions = [];
+ let allValid = true;
+
+ for (const element of elements) {
+ // Each item in the array is an object
+ // e.g. { perspective: 50 }
+ if (
+ element?.type === 'ObjectExpression' &&
+ element.properties.length === 1
+ ) {
+ const property = element.properties[0];
+ if (
+ property?.type === 'ObjectProperty' &&
+ property.key?.type === 'Identifier'
+ ) {
+ const transformType = property.key.name;
+
+ if (transformType === 'matrix' || transformType === 'matrix3d') {
+ // For matrix transforms, the value should be an ArrayExpression
+ if (property.value?.type === 'ArrayExpression') {
+ const matrixValues = [];
+ let matrixValid = true;
+
+ for (const matrixElement of property.value.elements) {
+ if (matrixElement?.type === 'NumericLiteral') {
+ matrixValues.push(matrixElement.value);
+ } else {
+ matrixValid = false;
+ break;
+ }
+ }
+
+ if (matrixValid && matrixValues.length > 0) {
+ transformFunctions.push(
+ `${transformType}(${matrixValues.join(',')})`
+ );
+ } else {
+ allValid = false;
+ break;
+ }
+ } else {
+ allValid = false;
+ break;
+ }
+ } else {
+ // For other transforms, the value should be a Literal
+ if (
+ property.value?.type === 'NumericLiteral' ||
+ property.value?.type === 'StringLiteral'
+ ) {
+ const transformValue = property.value.value;
+ let normalizedValue;
+
+ if (typeof transformValue === 'number') {
+ // Don't add px suffix for scale transforms
+ if (
+ transformType === 'scale' ||
+ transformType === 'scaleX' ||
+ transformType === 'scaleY' ||
+ transformType === 'scaleZ'
+ ) {
+ normalizedValue = transformValue.toString();
+ } else {
+ normalizedValue = `${transformValue}px`;
+ }
+ } else if (typeof transformValue === 'string') {
+ normalizedValue = transformValue;
+ } else {
+ allValid = false;
+ break;
+ }
+ transformFunctions.push(`${transformType}(${normalizedValue})`);
+ } else {
+ allValid = false;
+ break;
+ }
+ }
+ } else {
+ allValid = false;
+ break;
+ }
+ } else {
+ allValid = false;
+ break;
+ }
+ }
+
+ if (allValid && transformFunctions.length > 0) {
+ styleDeclarationPath.node.value = t.stringLiteral(
+ transformFunctions.join(' ')
+ );
+ }
+ }
+
+ // Transform 'transformOrigin'
+ // From array of strings/numbers to space-separated string
+ if (
+ currentPropertyName === 'transformOrigin' &&
+ styleValue?.type === 'ArrayExpression'
+ ) {
+ const elements = styleValue.elements;
+ const stringValues = [];
+ let allValid = true;
+
+ for (const element of elements) {
+ if (element?.type === 'StringLiteral') {
+ stringValues.push(element.value);
+ } else if (element?.type === 'NumericLiteral') {
+ stringValues.push(`${element.value}px`);
+ } else {
+ allValid = false;
+ break;
+ }
+ }
+
+ if (allValid && stringValues.length > 0) {
+ styleDeclarationPath.node.value = t.stringLiteral(stringValues.join(' '));
+ }
+ }
+
+ // Transform 'writingDirection' (iOS)
+ if (
+ currentPropertyName === 'writingDirection' &&
+ styleValue?.type === 'StringLiteral'
+ ) {
+ styleProperty.name = 'direction';
+ }
+}
+
+function reactNativeMigratePlugin({ types: t }) {
+ const reactNativeElementMap = new Map([
+ ['View', 'html.div'],
+ ['Text', 'html.span'],
+ ['Image', 'html.img'],
+ ['ScrollView', 'html.div'],
+ ['TextInput', 'html.input'],
+ ['Switch', 'html.input'],
+ ['Button', 'html.button']
+ ]);
+
+ return {
+ name: 'react-native-migrate',
+ visitor: {
+ Program: {
+ enter(path, state) {
+ state.hasTransformedElements = false;
+ state.hasStyleSheetCreate = false;
+ state.hasReactNativeStyles = false;
+ state.usedReactNativeElements = new Set();
+ },
+ exit(path, state) {
+ if (!state.hasTransformedElements && !state.hasStyleSheetCreate) {
+ return;
+ }
+
+ let unusedReactNativeImportPath = null;
+ let hasStyleSheetImport = false;
+ let hasReactNativeStylesImport = false;
+
+ // Remove unused React Native imports and check for StyleSheet
+ path.traverse({
+ ImportDeclaration(importPath) {
+ if (importPath.node.source.value === 'react-native') {
+ const remainingSpecifiers = importPath.node.specifiers.filter(
+ (spec) => {
+ if (
+ spec.type === 'ImportSpecifier' &&
+ spec.imported?.name
+ ) {
+ if (spec.imported.name === 'StyleSheet') {
+ hasStyleSheetImport = true;
+ return false; // Remove StyleSheet import
+ }
+ return !state.usedReactNativeElements.has(
+ spec.imported.name
+ );
+ }
+ return true;
+ }
+ );
+
+ if (remainingSpecifiers.length === 0) {
+ unusedReactNativeImportPath = importPath;
+ } else if (
+ remainingSpecifiers.length !==
+ importPath.node.specifiers.length
+ ) {
+ importPath.node.specifiers = remainingSpecifiers;
+ }
+ }
+ // Check if reactNativeStyles is already imported
+ if (importPath.node.source.value === 'reactNativeStyles') {
+ hasReactNativeStylesImport = true;
+ }
+ }
+ });
+
+ const newImports = [];
+
+ // Add react-strict-dom import if we transformed elements
+ if (state.hasTransformedElements) {
+ const rsdImportDecl = createImport(t, ['html'], 'react-strict-dom');
+ newImports.push(rsdImportDecl);
+ }
+
+ // Add reactNativeStyles import if we added styles and it's not already imported
+ if (state.hasReactNativeStyles && !hasReactNativeStylesImport) {
+ const reactNativeStylesImportDecl = createDefaultImport(
+ t,
+ 'reactNativeStyles',
+ 'reactNativeStyles'
+ );
+ newImports.push(reactNativeStylesImportDecl);
+ }
+
+ // Add stylex import if we found StyleSheet usage
+ if (state.hasStyleSheetCreate && hasStyleSheetImport) {
+ const stylexImportDecl = createDefaultImport(t, 'stylex', 'stylex');
+ newImports.push(stylexImportDecl);
+ }
+
+ if (newImports.length > 0) {
+ if (unusedReactNativeImportPath != null) {
+ unusedReactNativeImportPath.remove();
+ }
+ path.unshiftContainer('body', newImports);
+ }
+ }
+ },
+ CallExpression(path, state) {
+ const node = path.node;
+ // Handle StyleSheet.create() calls
+ if (
+ node.callee?.type === 'MemberExpression' &&
+ node.callee.object?.type === 'Identifier' &&
+ node.callee.object.name === 'StyleSheet' &&
+ node.callee.property?.type === 'Identifier' &&
+ node.callee.property.name === 'create'
+ ) {
+ // Replace StyleSheet.create with stylex.create
+ node.callee.object.name = 'stylex';
+ state.hasStyleSheetCreate = true;
+
+ // Process the argument object to modify style rules
+ if (
+ node.arguments.length > 0 &&
+ node.arguments[0].type === 'ObjectExpression'
+ ) {
+ const styleRulesPath = path.get('arguments.0');
+
+ // Iterate through each style key (e.g., 'root', 'container', 'text', etc.)
+ for (const propertyPath of styleRulesPath.get('properties')) {
+ if (
+ propertyPath.node.type === 'ObjectProperty' &&
+ propertyPath.node.value?.type === 'ObjectExpression'
+ ) {
+ // Iterate through each style declaration within the style rule
+ for (const styleDeclarationPath of propertyPath.get(
+ 'value.properties'
+ )) {
+ if (styleDeclarationPath.node.type === 'ObjectProperty') {
+ transformStyleProperty(t, styleDeclarationPath);
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ JSXElement(path, state) {
+ const node = path.node;
+ const openingElement = node.openingElement;
+ const closingElement = node.closingElement;
+
+ if (openingElement.name.type !== 'JSXIdentifier') {
+ return;
+ }
+
+ const reactNativeElementName = openingElement.name.name;
+ let targetElement = reactNativeElementMap.get(reactNativeElementName);
+
+ if (targetElement == null) {
+ return;
+ }
+
+ const attributes = openingElement.attributes || [];
+
+ const newAttributes = [];
+
+ // Check if we need conditional rendering for TextInput with multiline expression
+ let needsConditionalTextInputRendering = false;
+ let textInputMultilineValue = null;
+ let textInputnumberOfLinesValue = null;
+
+ // Iterate over all React Native attributes to build up the array
+ // of React Strict DOM attributes. If we encounter literals, we can
+ // perform more complete transforms. Otherwise we generate expressions
+ // to resolve the correct value at runtime where possible.
+ for (const attr of attributes) {
+ if (attr.type === 'JSXSpreadAttribute') {
+ newAttributes.push(attr);
+ }
+ // Transform 'accessibilityElementsHidden' (iOS) to 'aria-hidden'
+ else if (isJSXAttributeNamed(attr, 'accessibilityElementsHidden')) {
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('aria-hidden'), attr.value)
+ );
+ }
+ // Transform 'accessibilityLabel' to 'aria-label'
+ else if (isJSXAttributeNamed(attr, 'accessibilityLabel')) {
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('aria-label'), attr.value)
+ );
+ }
+ // Transform 'accessibilityLabelledBy' (Android) to 'aria-labelledby'
+ else if (isJSXAttributeNamed(attr, 'accessibilityLabelledBy')) {
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('aria-labelledby'), attr.value)
+ );
+ }
+ // Transform 'accessibilityLiveRegion' (Android) to 'aria-live'
+ else if (isJSXAttributeNamed(attr, 'accessibilityLiveRegion')) {
+ if (
+ attr.value?.type === 'StringLiteral' &&
+ typeof attr.value.value === 'string'
+ ) {
+ const value = attr.value.value;
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier('aria-live'),
+ t.stringLiteral(value === 'none' ? 'off' : value)
+ )
+ );
+ } else if (
+ attr.value != null &&
+ attr.value.type === 'JSXExpressionContainer' &&
+ attr.value.expression != null &&
+ attr.value.expression.type !== 'JSXEmptyExpression'
+ ) {
+ const expression = attr.value.expression;
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier('aria-live'),
+ t.jsxExpressionContainer(
+ t.conditionalExpression(
+ t.binaryExpression(
+ '===',
+ expression,
+ t.stringLiteral('off')
+ ),
+ t.stringLiteral('none'),
+ expression
+ )
+ )
+ )
+ );
+ }
+ }
+ // Transform 'accessibilityRole' to 'role' (or element)
+ else if (isJSXAttributeNamed(attr, 'accessibilityRole')) {
+ if (
+ attr.value?.type === 'StringLiteral' &&
+ typeof attr.value.value === 'string'
+ ) {
+ const roleValue = attr.value.value;
+ // Special handling for "button" and "header" values.
+ // These roles can map directly to `html.*` elements.
+ if (roleValue === 'button' || roleValue === 'header') {
+ // Skip this attribute (don't add to newAttributes)
+ if (roleValue === 'button') {
+ targetElement = 'html.button';
+ } else if (roleValue === 'header') {
+ targetElement = 'html.header';
+ }
+ } else {
+ // For other values, transform to `role`.
+ // Map to ARIA role as needed.
+ let mappedValue;
+ switch (roleValue) {
+ case 'adjustable':
+ mappedValue = 'slider';
+ break;
+ case 'image':
+ mappedValue = 'img';
+ break;
+ case 'alert':
+ case 'checkbox':
+ case 'combobox':
+ case 'link':
+ case 'menu':
+ case 'menubar':
+ case 'menuitem':
+ case 'none':
+ case 'progressbar':
+ case 'radio':
+ case 'radiogroup':
+ case 'scrollbar':
+ case 'search':
+ case 'spinbutton':
+ case 'switch':
+ case 'tab':
+ case 'tablist':
+ case 'timer':
+ case 'toolbar':
+ mappedValue = roleValue;
+ break;
+ default:
+ // Not a valid role
+ mappedValue = null;
+ }
+ if (mappedValue != null) {
+ // Change prop name to 'role' and update value
+ const newValue =
+ mappedValue !== roleValue
+ ? t.stringLiteral(mappedValue)
+ : attr.value;
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('role'), newValue)
+ );
+ }
+ }
+ } else {
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('role'), attr.value)
+ );
+ }
+ }
+ // Transform 'accessibilityState' to ARIA props
+ else if (isJSXAttributeNamed(attr, 'accessibilityState')) {
+ if (
+ attr.value?.type === 'JSXExpressionContainer' &&
+ attr.value.expression?.type === 'ObjectExpression'
+ ) {
+ const objectExpr = attr.value.expression;
+ // Process each property in the accessibilityState object
+ for (const prop of objectExpr.properties) {
+ if (
+ prop.type === 'ObjectProperty' &&
+ prop.key?.type === 'Identifier'
+ ) {
+ const key = prop.key.name;
+ const value = prop.value;
+ // Create aria-* attribute for each property
+ const jsxValue =
+ value.type === 'StringLiteral'
+ ? t.stringLiteral(value.value)
+ : t.jsxExpressionContainer(value);
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier(`aria-${key}`), jsxValue)
+ );
+ }
+ }
+ } else if (
+ attr.value?.type === 'JSXExpressionContainer' &&
+ attr.value.expression != null &&
+ attr.value.expression.type !== 'JSXEmptyExpression'
+ ) {
+ const expression = attr.value.expression;
+ ['busy', 'checked', 'disabled', 'expanded', 'selected'].forEach(
+ (key) => {
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier(`aria-${key}`),
+ t.jsxExpressionContainer(
+ t.optionalMemberExpression(
+ expression,
+ t.identifier(key),
+ false,
+ true
+ )
+ )
+ )
+ );
+ }
+ );
+ }
+ }
+ // Transform 'accessibilityValue' to ARIA props
+ else if (isJSXAttributeNamed(attr, 'accessibilityValue')) {
+ if (
+ attr.value?.type === 'JSXExpressionContainer' &&
+ attr.value.expression?.type === 'ObjectExpression'
+ ) {
+ const objectExpr = attr.value.expression;
+ // Process each property in the accessibilityValue object
+ for (const prop of objectExpr.properties) {
+ if (
+ prop.type === 'ObjectProperty' &&
+ prop.key?.type === 'Identifier'
+ ) {
+ const key = prop.key.name;
+ const value = prop.value;
+ // Create aria-* attribute for each property
+ const jsxValue =
+ value.type === 'StringLiteral'
+ ? t.stringLiteral(value.value)
+ : t.jsxExpressionContainer(value);
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier(`aria-value${key}`),
+ jsxValue
+ )
+ );
+ }
+ }
+ } else if (
+ attr.value?.type === 'JSXExpressionContainer' &&
+ attr.value.expression != null &&
+ attr.value.expression.type !== 'JSXEmptyExpression'
+ ) {
+ const expression = attr.value.expression;
+ ['max', 'min', 'now', 'text'].forEach((key) => {
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier(`aria-value${key}`),
+ t.jsxExpressionContainer(
+ t.optionalMemberExpression(
+ expression,
+ t.identifier(key),
+ false,
+ true
+ )
+ )
+ )
+ );
+ });
+ }
+ }
+ // Transform 'accessibilityViewIsModal' (iOS) to 'aria-modal'
+ else if (isJSXAttributeNamed(attr, 'accessibilityViewIsModal')) {
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('aria-modal'), attr.value)
+ );
+ }
+ // Transform 'focusable' (Android) to 'tabIndex'
+ else if (isJSXAttributeNamed(attr, 'focusable')) {
+ const isFocusable = isJSXAttributeBoolean(attr, true);
+ const isFocusableFalse = isJSXAttributeBoolean(attr, false);
+ if (isFocusable) {
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier('tabIndex'),
+ t.stringLiteral('0')
+ )
+ );
+ } else if (
+ !isFocusableFalse &&
+ attr.value?.type === 'JSXExpressionContainer' &&
+ attr.value.expression != null &&
+ attr.value.expression.type !== 'JSXEmptyExpression'
+ ) {
+ const expression = attr.value.expression;
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier('tabIndex'),
+ t.jsxExpressionContainer(
+ t.conditionalExpression(
+ t.binaryExpression(
+ '===',
+ expression,
+ t.booleanLiteral(true)
+ ),
+ t.stringLiteral('0'),
+ t.identifier('undefined')
+ )
+ )
+ )
+ );
+ }
+ }
+ // Transform 'importantForAccessibility' (Android) to 'aria-hidden'
+ else if (isJSXAttributeNamed(attr, 'importantForAccessibility')) {
+ const noHideDescendants = 'no-hide-descendants';
+ if (
+ attr.value?.type === 'StringLiteral' &&
+ attr.value.value === noHideDescendants
+ ) {
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier('aria-hidden'),
+ t.jsxExpressionContainer(t.booleanLiteral(true))
+ )
+ );
+ } else if (
+ attr.value?.type === 'JSXExpressionContainer' &&
+ attr.value.expression != null &&
+ attr.value.expression.type !== 'JSXEmptyExpression'
+ ) {
+ const expression = attr.value.expression;
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier('aria-hidden'),
+ t.jsxExpressionContainer(
+ t.conditionalExpression(
+ t.binaryExpression(
+ '===',
+ expression,
+ t.stringLiteral(noHideDescendants)
+ ),
+ t.booleanLiteral(true),
+ t.identifier('undefined')
+ )
+ )
+ )
+ );
+ }
+ }
+ // Transform 'multiline' (TextInput)
+ else if (
+ reactNativeElementName === 'TextInput' &&
+ isJSXAttributeNamed(attr, 'multiline')
+ ) {
+ if (isJSXAttributeBoolean(attr, true)) {
+ // Change the targetElement and ignore the attribute
+ targetElement = 'html.textarea';
+ } else if (
+ !isJSXAttributeBoolean(attr, false) &&
+ attr.value?.type === 'JSXExpressionContainer' &&
+ attr.value.expression != null &&
+ attr.value.expression.type !== 'JSXEmptyExpression'
+ ) {
+ // Mark for conditional rendering
+ needsConditionalTextInputRendering = true;
+ textInputMultilineValue = attr.value.expression;
+ }
+ }
+ // Transform 'nativeID' to 'id'
+ else if (isJSXAttributeNamed(attr, 'nativeID')) {
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('id'), attr.value)
+ );
+ }
+ // Transform 'numberOfLines'
+ else if (isJSXAttributeNamed(attr, 'numberOfLines')) {
+ // ...to "rows"
+ if (reactNativeElementName === 'TextInput') {
+ if (needsConditionalTextInputRendering) {
+ // Store for conditional rendering
+ textInputnumberOfLinesValue = attr.value;
+ } else {
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('rows'), attr.value)
+ );
+ }
+ }
+ }
+ // Transform 'source' to 'src' (Image)
+ else if (
+ reactNativeElementName === 'Image' &&
+ isJSXAttributeNamed(attr, 'source') &&
+ attr.value?.type === 'JSXExpressionContainer' &&
+ attr.value.expression?.type === 'ObjectExpression'
+ ) {
+ const objectExpr = attr.value.expression;
+ const uriProperty = objectExpr.properties.find(
+ (prop) =>
+ prop.type === 'ObjectProperty' &&
+ prop.key?.type === 'Identifier' &&
+ prop.key.name === 'uri'
+ );
+ if (
+ uriProperty?.type === 'ObjectProperty' &&
+ uriProperty.value?.type === 'StringLiteral'
+ ) {
+ const uriValue = uriProperty.value.value;
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier('src'),
+ t.stringLiteral(uriValue)
+ )
+ );
+ }
+ }
+ // Transform 'style' attribute
+ else if (isJSXAttributeNamed(attr, 'style')) {
+ newAttributes.push(attr);
+ }
+ // Transform 'testID' to 'data-testid'
+ else if (isJSXAttributeNamed(attr, 'testID')) {
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('data-testid'), attr.value)
+ );
+ }
+ // Forward all other attributes for now
+ else {
+ newAttributes.push(attr);
+ }
+ }
+
+ // Element-specific RSD attribute additions
+ // Modify the style attribute
+ if (
+ reactNativeElementName === 'ScrollView' ||
+ reactNativeElementName === 'Text' ||
+ reactNativeElementName === 'TextInput' ||
+ reactNativeElementName === 'View'
+ ) {
+ state.hasReactNativeStyles = true;
+ let hasStyle = false;
+ const styleName = {
+ ScrollView: 'view',
+ Text: 'text',
+ TextInput: 'textInput',
+ View: 'view'
+ };
+ for (let i = 0; i < newAttributes.length; i++) {
+ const attr = newAttributes[i];
+ if (
+ typeof attr === 'object' &&
+ attr.type === 'JSXAttribute' &&
+ attr.name?.type === 'JSXIdentifier' &&
+ attr.name.name === 'style'
+ ) {
+ if (attr.value != null) {
+ hasStyle = true;
+ // There's already a style prop, prepend reactNativeStyles.view
+ let existingExpression;
+ if (
+ attr.value.type === 'JSXExpressionContainer' &&
+ attr.value.expression != null &&
+ attr.value.expression.type !== 'JSXEmptyExpression'
+ ) {
+ existingExpression = attr.value.expression;
+ } else if (attr.value.type === 'StringLiteral') {
+ existingExpression = attr.value;
+ } else {
+ // Skip transformation if we can't extract a valid expression
+ continue;
+ }
+ // Create a JSX expression container with an array of styles
+ const memberExpr = t.memberExpression(
+ t.identifier('reactNativeStyles'),
+ t.identifier(styleName[reactNativeElementName])
+ );
+ const arrayExpr = t.arrayExpression([
+ memberExpr,
+ existingExpression
+ ]);
+ // Update the RSD style attribute with our changes
+ newAttributes[i] = t.jsxAttribute(
+ t.jsxIdentifier('style'),
+ t.jsxExpressionContainer(arrayExpr)
+ );
+ }
+ }
+ }
+ if (!hasStyle) {
+ newAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier('style'),
+ t.jsxExpressionContainer(
+ t.memberExpression(
+ t.identifier('reactNativeStyles'),
+ t.identifier(styleName[reactNativeElementName])
+ )
+ )
+ )
+ );
+ }
+ }
+ if (reactNativeElementName === 'Switch') {
+ // Add type="checkbox" if not present
+ newAttributes.push(
+ t.jsxAttribute(t.jsxIdentifier('type'), t.stringLiteral('checkbox'))
+ );
+ }
+
+ // Replace the entire attributes array
+ openingElement.attributes = newAttributes;
+
+ // Other transforms once attributes are updated
+ if (reactNativeElementName === 'Button') {
+ // Move title prop to children
+ const titleAttr = openingElement.attributes.find(
+ (attr) =>
+ attr.type === 'JSXAttribute' &&
+ attr.name?.type === 'JSXIdentifier' &&
+ attr.name.name === 'title'
+ );
+ if (
+ titleAttr?.type === 'JSXAttribute' &&
+ titleAttr.value?.type === 'StringLiteral'
+ ) {
+ const textValue = titleAttr.value.value;
+ // Remove title attribute and ensure element is not self-closing
+ openingElement.attributes = openingElement.attributes.filter(
+ (attr) => attr !== titleAttr
+ );
+ openingElement.selfClosing = false;
+
+ // Ensure there's a closing element
+ if (!closingElement && targetElement != null) {
+ node.closingElement = t.jsxClosingElement(
+ t.jsxIdentifier(targetElement)
+ );
+ }
+
+ // Add title as text content - create a simple text node
+ node.children = [t.jsxText(textValue)];
+ }
+ }
+
+ // Handle conditional rendering for TextInput with multiline expression
+ if (
+ needsConditionalTextInputRendering &&
+ textInputMultilineValue != null
+ ) {
+ // Create textarea attributes (add rows if numberOfLines exists)
+ const textareaAttributes = [...newAttributes];
+ if (textInputnumberOfLinesValue != null) {
+ textareaAttributes.push(
+ t.jsxAttribute(
+ t.jsxIdentifier('rows'),
+ textInputnumberOfLinesValue
+ )
+ );
+ }
+
+ // Create input attributes (same as newAttributes)
+ const inputAttributes = [...newAttributes];
+
+ // Create the conditional expression
+ const conditionalExpr = t.conditionalExpression(
+ t.binaryExpression(
+ '===',
+ textInputMultilineValue,
+ t.booleanLiteral(true)
+ ),
+ t.jsxElement(
+ t.jsxOpeningElement(
+ t.jsxIdentifier('html.textarea'),
+ textareaAttributes,
+ true
+ ),
+ null,
+ [],
+ true
+ ),
+ t.jsxElement(
+ t.jsxOpeningElement(
+ t.jsxIdentifier('html.input'),
+ inputAttributes,
+ true
+ ),
+ null,
+ [],
+ true
+ )
+ );
+
+ // Replace the entire JSX element with the conditional expression wrapped in braces
+ path.replaceWith(t.jsxExpressionContainer(conditionalExpr));
+ }
+ // Transform element names
+ else if (targetElement != null) {
+ openingElement.name.name = targetElement;
+ if (closingElement?.name?.type === 'JSXIdentifier') {
+ closingElement.name.name = targetElement;
+ }
+ }
+
+ state.hasTransformedElements = true;
+ state.usedReactNativeElements.add(reactNativeElementName);
+ }
+ }
+ };
+}
+
+function createImport(t, importNames, moduleName, importKind = 'value') {
+ return t.importDeclaration(
+ importNames.map((importName) =>
+ t.importSpecifier(t.identifier(importName), t.identifier(importName))
+ ),
+ t.stringLiteral(moduleName)
+ );
+}
+
+function createDefaultImport(t, importName, moduleName) {
+ return t.importDeclaration(
+ [t.importDefaultSpecifier(t.identifier(importName))],
+ t.stringLiteral(moduleName)
+ );
+}
+
+function isJSXAttributeNamed(attribute, attributeName) {
+ return (
+ attribute.type === 'JSXAttribute' &&
+ attribute.name?.type === 'JSXIdentifier' &&
+ attribute.name.name === attributeName
+ );
+}
+
+function isJSXAttributeBoolean(attribute, bool) {
+ const attributeBooleanValue =
+ attribute.value?.type === 'JSXExpressionContainer' &&
+ attribute.value.expression?.type === 'BooleanLiteral' &&
+ attribute.value.expression.value;
+
+ if (
+ // is the attribute boolean?
+ (bool == null && attributeBooleanValue) ||
+ // is the attribute implicit boolean true
+ (bool === true && attribute.value == null)
+ ) {
+ return true;
+ } else {
+ return (
+ // does the attribute boolean match the arg
+ attribute.value?.type === 'JSXExpressionContainer' &&
+ attribute.value.expression?.type === 'BooleanLiteral' &&
+ attribute.value.expression.value === bool
+ );
+ }
+}
+
+module.exports = reactNativeMigratePlugin;
diff --git a/packages/react-strict-dom/tests/__snapshots__/babel-migrate-test.node.js.snap b/packages/react-strict-dom/tests/__snapshots__/babel-migrate-test.node.js.snap
new file mode 100644
index 00000000..87873772
--- /dev/null
+++ b/packages/react-strict-dom/tests/__snapshots__/babel-migrate-test.node.js.snap
@@ -0,0 +1,440 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`react-strict-dom-migrate "accessibilityElementsHidden" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "accessibilityLabel" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "accessibilityLabelledBy" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "accessibilityLiveRegion" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "accessibilityRole" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "accessibilityState" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "accessibilityValue" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+
+
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "accessibilityViewIsModal" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "focusable" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "importantForAccessibility" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "nativeID" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate "testID" prop 1`] = `
+"import { html } from "react-strict-dom";
+import reactNativeStyles from "reactNativeStyles";
+import * as React from 'react';
+export default function ReactNativeElementTransform() {
+ return <>
+
+
+ >;
+}"
+`;
+
+exports[`react-strict-dom-migrate