Skip to content

Commit 5b19e50

Browse files
authored
feat: add web support to all examples with vite (#933)
1 parent 410945e commit 5b19e50

File tree

35 files changed

+418
-114
lines changed

35 files changed

+418
-114
lines changed

docs/pages/create.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ If you want to create your own React Native library, scaffolding the project can
1111
- [TypeScript](https://www.typescriptlang.org/) to ensure type-safe code and better DX
1212
- Support for [Turbo Modules](https://reactnative.dev/docs/turbo-native-modules-introduction) & [Fabric](https://reactnative.dev/docs/fabric-native-components-introduction)
1313
- Support for [Kotlin](https://kotlinlang.org/) on Android & [Swift](https://developer.apple.com/swift/) on iOS
14-
- Support for C++ to write cross-platform native code
15-
- [Expo](https://expo.io/) for libraries without native code and web support
14+
- Example apps with [Community CLI](https://github.com/react-native-community/cli), [Expo](https://expo.dev/) or [React Native Test App](https://github.com/microsoft/react-native-test-app)
15+
- Support for Web with [Expo Web](https://docs.expo.dev/workflow/web/) or [Vite](https://vitejs.dev/)
1616
- [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/), [Lefthook](https://github.com/evilmartians/lefthook) and [Release It](https://github.com/release-it/release-it) pre-configured
1717
- [`react-native-builder-bob`](./build.md) pre-configured to compile your files
1818
- [GitHub Actions](https://github.com/features/actions) pre-configured to run tests and lint on the CI

packages/create-react-native-library/src/exampleApp/generateExampleApp.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ const PACKAGES_TO_REMOVE = [
4141

4242
const PACKAGES_TO_ADD_EXPO_WEB = {
4343
'@expo/metro-runtime': '~5.0.4',
44-
'react-dom': '19.1.0',
4544
'react-native-web': '~0.21.1',
4645
};
4746

@@ -173,9 +172,11 @@ export default async function generateExampleApp({
173172
'build:ios': `react-native build-ios --mode Debug`,
174173
};
175174

176-
if (config.example === 'vanilla') {
175+
if (config.example != null) {
177176
Object.assign(scripts, SCRIPTS_TO_ADD);
178-
} else if (config.example === 'test-app') {
177+
}
178+
179+
if (config.example === 'test-app') {
179180
// `react-native-test-app` doesn't bundle application by default in 'Release' mode and also `bundle` command doesn't create a directory.
180181
// `mkdist` script should be removed after stable React Native major contains this fix: https://github.com/facebook/react-native/pull/45182.
181182

@@ -276,8 +277,6 @@ export default async function generateExampleApp({
276277
scripts.android = 'expo run:android';
277278
scripts.ios = 'expo run:ios';
278279

279-
delete scripts.web;
280-
281280
await fs.writeFile(
282281
path.join(directory, '.gitignore'),
283282
dedent`
@@ -286,14 +285,22 @@ export default async function generateExampleApp({
286285
ios/
287286
`
288287
);
289-
} else {
290-
Object.entries(PACKAGES_TO_ADD_EXPO_WEB).forEach(([name, version]) => {
291-
dependencies[name] = bundledNativeModules[name] || version;
292-
});
288+
}
293289

294-
scripts.web = 'expo start --web';
290+
const reactVersion = dependencies.react ?? devDependencies.react;
291+
292+
if (typeof reactVersion !== 'string') {
293+
throw new Error("Couldn't find the package 'react' in the example app.");
295294
}
296295

296+
Object.entries(PACKAGES_TO_ADD_EXPO_WEB).forEach(([name, version]) => {
297+
dependencies[name] = bundledNativeModules[name] || version;
298+
});
299+
300+
dependencies['react-dom'] = reactVersion;
301+
scripts.web = 'expo start --web';
302+
scripts['build:web'] = 'expo export --platform web';
303+
297304
const app = await fs.readJSON(path.join(directory, 'app.json'));
298305

299306
app.expo.name = `${config.project.name} Example`;

packages/create-react-native-library/src/inform.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function printNonLocalLibNextSteps(config: TemplateConfiguration) {
77
const platforms = {
88
ios: { name: 'iOS', color: 'cyan' },
99
android: { name: 'Android', color: 'green' },
10-
...(config.example === 'expo'
10+
...(config.example === 'expo' || config.tools.includes('vite')
1111
? ({ web: { name: 'Web', color: 'blue' } } as const)
1212
: null),
1313
} as const;

packages/create-react-native-library/src/prompt.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -319,15 +319,6 @@ export const prompt = create(['[name]'], {
319319
title: choice.title,
320320
value: choice.value,
321321
description: choice.description,
322-
skip: (): boolean => {
323-
const answers = prompt.read();
324-
325-
if (answers.type === 'library') {
326-
return choice.value !== 'expo';
327-
}
328-
329-
return false;
330-
},
331322
})),
332323
required: true,
333324
skip: (): boolean => {
@@ -344,8 +335,27 @@ export const prompt = create(['[name]'], {
344335
value: key,
345336
title: tool.name,
346337
description: tool.description,
338+
skip: (): boolean => {
339+
if ('condition' in tool && tool.condition) {
340+
return !tool.condition({ example: prompt.read().example });
341+
}
342+
343+
return false;
344+
},
347345
})),
348-
default: Object.keys(AVAILABLE_TOOLS),
346+
default: (): string[] => {
347+
const answers = prompt.read();
348+
349+
return Object.entries(AVAILABLE_TOOLS)
350+
.filter(([, tool]) => {
351+
if ('condition' in tool && tool.condition) {
352+
return tool.condition({ example: answers.example });
353+
}
354+
355+
return true;
356+
})
357+
.map(([key]) => key);
358+
},
349359
required: true,
350360
skip: (): boolean => {
351361
const answers = prompt.read();

packages/create-react-native-library/src/template.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const EXAMPLE_COMMON_FILES = path.resolve(
5656
__dirname,
5757
'../templates/example-common'
5858
);
59+
const EXAMPLE_BARE_FILES = path.resolve(__dirname, '../templates/example-bare');
5960
const EXAMPLE_MODULE_NEW_FILES = path.resolve(
6061
__dirname,
6162
'../templates/example-module-new'
@@ -64,13 +65,14 @@ const EXAMPLE_VIEW_FILES = path.resolve(__dirname, '../templates/example-view');
6465
const EXAMPLE_EXPO_FILES = path.resolve(__dirname, '../templates/example-expo');
6566

6667
const JS_FILES = path.resolve(__dirname, '../templates/js-library');
68+
const JS_VIEW_FILES = path.resolve(__dirname, '../templates/js-view');
6769
const NATIVE_COMMON_FILES = path.resolve(
6870
__dirname,
6971
'../templates/native-common'
7072
);
71-
const NATIVE_COMMON_EXAMPLE_FILES = path.resolve(
73+
const EXAMPLE_NATIVE_COMMON_FILES = path.resolve(
7274
__dirname,
73-
'../templates/native-common-example'
75+
'../templates/example-native-common'
7476
);
7577
const NITRO_COMMON_FILES = path.resolve(__dirname, '../templates/nitro-common');
7678

@@ -203,27 +205,50 @@ export async function applyTemplates(
203205

204206
if (answers.languages === 'js') {
205207
await applyTemplate(config, JS_FILES, folder);
206-
await applyTemplate(config, EXAMPLE_EXPO_FILES, folder);
207-
} else {
208-
await applyTemplate(config, NATIVE_COMMON_FILES, folder);
209208

210209
if (config.example != null) {
211-
await applyTemplate(config, NATIVE_COMMON_EXAMPLE_FILES, folder);
210+
if (config.example === 'expo') {
211+
await applyTemplate(config, EXAMPLE_EXPO_FILES, folder);
212+
} else {
213+
await applyTemplate(config, EXAMPLE_BARE_FILES, folder);
214+
}
212215
}
216+
} else {
217+
await applyTemplate(config, NATIVE_COMMON_FILES, folder);
213218

214219
if (config.example === 'expo') {
215220
await applyTemplate(config, EXAMPLE_EXPO_FILES, folder);
221+
222+
if (config.project.native) {
223+
await applyTemplate(config, EXAMPLE_NATIVE_COMMON_FILES, folder);
224+
}
225+
} else if (config.example != null) {
226+
await applyTemplate(config, EXAMPLE_BARE_FILES, folder);
227+
228+
if (config.project.native) {
229+
await applyTemplate(config, EXAMPLE_NATIVE_COMMON_FILES, folder);
230+
}
216231
}
217232

218233
if (config.project.moduleConfig === 'nitro-modules') {
219234
await applyTemplate(config, NITRO_COMMON_FILES, folder);
220235
await applyTemplate(config, NATIVE_FILES['module_nitro'], folder);
236+
237+
if (config.example === 'expo' || config.tools.includes('vite')) {
238+
await applyTemplate(config, JS_FILES, folder);
239+
}
240+
221241
return;
222242
}
223243

224244
if (config.project.viewConfig === 'nitro-view') {
225245
await applyTemplate(config, NITRO_COMMON_FILES, folder);
226246
await applyTemplate(config, NATIVE_FILES['view_nitro'], folder);
247+
248+
if (config.example === 'expo' || config.tools.includes('vite')) {
249+
await applyTemplate(config, JS_VIEW_FILES, folder);
250+
}
251+
227252
return;
228253
}
229254

@@ -244,6 +269,14 @@ export async function applyTemplates(
244269
}_new` as const;
245270

246271
await applyTemplate(config, KOTLIN_FILES[templateType], folder);
272+
273+
if (config.example === 'expo' || config.tools.includes('vite')) {
274+
if (config.project.viewConfig !== null) {
275+
await applyTemplate(config, JS_VIEW_FILES, folder);
276+
} else {
277+
await applyTemplate(config, JS_FILES, folder);
278+
}
279+
}
247280
}
248281
}
249282

packages/create-react-native-library/src/utils/configureTools.ts

Lines changed: 107 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,27 @@ import path from 'node:path';
33
import { applyTemplate, type TemplateConfiguration } from '../template';
44
import sortObjectKeys from './sortObjectKeys';
55

6+
type PackageJson = {
7+
dependencies?: Record<string, string>;
8+
devDependencies?: Record<string, string>;
9+
peerDependencies?: Record<string, string>;
10+
[key: string]: unknown;
11+
};
12+
613
type Tool = {
714
name: string;
815
description: string;
9-
condition?: (config: TemplateConfiguration) => boolean;
16+
condition?: (context: Pick<TemplateConfiguration, 'example'>) => boolean;
17+
postprocess?: (options: {
18+
config: TemplateConfiguration;
19+
root: string;
20+
}) => void | Promise<void>;
1021
};
1122

1223
type Options = {
1324
tools: string[];
1425
root: string;
15-
packageJson: Record<string, unknown>;
26+
packageJson: PackageJson;
1627
config: TemplateConfiguration;
1728
};
1829

@@ -41,11 +52,45 @@ const TURBOREPO = {
4152
description: 'Cache build outputs on CI',
4253
};
4354

55+
const VITE: Tool = {
56+
name: 'Vite',
57+
description: 'Add web support to the example app',
58+
condition: (config) => config.example != null && config.example !== 'expo',
59+
postprocess: async ({ root }) => {
60+
const examplePkgPath = path.join(root, 'example', 'package.json');
61+
62+
if (!fs.existsSync(examplePkgPath)) {
63+
throw new Error("Couldn't find the example app's package.json.");
64+
}
65+
66+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
67+
const examplePackageJson = (await fs.readJson(
68+
examplePkgPath
69+
)) as PackageJson;
70+
71+
const reactVersion =
72+
examplePackageJson.dependencies?.react ??
73+
examplePackageJson.devDependencies?.react;
74+
75+
if (reactVersion == null) {
76+
throw new Error("Couldn't find the package 'react' in the example app.");
77+
}
78+
79+
examplePackageJson.dependencies = sortObjectKeys({
80+
...examplePackageJson.dependencies,
81+
'react-dom': reactVersion,
82+
});
83+
84+
await fs.writeJson(examplePkgPath, examplePackageJson, { spaces: 2 });
85+
},
86+
};
87+
4488
export const AVAILABLE_TOOLS = {
4589
eslint: ESLINT,
4690
jest: JEST,
4791
lefthook: LEFTHOOK,
4892
'release-it': RELEASE_IT,
93+
vite: VITE,
4994
} as const satisfies Record<string, Tool>;
5095

5196
const REQUIRED_TOOLS = {
@@ -89,44 +134,73 @@ export async function configureTools({
89134
await applyTemplate(config, toolDir, root);
90135
}
91136

92-
const pkgPath = path.join(toolDir, '~package.json');
137+
const examplePkgPath = path.join(toolDir, 'example', '~package.json');
138+
139+
await mergePackageJsonTemplate(
140+
path.join(toolDir, '~package.json'),
141+
packageJson
142+
);
93143

94-
if (fs.existsSync(pkgPath)) {
144+
if (fs.existsSync(examplePkgPath)) {
95145
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
96-
const toolPkg = (await fs.readJson(pkgPath)) as Record<string, unknown>;
146+
const existingExamplePackageJson = (await fs.readJson(
147+
path.join(root, 'example', 'package.json')
148+
)) as PackageJson;
149+
150+
await mergePackageJsonTemplate(
151+
examplePkgPath,
152+
existingExamplePackageJson
153+
);
154+
155+
await fs.writeJson(
156+
path.join(root, 'example', 'package.json'),
157+
existingExamplePackageJson,
158+
{
159+
spaces: 2,
160+
}
161+
);
162+
}
163+
164+
await tool.postprocess?.({ config, root });
165+
}
166+
}
167+
168+
async function mergePackageJsonTemplate(
169+
templatePath: string,
170+
packageJson: PackageJson
171+
) {
172+
if (!fs.existsSync(templatePath)) {
173+
return;
174+
}
175+
176+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
177+
const template = (await fs.readJson(templatePath)) as PackageJson;
178+
179+
for (const [field, value] of Object.entries(template)) {
180+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
181+
if (
182+
typeof packageJson[field] === 'object' ||
183+
packageJson[field] == null
184+
) {
185+
packageJson[field] = {
186+
...packageJson[field],
187+
...value,
188+
};
97189

98-
for (const [field, value] of Object.entries(toolPkg)) {
99190
if (
100-
typeof value === 'object' &&
101-
value !== null &&
102-
!Array.isArray(value)
191+
field === 'dependencies' ||
192+
field === 'devDependencies' ||
193+
field === 'peerDependencies'
103194
) {
104-
if (
105-
typeof packageJson[field] === 'object' ||
106-
packageJson[field] == null
107-
) {
108-
packageJson[field] = {
109-
...packageJson[field],
110-
...value,
111-
};
112-
113-
if (
114-
field === 'dependencies' ||
115-
field === 'devDependencies' ||
116-
field === 'peerDependencies'
117-
) {
118-
// @ts-expect-error: We know they are objects here
119-
packageJson[field] = sortObjectKeys(packageJson[field]);
120-
}
121-
} else {
122-
throw new Error(
123-
`Cannot merge '${field}' field because it is not an object (got '${String(packageJson[field])}').`
124-
);
125-
}
126-
} else {
127-
packageJson[field] = value;
195+
packageJson[field] = sortObjectKeys(packageJson[field]);
128196
}
197+
} else {
198+
throw new Error(
199+
`Cannot merge '${field}' field because it is not an object (got '${String(packageJson[field])}').`
200+
);
129201
}
202+
} else {
203+
packageJson[field] = value;
130204
}
131205
}
132206
}

0 commit comments

Comments
 (0)