Skip to content

Commit 242b3e8

Browse files
committed
fix: babel plugin rewrites for react-native-web
1 parent 2267265 commit 242b3e8

File tree

7 files changed

+671
-311
lines changed

7 files changed

+671
-311
lines changed

src/__tests__/babel/react-native-web.test.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,81 @@ describe("react-native-web", () => {
1414
plugins: ["@babel/plugin-syntax-jsx"],
1515
},
1616
tests: appendTitles([
17+
/* import tests */
1718
{
1819
code: `import 'react-native-web';`,
1920
output: `import "react-native-css/components";`,
2021
},
22+
{
23+
code: `import ReactNativeWeb from 'react-native-web';`,
24+
output: `import ReactNativeWeb from "react-native-css/components";`,
25+
},
2126
{
2227
code: `import { View } from 'react-native-web';`,
2328
output: `import { View } from "react-native-css/components/View";`,
2429
},
2530
{
26-
code: `import View from 'react-native-web/dist/commonjs/exports/View';`,
31+
code: `import View from 'react-native-web/dist/cjs/View';`,
2732
output: `import { View } from "react-native-css/components/View";`,
2833
},
2934
{
30-
code: `import View from 'react-native-web/dist/module/exports/View';`,
35+
code: `import View from 'react-native-web/dist/modules/View';`,
3136
output: `import { View } from "react-native-css/components/View";`,
3237
},
3338
{
3439
code: `import View from '../View';`,
3540
output: `import { View } from "react-native-css/components/View";`,
3641
babelOptions: {
37-
filename: "react-native-web/dist/module/exports/ScrollView/index.js",
42+
filename: "react-native-web/dist/modules/ScrollView/index.js",
43+
},
44+
},
45+
46+
/* require() tests */
47+
{
48+
code: `const { Text } = require('react-native-web');`,
49+
output: `const { Text } = require("react-native-css/components/Text");`,
50+
},
51+
{
52+
code: `const Text = require('react-native-web/dist/modules/Text');`,
53+
output: `const { Text } = require("react-native-css/components/Text");`,
54+
},
55+
{
56+
code: `const _Text = require('react-native-web/dist/modules/Text');`,
57+
output: `const { Text: _Text } = require("react-native-css/components/Text");`,
58+
},
59+
{
60+
code: `const Text = require('react-native-web/dist/cjs/Text');`,
61+
output: `const { Text } = require("react-native-css/components/Text");`,
62+
},
63+
{
64+
code: `const _Text = require('react-native-web/dist/cjs/Text');`,
65+
output: `const { Text: _Text } = require("react-native-css/components/Text");`,
66+
},
67+
{
68+
code: `const Text = require('react-native-web/dist/exports/Text');`,
69+
output: `const { Text } = require("react-native-css/components/Text");`,
70+
},
71+
{
72+
code: `const _Text = require('react-native-web/dist/exports/Text');`,
73+
output: `const { Text: _Text } = require("react-native-css/components/Text");`,
74+
},
75+
{
76+
code: `const _Text = _interopRequireDefault(require('react-native-web/dist/Text'));`,
77+
output: `const { Text: _Text } = require("react-native-css/components/Text");`,
78+
},
79+
{
80+
code: `const _Text = _interopRequireDefault(require('react-native-web/dist/modules/Text'));`,
81+
output: `const { Text: _Text } = require("react-native-css/components/Text");`,
82+
},
83+
{
84+
code: `const _Text = _interopRequireDefault(require('react-native-web/dist/cjs/Text'));`,
85+
output: `const { Text: _Text } = require("react-native-css/components/Text");`,
86+
},
87+
{
88+
code: `const View = _interopRequireDefault(require('../View'));`,
89+
output: `const { View } = require("react-native-css/components/View");`,
90+
babelOptions: {
91+
filename: "react-native-web/dist/modules/ScrollView/index.js",
3892
},
3993
},
4094
]),

src/__tests__/babel/smoke.test.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,10 @@ describe("plugin smoke tests", () => {
1515
filename: "/someFile.js",
1616
},
1717
tests: appendTitles([
18-
{
19-
code: `import 'react-native';`,
20-
output: `import "react-native-css/components";`,
21-
},
2218
{
2319
code: `import '../global.css';`,
2420
output: `import "../global.css";`,
2521
},
26-
{
27-
code: `import { View } from 'react-native';`,
28-
output: `import { View } from "react-native";`,
29-
babelOptions: {
30-
filename: "/node_modules/react-native-css/components/View.js",
31-
},
32-
},
33-
{
34-
code: `import { View } from 'react-native';`,
35-
output: `import { View } from "react-native";`,
36-
babelOptions: {
37-
filename: "react-native-css/src/components/View.js",
38-
},
39-
},
4022
{
4123
code: `import * as NativeComponentRegistry from '../../NativeComponent/NativeComponentRegistry'`,
4224
output: `import * as NativeComponentRegistry from "../../NativeComponent/NativeComponentRegistry";`,

src/babel/allowedModules.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { readdirSync } from "fs";
2+
import { join, parse } from "path";
3+
4+
function getFilesWithoutExtension(dirPath: string) {
5+
// Read all files and directories inside dirPath synchronously
6+
const entries = readdirSync(dirPath, { withFileTypes: true });
7+
8+
// Filter only files (ignore directories)
9+
const files = entries.filter(
10+
(entry) =>
11+
entry.isFile() &&
12+
/\.[jt]sx?$/.exec(entry.name) &&
13+
!/index\.[jt]sx?$/.exec(entry.name),
14+
);
15+
16+
// For each file, get the filename without extension
17+
const filesWithoutExt = files.map((file) => parse(file.name).name);
18+
19+
return filesWithoutExt;
20+
}
21+
22+
export const allowedModules = new Set(
23+
getFilesWithoutExtension(join(__dirname, "../components")),
24+
);

src/babel/helpers.ts

Lines changed: 16 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2-
import { readdirSync } from "fs";
3-
import { dirname, join, normalize, parse, resolve, sep } from "path";
4-
import { type NodePath } from "@babel/traverse";
1+
import tBabelTypes, { type CallExpression } from "@babel/types";
52

6-
import t from "@babel/types";
7-
8-
export type BabelTypes = typeof t;
3+
export type BabelTypes = typeof tBabelTypes;
94

105
export interface PluginOpts {
116
target?: string;
@@ -18,165 +13,28 @@ export interface PluginState {
1813
filename: string;
1914
}
2015

21-
export const allowedModules = new Set(
22-
getFilesWithoutExtension(join(__dirname, "../components")),
23-
);
24-
25-
function getFilesWithoutExtension(dirPath: string) {
26-
// Read all files and directories inside dirPath synchronously
27-
const entries = readdirSync(dirPath, { withFileTypes: true });
28-
29-
// Filter only files (ignore directories)
30-
const files = entries.filter(
31-
(entry) =>
32-
entry.isFile() &&
33-
/\.[jt]sx?$/.exec(entry.name) &&
34-
!/index\.[jt]sx?$/.exec(entry.name),
35-
);
36-
37-
// For each file, get the filename without extension
38-
const filesWithoutExt = files.map((file) => parse(file.name).name);
39-
40-
return filesWithoutExt;
41-
}
42-
43-
export function isInsideModule(filename: string, module: string): boolean {
44-
const normalized = normalize(filename);
45-
46-
// Match exact module name
47-
if (normalized === module) {
48-
return true;
49-
}
50-
51-
// Absolute posix paths
52-
if (normalized.startsWith(`${module}/`)) {
53-
return true;
16+
export function getInteropRequireDefaultSource(
17+
init: CallExpression,
18+
t: BabelTypes,
19+
) {
20+
if (!t.isIdentifier(init.callee, { name: "_interopRequireDefault" })) {
21+
return;
5422
}
5523

56-
// Check for our local development structure
57-
if (normalized.includes(`${sep}${module}${sep}src${sep}`)) {
58-
// Ignore the test files
59-
return !normalized.includes("__tests__");
60-
}
24+
const interopArg = init.arguments.at(0);
6125

62-
// Match classic node_modules
63-
if (normalized.includes(`${sep}node_modules${sep}${module}${sep}`)) {
64-
return true;
65-
}
66-
67-
// Match Yarn PnP .zip-style paths (e.g., .zip/node_modules/${module}/)
6826
if (
69-
normalized.includes(`${sep}.zip${sep}node_modules${sep}${module}${sep}`)
27+
!t.isCallExpression(interopArg) ||
28+
!t.isIdentifier(interopArg.callee, { name: "require" })
7029
) {
71-
return true;
72-
}
73-
74-
// Match Yarn .yarn/cache/${module}/*
75-
if (normalized.includes(`${sep}.yarn${sep}cache${sep}${module}${sep}`)) {
76-
return true;
30+
return;
7731
}
7832

79-
return false;
80-
}
81-
82-
export function isPackageImport(
83-
path: NodePath<t.ImportDeclaration | t.ExportNamedDeclaration>,
84-
) {
85-
const source = path.node.source?.value;
86-
if (!source) {
87-
return false;
88-
}
89-
90-
return source === "react-native" || source === "react-native-web";
91-
}
92-
93-
export function shouldTransformImport(
94-
path: NodePath<t.ImportDeclaration | t.ExportNamedDeclaration>,
95-
filename: string,
96-
) {
97-
let source = path.node.source?.value;
98-
99-
if (!source) {
100-
return false;
101-
}
102-
103-
if (source.startsWith(".")) {
104-
source = resolve(dirname(filename), source);
105-
}
106-
107-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
108-
return isReactNativeSource(source) || isReactNativeWebSource(source);
109-
}
110-
111-
function isReactNativeSource(source: string) {
112-
if (source === "react-native") return true;
113-
114-
const components = source.split(
115-
`react-native${sep}Libraries${sep}Components${sep}`,
116-
);
117-
118-
if (components.length > 1) {
119-
const component = components[1]?.split(sep)[0];
120-
return component && allowedModules.has(component);
121-
}
122-
123-
return false;
124-
}
125-
126-
function isReactNativeWebSource(source: string) {
127-
if (source === "react-native-web") return true;
128-
129-
let components = source.split(
130-
`react-native-web${sep}dist${sep}commonjs${sep}exports${sep}`,
131-
);
132-
if (components.length > 1) {
133-
const component = components[1]?.split(sep)[0];
134-
return component && allowedModules.has(component);
135-
}
136-
137-
components = source.split(
138-
`react-native-web${sep}dist${sep}module${sep}exports${sep}`,
139-
);
140-
if (components.length > 1) {
141-
const component = components[1]?.split(sep)[0];
142-
return component && allowedModules.has(component);
143-
}
144-
145-
return false;
146-
}
147-
148-
export function shouldTransformRequire(
149-
t: BabelTypes,
150-
node: t.VariableDeclaration,
151-
basePath: string,
152-
) {
153-
const { declarations } = node;
154-
155-
const declaration = declarations[0];
156-
157-
if (declarations.length > 1 || !declaration) {
158-
return false;
159-
}
160-
const { id, init } = declaration;
161-
162-
let source =
163-
(t.isObjectPattern(id) || t.isIdentifier(id)) &&
164-
t.isCallExpression(init) &&
165-
t.isIdentifier(init.callee) &&
166-
init.callee.name === "require" &&
167-
init.arguments.length === 1 &&
168-
"value" in init.arguments[0]! &&
169-
typeof init.arguments[0].value === "string" &&
170-
init.arguments[0].value;
171-
172-
if (!source) {
173-
return false;
174-
}
33+
const requireArg = interopArg.arguments.at(0);
17534

176-
if (source.startsWith(".")) {
177-
source = resolve(basePath, source);
35+
if (!t.isStringLiteral(requireArg)) {
36+
return;
17837
}
17938

180-
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
181-
return isReactNativeSource(source) || isReactNativeWebSource(source);
39+
return requireArg.value;
18240
}

0 commit comments

Comments
 (0)