diff --git a/src/__tests__/babel/react-native-web.test.ts b/src/__tests__/babel/react-native-web.test.ts index 612d529..5d54a4d 100644 --- a/src/__tests__/babel/react-native-web.test.ts +++ b/src/__tests__/babel/react-native-web.test.ts @@ -14,27 +14,81 @@ describe("react-native-web", () => { plugins: ["@babel/plugin-syntax-jsx"], }, tests: appendTitles([ + /* import tests */ { code: `import 'react-native-web';`, output: `import "react-native-css/components";`, }, + { + code: `import ReactNativeWeb from 'react-native-web';`, + output: `import ReactNativeWeb from "react-native-css/components";`, + }, { code: `import { View } from 'react-native-web';`, output: `import { View } from "react-native-css/components/View";`, }, { - code: `import View from 'react-native-web/dist/commonjs/exports/View';`, + code: `import View from 'react-native-web/dist/cjs/View';`, output: `import { View } from "react-native-css/components/View";`, }, { - code: `import View from 'react-native-web/dist/module/exports/View';`, + code: `import View from 'react-native-web/dist/modules/View';`, output: `import { View } from "react-native-css/components/View";`, }, { code: `import View from '../View';`, output: `import { View } from "react-native-css/components/View";`, babelOptions: { - filename: "react-native-web/dist/module/exports/ScrollView/index.js", + filename: "react-native-web/dist/modules/ScrollView/index.js", + }, + }, + + /* require() tests */ + { + code: `const { Text } = require('react-native-web');`, + output: `const { Text } = require("react-native-css/components/Text");`, + }, + { + code: `const Text = require('react-native-web/dist/modules/Text');`, + output: `const { Text } = require("react-native-css/components/Text");`, + }, + { + code: `const _Text = require('react-native-web/dist/modules/Text');`, + output: `const { Text: _Text } = require("react-native-css/components/Text");`, + }, + { + code: `const Text = require('react-native-web/dist/cjs/Text');`, + output: `const { Text } = require("react-native-css/components/Text");`, + }, + { + code: `const _Text = require('react-native-web/dist/cjs/Text');`, + output: `const { Text: _Text } = require("react-native-css/components/Text");`, + }, + { + code: `const Text = require('react-native-web/dist/exports/Text');`, + output: `const { Text } = require("react-native-css/components/Text");`, + }, + { + code: `const _Text = require('react-native-web/dist/exports/Text');`, + output: `const { Text: _Text } = require("react-native-css/components/Text");`, + }, + { + code: `const _Text = _interopRequireDefault(require('react-native-web/dist/Text'));`, + output: `const { Text: _Text } = require("react-native-css/components/Text");`, + }, + { + code: `const _Text = _interopRequireDefault(require('react-native-web/dist/modules/Text'));`, + output: `const { Text: _Text } = require("react-native-css/components/Text");`, + }, + { + code: `const _Text = _interopRequireDefault(require('react-native-web/dist/cjs/Text'));`, + output: `const { Text: _Text } = require("react-native-css/components/Text");`, + }, + { + code: `const View = _interopRequireDefault(require('../View'));`, + output: `const { View } = require("react-native-css/components/View");`, + babelOptions: { + filename: "react-native-web/dist/modules/ScrollView/index.js", }, }, ]), diff --git a/src/__tests__/babel/smoke.test.ts b/src/__tests__/babel/smoke.test.ts index 2913491..ad5c202 100644 --- a/src/__tests__/babel/smoke.test.ts +++ b/src/__tests__/babel/smoke.test.ts @@ -15,28 +15,10 @@ describe("plugin smoke tests", () => { filename: "/someFile.js", }, tests: appendTitles([ - { - code: `import 'react-native';`, - output: `import "react-native-css/components";`, - }, { code: `import '../global.css';`, output: `import "../global.css";`, }, - { - code: `import { View } from 'react-native';`, - output: `import { View } from "react-native";`, - babelOptions: { - filename: "/node_modules/react-native-css/components/View.js", - }, - }, - { - code: `import { View } from 'react-native';`, - output: `import { View } from "react-native";`, - babelOptions: { - filename: "react-native-css/src/components/View.js", - }, - }, { code: `import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry'`, output: `import * as NativeComponentRegistry from "../../NativeComponent/NativeComponentRegistry";`, diff --git a/src/babel/allowedModules.ts b/src/babel/allowedModules.ts new file mode 100644 index 0000000..212c2c7 --- /dev/null +++ b/src/babel/allowedModules.ts @@ -0,0 +1,24 @@ +import { readdirSync } from "fs"; +import { join, parse } from "path"; + +function getFilesWithoutExtension(dirPath: string) { + // Read all files and directories inside dirPath synchronously + const entries = readdirSync(dirPath, { withFileTypes: true }); + + // Filter only files (ignore directories) + const files = entries.filter( + (entry) => + entry.isFile() && + /\.[jt]sx?$/.exec(entry.name) && + !/index\.[jt]sx?$/.exec(entry.name), + ); + + // For each file, get the filename without extension + const filesWithoutExt = files.map((file) => parse(file.name).name); + + return filesWithoutExt; +} + +export const allowedModules = new Set( + getFilesWithoutExtension(join(__dirname, "../components")), +); diff --git a/src/babel/helpers.ts b/src/babel/helpers.ts index e9ea674..c66f630 100644 --- a/src/babel/helpers.ts +++ b/src/babel/helpers.ts @@ -1,11 +1,6 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { readdirSync } from "fs"; -import { dirname, join, normalize, parse, resolve, sep } from "path"; -import { type NodePath } from "@babel/traverse"; +import tBabelTypes, { type CallExpression } from "@babel/types"; -import t from "@babel/types"; - -export type BabelTypes = typeof t; +export type BabelTypes = typeof tBabelTypes; export interface PluginOpts { target?: string; @@ -18,165 +13,28 @@ export interface PluginState { filename: string; } -export const allowedModules = new Set( - getFilesWithoutExtension(join(__dirname, "../components")), -); - -function getFilesWithoutExtension(dirPath: string) { - // Read all files and directories inside dirPath synchronously - const entries = readdirSync(dirPath, { withFileTypes: true }); - - // Filter only files (ignore directories) - const files = entries.filter( - (entry) => - entry.isFile() && - /\.[jt]sx?$/.exec(entry.name) && - !/index\.[jt]sx?$/.exec(entry.name), - ); - - // For each file, get the filename without extension - const filesWithoutExt = files.map((file) => parse(file.name).name); - - return filesWithoutExt; -} - -export function isInsideModule(filename: string, module: string): boolean { - const normalized = normalize(filename); - - // Match exact module name - if (normalized === module) { - return true; - } - - // Absolute posix paths - if (normalized.startsWith(`${module}/`)) { - return true; +export function getInteropRequireDefaultSource( + init: CallExpression, + t: BabelTypes, +) { + if (!t.isIdentifier(init.callee, { name: "_interopRequireDefault" })) { + return; } - // Check for our local development structure - if (normalized.includes(`${sep}${module}${sep}src${sep}`)) { - // Ignore the test files - return !normalized.includes("__tests__"); - } + const interopArg = init.arguments.at(0); - // Match classic node_modules - if (normalized.includes(`${sep}node_modules${sep}${module}${sep}`)) { - return true; - } - - // Match Yarn PnP .zip-style paths (e.g., .zip/node_modules/${module}/) if ( - normalized.includes(`${sep}.zip${sep}node_modules${sep}${module}${sep}`) + !t.isCallExpression(interopArg) || + !t.isIdentifier(interopArg.callee, { name: "require" }) ) { - return true; - } - - // Match Yarn .yarn/cache/${module}/* - if (normalized.includes(`${sep}.yarn${sep}cache${sep}${module}${sep}`)) { - return true; + return; } - return false; -} - -export function isPackageImport( - path: NodePath, -) { - const source = path.node.source?.value; - if (!source) { - return false; - } - - return source === "react-native" || source === "react-native-web"; -} - -export function shouldTransformImport( - path: NodePath, - filename: string, -) { - let source = path.node.source?.value; - - if (!source) { - return false; - } - - if (source.startsWith(".")) { - source = resolve(dirname(filename), source); - } - - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return isReactNativeSource(source) || isReactNativeWebSource(source); -} - -function isReactNativeSource(source: string) { - if (source === "react-native") return true; - - const components = source.split( - `react-native${sep}Libraries${sep}Components${sep}`, - ); - - if (components.length > 1) { - const component = components[1]?.split(sep)[0]; - return component && allowedModules.has(component); - } - - return false; -} - -function isReactNativeWebSource(source: string) { - if (source === "react-native-web") return true; - - let components = source.split( - `react-native-web${sep}dist${sep}commonjs${sep}exports${sep}`, - ); - if (components.length > 1) { - const component = components[1]?.split(sep)[0]; - return component && allowedModules.has(component); - } - - components = source.split( - `react-native-web${sep}dist${sep}module${sep}exports${sep}`, - ); - if (components.length > 1) { - const component = components[1]?.split(sep)[0]; - return component && allowedModules.has(component); - } - - return false; -} - -export function shouldTransformRequire( - t: BabelTypes, - node: t.VariableDeclaration, - basePath: string, -) { - const { declarations } = node; - - const declaration = declarations[0]; - - if (declarations.length > 1 || !declaration) { - return false; - } - const { id, init } = declaration; - - let source = - (t.isObjectPattern(id) || t.isIdentifier(id)) && - t.isCallExpression(init) && - t.isIdentifier(init.callee) && - init.callee.name === "require" && - init.arguments.length === 1 && - "value" in init.arguments[0]! && - typeof init.arguments[0].value === "string" && - init.arguments[0].value; - - if (!source) { - return false; - } + const requireArg = interopArg.arguments.at(0); - if (source.startsWith(".")) { - source = resolve(basePath, source); + if (!t.isStringLiteral(requireArg)) { + return; } - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return isReactNativeSource(source) || isReactNativeWebSource(source); + return requireArg.value; } diff --git a/src/babel/import-plugin.ts b/src/babel/import-plugin.ts index f73aebe..9e70df3 100644 --- a/src/babel/import-plugin.ts +++ b/src/babel/import-plugin.ts @@ -1,14 +1,22 @@ import { type PluginObj } from "@babel/core"; +import { resolve } from "path"; import { type BabelTypes, type PluginState, - allowedModules, - shouldTransformImport, - isInsideModule, - isPackageImport, - shouldTransformRequire, + getInteropRequireDefaultSource, } from "./helpers"; +import { + handleReactNativeWebIdentifierRequire, + handleReactNativeWebImport, + handleReactNativeWebObjectPatternRequire, +} from "./react-native-web"; +import type { Statement } from "@babel/types"; +import { + handleReactNativeIdentifierRequire, + handleReactNativeImport, + handleReactNativeObjectPatternRequire, +} from "./react-native"; export default function ({ types: t, @@ -17,166 +25,150 @@ export default function ({ }): PluginObj { const processed = new WeakSet(); - function importDeclaration(...args: Parameters) { - const declaration = t.importDeclaration(...args); - processed.add(declaration); - return declaration; - } + const thisModuleDist = resolve(__dirname, "../../../dist"); + const thisModuleSrc = resolve(__dirname, "../../../src"); - function requireDeclaration( - ...args: Parameters - ) { - const declaration = t.variableDeclaration(...args); - processed.add(declaration); - return declaration; + function isFromThisModule(filename: string): boolean { + return ( + filename.startsWith(thisModuleDist) || filename.startsWith(thisModuleSrc) + ); } return { name: "Rewrite react-native to react-native-css", visitor: { ImportDeclaration(path, state): void { - if (processed.has(path) || processed.has(path.node)) { + if ( + processed.has(path) || + processed.has(path.node) || + isFromThisModule(state.filename) + ) { return; } - const { specifiers, source } = path.node; + const statements = + handleReactNativeImport(path.node, t, state.filename) ?? + handleReactNativeWebImport(path.node, t, state.filename); + + if (!statements) { + return; + } + + for (const statement of statements) { + processed.add(statement); + } + path.replaceWithMultiple(statements); + }, + VariableDeclaration(path, state): void { if ( - isInsideModule(state.filename, "react-native-css") || - !shouldTransformImport(path, state.filename) + processed.has(path) || + processed.has(path.node) || + isFromThisModule(state.filename) ) { return; } - if (specifiers.length === 0) { - path.replaceWith( - importDeclaration( - [], - t.stringLiteral("react-native-css/components"), - ), - ); - path.scope.registerDeclaration(path); + const firstDeclaration = path.node.declarations.at(0); + + // We only handle single variable declarations for now. + if (path.node.declarations.length > 1) { return; } - const imports = specifiers.map((specifier) => { - if (t.isImportDefaultSpecifier(specifier)) { - const name = specifier.local.name; - - if (isPackageImport(path)) { - return importDeclaration( - [t.importDefaultSpecifier(specifier.local)], - t.stringLiteral("react-native-css/components"), - ); - } else if (allowedModules.has(name)) { - return importDeclaration( - [t.importSpecifier(specifier.local, specifier.local)], - t.stringLiteral(`react-native-css/components/${name}`), - ); - } else { - return importDeclaration([specifier], source); - } - } else if (t.isImportNamespaceSpecifier(specifier)) { - return importDeclaration( - [specifier], - t.stringLiteral("react-native-css/components"), - ); - } else { - const localName = t.isStringLiteral(specifier.imported) - ? specifier.imported.value - : specifier.imported.name; - - return allowedModules.has(localName) - ? importDeclaration( - [t.importSpecifier(specifier.local, specifier.imported)], - t.stringLiteral(`react-native-css/components/${localName}`), - ) - : importDeclaration([specifier], source); - } - }); + // Skip declarations that are not initialized. eg `let x;` + if (!firstDeclaration?.init) { + return; + } - path.replaceInline(imports).map((importPath) => { - path.scope.registerDeclaration(importPath); - }); - }, - VariableDeclaration(path, state): void { - if (processed.has(path) || processed.has(path.node)) { + const { id, init } = firstDeclaration; + + // We only handle `const = ()` + if (!t.isCallExpression(init)) { return; } - if ( - isInsideModule(state.filename, "react-native-css") || - !shouldTransformRequire(t, path.node, state.filename) - ) { + // We only handle `const id = () OR const { } = ()` + if (!(t.isIdentifier(id) || t.isObjectPattern(id))) { return; } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { id, init } = path.node.declarations[0]!; - - if (!t.isObjectPattern(id) || !init) { - path.replaceWith( - requireDeclaration(path.node.kind, [ - t.variableDeclarator( - id, - t.callExpression(t.identifier("require"), [ - t.stringLiteral(`react-native-css/components`), - ]), - ), - ]), - ); + const initArg = init.arguments.at(0); + + if (!initArg) { return; } - const imports = id.properties.map((identifier) => { - if (t.isRestElement(identifier)) { - return requireDeclaration(path.node.kind, [ - t.variableDeclarator(identifier, init), - ]); - } + let statements: Statement[] | undefined; - if (!t.isIdentifier(identifier.key)) { - return requireDeclaration(path.node.kind, [ - t.variableDeclarator(t.objectPattern([identifier]), init), - ]); + // `const = require();` + if ( + t.isIdentifier(id) && + t.isIdentifier(init.callee, { name: "require" }) && + t.isStringLiteral(initArg) + ) { + statements = + handleReactNativeIdentifierRequire( + path, + t, + id.name, + initArg.value, + state.filename, + ) ?? + handleReactNativeWebIdentifierRequire( + path, + t, + id.name, + initArg.value, + state.filename, + ); + } else if ( + t.isObjectPattern(id) && + t.isIdentifier(init.callee, { name: "require" }) && + t.isStringLiteral(initArg) + ) { + statements = + handleReactNativeObjectPatternRequire( + path, + t, + id, + initArg.value, + state.filename, + ) ?? + handleReactNativeWebObjectPatternRequire( + path, + t, + id, + initArg.value, + state.filename, + ); + } else if ( + t.isIdentifier(id) && + t.isIdentifier(init.callee, { name: "_interopRequireDefault" }) + ) { + // `const = _interopRequireDefault(require());` + const source = getInteropRequireDefaultSource(init, t); + if (!source) { + return; } + statements = handleReactNativeWebIdentifierRequire( + path, + t, + id.name, + source, + state.filename, + ); + } - const name = identifier.key.name; - - if (!allowedModules.has(name)) { - return requireDeclaration(path.node.kind, [ - t.variableDeclarator( - t.objectPattern([ - t.objectProperty( - t.identifier(name), - t.identifier(name), - false, - true, - ), - ]), - init, - ), - ]); - } + if (!statements) { + return; + } + + for (const statement of statements) { + processed.add(statement); + } - return requireDeclaration(path.node.kind, [ - t.variableDeclarator( - t.objectPattern([ - t.objectProperty( - t.identifier(name), - t.identifier(name), - false, - true, - ), - ]), - t.callExpression(t.identifier("require"), [ - t.stringLiteral(`react-native-css/components/${name}`), - ]), - ), - ]); - }); - - path.replaceWithMultiple(imports); + path.replaceWithMultiple(statements); }, }, }; diff --git a/src/babel/react-native-web.ts b/src/babel/react-native-web.ts new file mode 100644 index 0000000..0029d83 --- /dev/null +++ b/src/babel/react-native-web.ts @@ -0,0 +1,225 @@ +import { type NodePath } from "@babel/traverse"; + +import tBabelTypes, { + type ImportDeclaration, + type ObjectPattern, + type Statement, + type VariableDeclaration, +} from "@babel/types"; +import { allowedModules } from "./allowedModules"; +import { resolve } from "path"; + +type BabelTypes = typeof tBabelTypes; + +function parseReactNativeWebSource(source: string, filename: string) { + if (source.startsWith(".")) { + source = resolve(filename, source); + + const internalPath = source.split("react-native-web/dist")[1]; + if (!internalPath) { + return; + } + + // Strip the absolute system filepath + source = `react-native-web/${internalPath}`; + } + + if (source === "react-native-web") { + return { source: "react-native-css/components" }; + } else if (source.startsWith("react-native-web/")) { + const name = source.split("/").at(-1); + if (!name || !allowedModules.has(name)) { + return; + } + + return { + source: `react-native-css/components/${name}`, + name, + }; + } + + return; +} + +export function handleReactNativeWebImport( + declaration: ImportDeclaration, + t: BabelTypes, + filename: string, +): Statement[] | undefined { + const { specifiers, source } = declaration; + + const rnwSource = parseReactNativeWebSource(source.value, filename); + if (!rnwSource) { + return; + } + + if (specifiers.length === 0) { + return [ + t.importDeclaration([], t.stringLiteral("react-native-css/components")), + ]; + } + + const statements: Statement[] = []; + + for (const specifier of specifiers) { + if (t.isImportDefaultSpecifier(specifier)) { + const { source: newSource, name } = rnwSource; + + if (!name) { + statements.push( + t.importDeclaration( + [t.importDefaultSpecifier(specifier.local)], + t.stringLiteral(newSource), + ), + ); + } else { + statements.push( + t.importDeclaration( + [t.importSpecifier(specifier.local, specifier.local)], + t.stringLiteral(`react-native-css/components/${name}`), + ), + ); + } + } else if (t.isImportNamespaceSpecifier(specifier)) { + statements.push( + t.importDeclaration( + [specifier], + t.stringLiteral("react-native-css/components"), + ), + ); + } else { + const localName = t.isStringLiteral(specifier.imported) + ? specifier.imported.value + : specifier.imported.name; + + if (!allowedModules.has(localName)) { + statements.push(t.importDeclaration([specifier], source)); + } else { + statements.push( + t.importDeclaration( + [t.importSpecifier(specifier.local, specifier.imported)], + t.stringLiteral(`react-native-css/components/${localName}`), + ), + ); + } + } + } + + return statements; +} + +export function handleReactNativeWebIdentifierRequire( + path: NodePath, + t: BabelTypes, + id: string, + source: string, + filename: string, +) { + const parsedSource = parseReactNativeWebSource(source, filename); + + if (!parsedSource) { + return; + } + + const { source: newSource, name } = parsedSource; + + if (name) { + return [ + t.variableDeclaration(path.node.kind, [ + t.variableDeclarator( + t.objectPattern([ + name === id + ? t.objectProperty( + t.identifier(name), + t.identifier(id), + false, + true, + ) + : t.objectProperty(t.identifier(name), t.identifier(id)), + ]), + t.callExpression(t.identifier("require"), [ + t.stringLiteral(newSource), + ]), + ), + ]), + ]; + } else { + return [ + t.variableDeclaration(path.node.kind, [ + t.variableDeclarator( + t.identifier(id), + t.callExpression(t.identifier("require"), [ + t.stringLiteral(newSource), + ]), + ), + ]), + ]; + } +} + +export function handleReactNativeWebObjectPatternRequire( + path: NodePath, + t: BabelTypes, + id: ObjectPattern, + source: string, + filename: string, +) { + const parsedSource = parseReactNativeWebSource(source, filename); + + // Don't handle `const { Text } = require('react-native-web/dist/cjs/Text');` - we only handle package exports + if (!parsedSource || parsedSource.name) { + return; + } + + const statements: Statement[] = []; + + for (const identifier of id.properties) { + if (t.isRestElement(identifier)) { + // Bail out on `const { ...rest } = require('react-native-web');` + // We need to exit as we do not handle `const { Text, ...rest } = require('react-native-web');` + return; + } else if ( + !(t.isIdentifier(identifier.value) && t.isIdentifier(identifier.key)) + ) { + // Bail out on anything that isn't `const { : } = require('react-native-web');` + return; + } else { + const name = identifier.key.name; + + if (!allowedModules.has(name)) { + statements.push( + t.variableDeclaration(path.node.kind, [ + t.variableDeclarator( + t.objectPattern([identifier]), + t.callExpression(t.identifier("require"), [ + t.stringLiteral(source), + ]), + ), + ]), + ); + } else { + const newSourceWithName = `react-native-css/components/${name}`; + + statements.push( + t.variableDeclaration(path.node.kind, [ + t.variableDeclarator( + t.objectPattern([ + t.objectProperty( + t.identifier(name), + t.identifier(identifier.value.name), + false, + true, + ), + ]), + t.callExpression(t.identifier("require"), [ + t.stringLiteral(newSourceWithName), + ]), + ), + ]), + ); + } + } + } + + return statements; +} diff --git a/src/babel/react-native.ts b/src/babel/react-native.ts new file mode 100644 index 0000000..8313935 --- /dev/null +++ b/src/babel/react-native.ts @@ -0,0 +1,225 @@ +import { type NodePath } from "@babel/traverse"; + +import tBabelTypes, { + type ImportDeclaration, + type ObjectPattern, + type Statement, + type VariableDeclaration, +} from "@babel/types"; +import { allowedModules } from "./allowedModules"; +import { dirname, resolve } from "path"; + +type BabelTypes = typeof tBabelTypes; + +function parseReactNativeSource(source: string, filename: string) { + if (source.startsWith(".")) { + source = resolve(dirname(filename), source); + + const internalPath = source.split("react-native/Libraries/Components/")[1]; + if (!internalPath) { + return; + } + + // Strip the absolute system filepath + source = `react-native/Libraries/Components/${internalPath}`; + } + + if (source === "react-native") { + return { source: "react-native-css/components" }; + } else if (source.startsWith("react-native/")) { + const name = source.split("/").at(-1); + if (!name || !allowedModules.has(name)) { + return; + } + + return { + source: `react-native-css/components/${name}`, + name, + }; + } + + return; +} + +export function handleReactNativeImport( + declaration: ImportDeclaration, + t: BabelTypes, + filename: string, +): Statement[] | undefined { + const { specifiers, source } = declaration; + + const rnwSource = parseReactNativeSource(source.value, filename); + if (!rnwSource) { + return; + } + + if (specifiers.length === 0) { + return [ + t.importDeclaration([], t.stringLiteral("react-native-css/components")), + ]; + } + + const statements: Statement[] = []; + + for (const specifier of specifiers) { + if (t.isImportDefaultSpecifier(specifier)) { + const { source: newSource, name } = rnwSource; + + if (!name) { + statements.push( + t.importDeclaration( + [t.importDefaultSpecifier(specifier.local)], + t.stringLiteral(newSource), + ), + ); + } else { + statements.push( + t.importDeclaration( + [t.importSpecifier(specifier.local, specifier.local)], + t.stringLiteral(`react-native-css/components/${name}`), + ), + ); + } + } else if (t.isImportNamespaceSpecifier(specifier)) { + statements.push( + t.importDeclaration( + [specifier], + t.stringLiteral("react-native-css/components"), + ), + ); + } else { + const localName = t.isStringLiteral(specifier.imported) + ? specifier.imported.value + : specifier.imported.name; + + if (!allowedModules.has(localName)) { + statements.push(t.importDeclaration([specifier], source)); + } else { + statements.push( + t.importDeclaration( + [t.importSpecifier(specifier.local, specifier.imported)], + t.stringLiteral(`react-native-css/components/${localName}`), + ), + ); + } + } + } + + return statements; +} + +export function handleReactNativeIdentifierRequire( + path: NodePath, + t: BabelTypes, + id: string, + source: string, + filename: string, +) { + const parsedSource = parseReactNativeSource(source, filename); + + if (!parsedSource) { + return; + } + + const { source: newSource, name } = parsedSource; + + if (name) { + return [ + t.variableDeclaration(path.node.kind, [ + t.variableDeclarator( + t.objectPattern([ + name === id + ? t.objectProperty( + t.identifier(name), + t.identifier(id), + false, + true, + ) + : t.objectProperty(t.identifier(name), t.identifier(id)), + ]), + t.callExpression(t.identifier("require"), [ + t.stringLiteral(newSource), + ]), + ), + ]), + ]; + } else { + return [ + t.variableDeclaration(path.node.kind, [ + t.variableDeclarator( + t.identifier(id), + t.callExpression(t.identifier("require"), [ + t.stringLiteral(newSource), + ]), + ), + ]), + ]; + } +} + +export function handleReactNativeObjectPatternRequire( + path: NodePath, + t: BabelTypes, + id: ObjectPattern, + source: string, + filename: string, +) { + const parsedSource = parseReactNativeSource(source, filename); + + // Don't handle `const { Text } = require('react-native/Libraries/Text/Text');` - we only handle package exports + if (!parsedSource || parsedSource.name) { + return; + } + + const statements: Statement[] = []; + + for (const identifier of id.properties) { + if (t.isRestElement(identifier)) { + // Bail out on `const { ...rest } = require('react-native');` + // We need to exit as we do not handle `const { Text, ...rest } = require('react-native');` + return; + } else if ( + !(t.isIdentifier(identifier.value) && t.isIdentifier(identifier.key)) + ) { + // Bail out on anything that isn't `const { : } = require('react-native');` + return; + } else { + const name = identifier.key.name; + + if (!allowedModules.has(name)) { + statements.push( + t.variableDeclaration(path.node.kind, [ + t.variableDeclarator( + t.objectPattern([identifier]), + t.callExpression(t.identifier("require"), [ + t.stringLiteral(source), + ]), + ), + ]), + ); + } else { + const newSourceWithName = `react-native-css/components/${name}`; + + statements.push( + t.variableDeclaration(path.node.kind, [ + t.variableDeclarator( + t.objectPattern([ + t.objectProperty( + t.identifier(name), + t.identifier(identifier.value.name), + false, + true, + ), + ]), + t.callExpression(t.identifier("require"), [ + t.stringLiteral(newSourceWithName), + ]), + ), + ]), + ); + } + } + } + + return statements; +}