UNPKG

create-nitro-lib

Version:
376 lines (343 loc) 10.7 kB
import path from 'node:path'; import { fileURLToPath } from 'node:url'; import fs from 'fs-extra'; import { toPascalCase } from '../utils/string.js'; export async function createExampleStructure(projectDir, config) { const exampleDir = path.join(projectDir, 'example'); const examplePackageJson = { name: 'example', main: 'expo-router/entry', version: '1.0.0', scripts: { start: 'expo start', android: 'expo run:android', ios: 'expo run:ios', web: 'expo start --web', prebuild: 'expo prebuild', pod: 'pod install --project-directory=ios', }, dependencies: { '@expo/vector-icons': '^14.0.2', '@react-navigation/native': '^7.0.14', expo: '~53.0.11', 'expo-build-properties': '~0.14.6', 'expo-dev-client': '~5.2.0', 'expo-font': '~13.3.1', 'expo-linking': '~7.1.5', 'expo-router': '~5.1.0', 'expo-splash-screen': '~0.30.9', 'expo-status-bar': '~2.2.3', 'expo-system-ui': '~5.0.8', 'expo-web-browser': '~14.1.6', react: '19.0.0', 'react-dom': '19.0.0', 'react-native': '0.79.3', [config.packageName]: '*', 'react-native-nitro-modules': '^0.26.2', 'react-native-reanimated': '~3.17.4', 'react-native-safe-area-context': '5.4.0', 'react-native-screens': '~4.11.1', 'react-native-web': '^0.20.0', }, devDependencies: { '@babel/core': '^7.27.4', '@types/react': '^19.1.8', '@types/react-native': '^0.73.0', }, 'release-it': { npm: { publish: false, }, git: false, github: { release: false, }, }, private: true, expo: { autolinking: { nativeModulesDir: '../node_modules', searchPaths: ['../node_modules', '../package'], }, }, }; const appDir = path.join(exampleDir, 'app'); const appNotFoundContent = `import { Link, Stack } from 'expo-router'; import { StyleSheet } from 'react-native'; import { Text, View } from '@/components/Themed'; export default function NotFoundScreen() { return ( <> <Stack.Screen options={{ title: 'Oops!' }} /> <View style={styles.container}> <Text style={styles.title}>This screen doesn't exist.</Text> <Link href="/" style={styles.link}> <Text style={styles.linkText}>Go to home screen!</Text> </Link> </View> </> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20, }, title: { fontSize: 20, fontWeight: 'bold', }, link: { marginTop: 15, paddingVertical: 15, }, linkText: { fontSize: 14, color: '#2e78b7', }, }); `; const pascalName = toPascalCase(config.name); const appIndexContent = `import { StyleSheet } from 'react-native' import { Text, View } from '@/components/Themed' import { ${pascalName} } from '${config.packageName}'; export default function IndexScreen() { return ( <View style={styles.container}> <Text style={styles.title}>{${pascalName}.hello("Nitro Developer")}</Text> <Text style={styles.title}>2 + 2 is {${pascalName}.add(2, 2)}</Text> <View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" /> </View> ) } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, title: { fontSize: 20, fontWeight: 'bold', paddingVertical: 10, textAlign: 'center', }, separator: { marginVertical: 30, height: 1, width: '80%', }, }) `; const appLayoutContent = `import 'expo-dev-client' import 'react-native-reanimated' import { useColorScheme } from '@/components/useColorScheme' import { DarkTheme, DefaultTheme, ThemeProvider, } from '@react-navigation/native' import { Stack } from 'expo-router' export { ErrorBoundary } from 'expo-router' export default function RootLayout() { const colorScheme = useColorScheme() return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <Stack> <Stack.Screen name="index" options={{ headerShown: false }} /> </Stack> </ThemeProvider> ) } `; const appJson = { expo: { name: 'example', slug: 'example', version: '1.0.0', orientation: 'portrait', icon: './assets/images/icon.png', scheme: 'example', userInterfaceStyle: 'automatic', newArchEnabled: true, splash: { image: './assets/images/splash-icon.png', resizeMode: 'contain', backgroundColor: '#ffffff', }, ios: { supportsTablet: true, }, android: { adaptiveIcon: { foregroundImage: './assets/images/adaptive-icon.png', backgroundColor: '#ffffff', }, edgeToEdgeEnabled: true, }, web: { bundler: 'metro', output: 'static', favicon: './assets/images/favicon.png', }, plugins: ['expo-router'], experiments: { typedRoutes: true, }, }, }; const tsConfig = { extends: 'expo/tsconfig.base', compilerOptions: { strict: true, paths: { '@/*': ['./*'], [config.packageName]: ['../package/src/index'], }, }, include: ['**/*.ts', '**/*.tsx', '.expo/types/**/*.ts', 'expo-env.d.ts'], }; const gitignoreContent = `# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files # dependencies node_modules/ # Expo .expo/ dist/ web-build/ expo-env.d.ts # Native .kotlin/ *.orig.* *.jks *.p8 *.p12 *.key *.mobileprovision # Metro .metro-health-check* # debug npm-debug.* yarn-debug.* yarn-error.* # macOS .DS_Store *.pem # local env files .env*.local # typescript *.tsbuildinfo `; const metroConfigContent = `const { getDefaultConfig } = require("expo/metro-config"); const path = require("path"); const projectRoot = __dirname; const monorepoRoot = path.resolve(projectRoot, "../.."); const config = getDefaultConfig(projectRoot); config.watchFolders = [monorepoRoot]; config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, "node_modules"), path.resolve(monorepoRoot, "node_modules"), ]; module.exports = config; `; await fs.writeJson(path.join(exampleDir, 'package.json'), examplePackageJson, { spaces: 2, }); await fs.writeJson(path.join(exampleDir, 'app.json'), appJson, { spaces: 2, }); await fs.writeJson(path.join(exampleDir, 'tsconfig.json'), tsConfig, { spaces: 2, }); await fs.writeFile(path.join(exampleDir, '.gitignore'), gitignoreContent); await fs.writeFile(path.join(exampleDir, 'metro.config.js'), metroConfigContent); const componentsDir = path.join(exampleDir, 'components'); const assetsImagesDir = path.join(exampleDir, 'assets', 'images'); const currentDir = path.dirname(fileURLToPath(import.meta.url)); const sourceAssetsDir = path.join(currentDir, '..', '..', 'src', 'assets', 'images'); const themedComponentContent = `import { Text as DefaultText, useColorScheme, View as DefaultView } from 'react-native'; import Colors from '@/constants/Colors'; type ThemeProps = { lightColor?: string; darkColor?: string; }; export type TextProps = ThemeProps & DefaultText['props']; export type ViewProps = ThemeProps & DefaultView['props']; export function useThemeColor( props: { light?: string; dark?: string }, colorName: keyof typeof Colors.light & keyof typeof Colors.dark ) { const theme = useColorScheme() ?? 'light'; const colorFromProps = props[theme]; if (colorFromProps) { return colorFromProps; } else { return Colors[theme][colorName]; } } export function Text(props: TextProps) { const { style, lightColor, darkColor, ...otherProps } = props; const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); return <DefaultText style={[{ color }, style]} {...otherProps} />; } export function View(props: ViewProps) { const { style, lightColor, darkColor, ...otherProps } = props; const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); return <DefaultView style={[{ backgroundColor }, style]} {...otherProps} />; } `; const useColorSchemeContent = `import { useColorScheme as _useColorScheme } from 'react-native'; export function useColorScheme() { return _useColorScheme(); } `; const colorsConstantContent = `const tintColorLight = '#2f95dc'; const tintColorDark = '#fff'; export default { light: { text: '#000', background: '#fff', tint: tintColorLight, tabIconDefault: '#ccc', tabIconSelected: tintColorLight, }, dark: { text: '#fff', background: '#000', tint: tintColorDark, tabIconDefault: '#ccc', tabIconSelected: tintColorDark, }, }; `; const constantsDir = path.join(exampleDir, 'constants'); await fs.ensureDir(appDir); await fs.ensureDir(componentsDir); await fs.ensureDir(constantsDir); await fs.ensureDir(assetsImagesDir); await fs.writeFile(path.join(appDir, '+not-found.tsx'), appNotFoundContent); await fs.writeFile(path.join(appDir, 'index.tsx'), appIndexContent); await fs.writeFile(path.join(appDir, '_layout.tsx'), appLayoutContent); await fs.writeFile(path.join(componentsDir, 'Themed.tsx'), themedComponentContent); await fs.writeFile(path.join(componentsDir, 'useColorScheme.tsx'), useColorSchemeContent); await fs.writeFile(path.join(constantsDir, 'Colors.ts'), colorsConstantContent); // Copy image assets const imageFiles = [ 'icon.png', 'splash-icon.png', 'adaptive-icon.png', 'favicon.png', ]; for (const imageFile of imageFiles) { const sourcePath = path.join(sourceAssetsDir, imageFile); const destPath = path.join(assetsImagesDir, imageFile); if (await fs.pathExists(sourcePath)) { await fs.copy(sourcePath, destPath); } } }