Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions src/__tests__/babel/react-native-web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
]),
Expand Down
18 changes: 0 additions & 18 deletions src/__tests__/babel/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";`,
Expand Down
24 changes: 24 additions & 0 deletions src/babel/allowedModules.ts
Original file line number Diff line number Diff line change
@@ -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")),
);
174 changes: 16 additions & 158 deletions src/babel/helpers.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<t.ImportDeclaration | t.ExportNamedDeclaration>,
) {
const source = path.node.source?.value;
if (!source) {
return false;
}

return source === "react-native" || source === "react-native-web";
}

export function shouldTransformImport(
path: NodePath<t.ImportDeclaration | t.ExportNamedDeclaration>,
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;
}
Loading