diff --git a/my-expo-router-test-app/.gitignore b/my-expo-router-test-app/.gitignore new file mode 100644 index 00000000..f8c6c2e8 --- /dev/null +++ b/my-expo-router-test-app/.gitignore @@ -0,0 +1,43 @@ +# 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 + +app-example + +# generated native folders +/ios +/android diff --git a/my-expo-router-test-app/App.js b/my-expo-router-test-app/App.js new file mode 100644 index 00000000..67000ccb --- /dev/null +++ b/my-expo-router-test-app/App.js @@ -0,0 +1 @@ +import 'expo-router/entry' diff --git a/my-expo-router-test-app/README.md b/my-expo-router-test-app/README.md new file mode 100644 index 00000000..48dd63ff --- /dev/null +++ b/my-expo-router-test-app/README.md @@ -0,0 +1,50 @@ +# Welcome to your Expo app šŸ‘‹ + +This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). + +## Get started + +1. Install dependencies + + ```bash + npm install + ``` + +2. Start the app + + ```bash + npx expo start + ``` + +In the output, you'll find options to open the app in a + +- [development build](https://docs.expo.dev/develop/development-builds/introduction/) +- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) +- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) +- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo + +You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). + +## Get a fresh project + +When you're ready, run: + +```bash +npm run reset-project +``` + +This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. + +## Learn more + +To learn more about developing your project with Expo, look at the following resources: + +- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). +- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. + +## Join the community + +Join our community of developers creating universal apps. + +- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. +- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. diff --git a/my-expo-router-test-app/app.json b/my-expo-router-test-app/app.json new file mode 100644 index 00000000..c66b57a4 --- /dev/null +++ b/my-expo-router-test-app/app.json @@ -0,0 +1,48 @@ +{ + "expo": { + "name": "my-expo-router-test-app", + "slug": "my-expo-router-test-app", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "myexporoutertestapp", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "backgroundColor": "#E6F4FE", + "foregroundImage": "./assets/images/android-icon-foreground.png", + "backgroundImage": "./assets/images/android-icon-background.png", + "monochromeImage": "./assets/images/android-icon-monochrome.png" + }, + "edgeToEdgeEnabled": true, + "predictiveBackGestureEnabled": false + }, + "web": { + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-splash-screen", + { + "image": "./assets/images/splash-icon.png", + "imageWidth": 200, + "resizeMode": "contain", + "backgroundColor": "#ffffff", + "dark": { + "backgroundColor": "#000000" + } + } + ] + ], + "experiments": { + "typedRoutes": true, + "reactCompiler": true + } + } +} diff --git a/my-expo-router-test-app/app/(tabs)/_layout.tsx b/my-expo-router-test-app/app/(tabs)/_layout.tsx new file mode 100644 index 00000000..718131ff --- /dev/null +++ b/my-expo-router-test-app/app/(tabs)/_layout.tsx @@ -0,0 +1,35 @@ +import { Tabs } from 'expo-router'; +import React from 'react'; + +import { HapticTab } from '../../components/haptic-tab'; +import { IconSymbol } from '../../components/ui/icon-symbol'; +import { Colors } from '../../constants/theme'; +import { useColorScheme } from '../../hooks/use-color-scheme'; + +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + , + }} + /> + , + }} + /> + + ); +} diff --git a/my-expo-router-test-app/app/(tabs)/explore.tsx b/my-expo-router-test-app/app/(tabs)/explore.tsx new file mode 100644 index 00000000..70d5f168 --- /dev/null +++ b/my-expo-router-test-app/app/(tabs)/explore.tsx @@ -0,0 +1,112 @@ +import { Image } from 'expo-image'; +import { Platform, StyleSheet } from 'react-native'; + +import { ExternalLink } from '../../components/external-link'; +import ParallaxScrollView from '../../components/parallax-scroll-view'; +import { ThemedText } from '../../components/themed-text'; +import { ThemedView } from '../../components/themed-view'; +import { Collapsible } from '../../components/ui/collapsible'; +import { IconSymbol } from '../../components/ui/icon-symbol'; +import { Fonts } from '../../constants/theme'; + +export default function TabTwoScreen() { + return ( + + }> + + + Explore + + + This app includes example code to help you get started. + + + This app has two screens:{' '} + app/(tabs)/index.tsx and{' '} + app/(tabs)/explore.tsx + + + The layout file in app/(tabs)/_layout.tsx{' '} + sets up the tab navigator. + + + Learn more + + + + + You can open this project on Android, iOS, and the web. To open the web version, press{' '} + w in the terminal running this project. + + + + + For static images, you can use the @2x and{' '} + @3x suffixes to provide files for + different screen densities + + + + Learn more + + + + + This template has light and dark mode support. The{' '} + useColorScheme() hook lets you inspect + what the user's current color scheme is, and so you can adjust UI colors accordingly. + + + Learn more + + + + + This template includes an example of an animated component. The{' '} + components/HelloWave.tsx component uses + the powerful{' '} + + react-native-reanimated + {' '} + library to create a waving hand animation. + + {Platform.select({ + ios: ( + + The components/ParallaxScrollView.tsx{' '} + component provides a parallax effect for the header image. + + ), + })} + + + ); +} + +const styles = StyleSheet.create({ + headerImage: { + color: '#808080', + bottom: -90, + left: -35, + position: 'absolute', + }, + titleContainer: { + flexDirection: 'row', + gap: 8, + }, +}); diff --git a/my-expo-router-test-app/app/(tabs)/index.tsx b/my-expo-router-test-app/app/(tabs)/index.tsx new file mode 100644 index 00000000..99106b31 --- /dev/null +++ b/my-expo-router-test-app/app/(tabs)/index.tsx @@ -0,0 +1,98 @@ +import { Image } from 'expo-image'; +import { Platform, StyleSheet } from 'react-native'; + +import { Link } from 'expo-router'; +import { HelloWave } from '../../components/hello-wave'; +import ParallaxScrollView from '../../components/parallax-scroll-view'; +import { ThemedText } from '../../components/themed-text'; +import { ThemedView } from '../../components/themed-view'; + +export default function HomeScreen() { + return ( + + }> + + Welcome! + + + + Step 1: Try it + + Edit app/(tabs)/index.tsx to see changes. + Press{' '} + + {Platform.select({ + ios: 'cmd + d', + android: 'cmd + m', + web: 'F12', + })} + {' '} + to open developer tools. + + + + + + Step 2: Explore + + + + alert('Action pressed')} /> + alert('Share pressed')} + /> + + alert('Delete pressed')} + /> + + + + + + {`Tap the Explore tab to learn more about what's included in this starter app.`} + + + + Step 3: Get a fresh start + + {`When you're ready, run `} + npm run reset-project to get a fresh{' '} + app directory. This will move the current{' '} + app to{' '} + app-example. + + + + ); +} + +const styles = StyleSheet.create({ + titleContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + stepContainer: { + gap: 8, + marginBottom: 8, + }, + reactLogo: { + height: 178, + width: 290, + bottom: 0, + left: 0, + position: 'absolute', + }, +}); diff --git a/my-expo-router-test-app/app/_layout.tsx b/my-expo-router-test-app/app/_layout.tsx new file mode 100644 index 00000000..370c066c --- /dev/null +++ b/my-expo-router-test-app/app/_layout.tsx @@ -0,0 +1,24 @@ +import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import 'react-native-reanimated'; + +import { useColorScheme } from '../hooks/use-color-scheme'; + +export const unstable_settings = { + anchor: '(tabs)', +}; + +export default function RootLayout() { + const colorScheme = useColorScheme(); + + return ( + + + + + + + + ); +} diff --git a/my-expo-router-test-app/app/modal.tsx b/my-expo-router-test-app/app/modal.tsx new file mode 100644 index 00000000..ff6244c6 --- /dev/null +++ b/my-expo-router-test-app/app/modal.tsx @@ -0,0 +1,29 @@ +import { Link } from 'expo-router'; +import { StyleSheet } from 'react-native'; + +import { ThemedText } from '../components/themed-text'; +import { ThemedView } from '../components/themed-view'; + +export default function ModalScreen() { + return ( + + This is a modal + + Go to home screen + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + link: { + marginTop: 15, + paddingVertical: 15, + }, +}); diff --git a/my-expo-router-test-app/assets/images/android-icon-background.png b/my-expo-router-test-app/assets/images/android-icon-background.png new file mode 100644 index 00000000..5ffefc5b Binary files /dev/null and b/my-expo-router-test-app/assets/images/android-icon-background.png differ diff --git a/my-expo-router-test-app/assets/images/android-icon-foreground.png b/my-expo-router-test-app/assets/images/android-icon-foreground.png new file mode 100644 index 00000000..3a9e5016 Binary files /dev/null and b/my-expo-router-test-app/assets/images/android-icon-foreground.png differ diff --git a/my-expo-router-test-app/assets/images/android-icon-monochrome.png b/my-expo-router-test-app/assets/images/android-icon-monochrome.png new file mode 100644 index 00000000..77484ebd Binary files /dev/null and b/my-expo-router-test-app/assets/images/android-icon-monochrome.png differ diff --git a/my-expo-router-test-app/assets/images/favicon.png b/my-expo-router-test-app/assets/images/favicon.png new file mode 100644 index 00000000..408bd746 Binary files /dev/null and b/my-expo-router-test-app/assets/images/favicon.png differ diff --git a/my-expo-router-test-app/assets/images/icon.png b/my-expo-router-test-app/assets/images/icon.png new file mode 100644 index 00000000..7165a53c Binary files /dev/null and b/my-expo-router-test-app/assets/images/icon.png differ diff --git a/my-expo-router-test-app/assets/images/partial-react-logo.png b/my-expo-router-test-app/assets/images/partial-react-logo.png new file mode 100644 index 00000000..66fd9570 Binary files /dev/null and b/my-expo-router-test-app/assets/images/partial-react-logo.png differ diff --git a/my-expo-router-test-app/assets/images/react-logo.png b/my-expo-router-test-app/assets/images/react-logo.png new file mode 100644 index 00000000..9d72a9ff Binary files /dev/null and b/my-expo-router-test-app/assets/images/react-logo.png differ diff --git a/my-expo-router-test-app/assets/images/react-logo@2x.png b/my-expo-router-test-app/assets/images/react-logo@2x.png new file mode 100644 index 00000000..2229b130 Binary files /dev/null and b/my-expo-router-test-app/assets/images/react-logo@2x.png differ diff --git a/my-expo-router-test-app/assets/images/react-logo@3x.png b/my-expo-router-test-app/assets/images/react-logo@3x.png new file mode 100644 index 00000000..a99b2032 Binary files /dev/null and b/my-expo-router-test-app/assets/images/react-logo@3x.png differ diff --git a/my-expo-router-test-app/assets/images/splash-icon.png b/my-expo-router-test-app/assets/images/splash-icon.png new file mode 100644 index 00000000..03d6f6b6 Binary files /dev/null and b/my-expo-router-test-app/assets/images/splash-icon.png differ diff --git a/my-expo-router-test-app/components/external-link.tsx b/my-expo-router-test-app/components/external-link.tsx new file mode 100644 index 00000000..883e515a --- /dev/null +++ b/my-expo-router-test-app/components/external-link.tsx @@ -0,0 +1,25 @@ +import { Href, Link } from 'expo-router'; +import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser'; +import { type ComponentProps } from 'react'; + +type Props = Omit, 'href'> & { href: Href & string }; + +export function ExternalLink({ href, ...rest }: Props) { + return ( + { + if (process.env.EXPO_OS !== 'web') { + // Prevent the default behavior of linking to the default browser on native. + event.preventDefault(); + // Open the link in an in-app browser. + await openBrowserAsync(href, { + presentationStyle: WebBrowserPresentationStyle.AUTOMATIC, + }); + } + }} + /> + ); +} diff --git a/my-expo-router-test-app/components/haptic-tab.tsx b/my-expo-router-test-app/components/haptic-tab.tsx new file mode 100644 index 00000000..7f3981cb --- /dev/null +++ b/my-expo-router-test-app/components/haptic-tab.tsx @@ -0,0 +1,18 @@ +import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'; +import { PlatformPressable } from '@react-navigation/elements'; +import * as Haptics from 'expo-haptics'; + +export function HapticTab(props: BottomTabBarButtonProps) { + return ( + { + if (process.env.EXPO_OS === 'ios') { + // Add a soft haptic feedback when pressing down on the tabs. + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + props.onPressIn?.(ev); + }} + /> + ); +} diff --git a/my-expo-router-test-app/components/hello-wave.tsx b/my-expo-router-test-app/components/hello-wave.tsx new file mode 100644 index 00000000..5def547c --- /dev/null +++ b/my-expo-router-test-app/components/hello-wave.tsx @@ -0,0 +1,19 @@ +import Animated from 'react-native-reanimated'; + +export function HelloWave() { + return ( + + šŸ‘‹ + + ); +} diff --git a/my-expo-router-test-app/components/parallax-scroll-view.tsx b/my-expo-router-test-app/components/parallax-scroll-view.tsx new file mode 100644 index 00000000..d45747c4 --- /dev/null +++ b/my-expo-router-test-app/components/parallax-scroll-view.tsx @@ -0,0 +1,79 @@ +import type { PropsWithChildren, ReactElement } from 'react'; +import { StyleSheet } from 'react-native'; +import Animated, { + interpolate, + useAnimatedRef, + useAnimatedStyle, + useScrollOffset, +} from 'react-native-reanimated'; + +import { ThemedView } from '../components/themed-view'; +import { useColorScheme } from '../hooks/use-color-scheme'; +import { useThemeColor } from '../hooks/use-theme-color'; + +const HEADER_HEIGHT = 250; + +type Props = PropsWithChildren<{ + headerImage: ReactElement; + headerBackgroundColor: { dark: string; light: string }; +}>; + +export default function ParallaxScrollView({ + children, + headerImage, + headerBackgroundColor, +}: Props) { + const backgroundColor = useThemeColor({}, 'background'); + const colorScheme = useColorScheme() ?? 'light'; + const scrollRef = useAnimatedRef(); + const scrollOffset = useScrollOffset(scrollRef); + const headerAnimatedStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateY: interpolate( + scrollOffset.value, + [-HEADER_HEIGHT, 0, HEADER_HEIGHT], + [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] + ), + }, + { + scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]), + }, + ], + }; + }); + + return ( + + + {headerImage} + + {children} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + height: HEADER_HEIGHT, + overflow: 'hidden', + }, + content: { + flex: 1, + padding: 32, + gap: 16, + overflow: 'hidden', + }, +}); diff --git a/my-expo-router-test-app/components/themed-text.tsx b/my-expo-router-test-app/components/themed-text.tsx new file mode 100644 index 00000000..4b0c5377 --- /dev/null +++ b/my-expo-router-test-app/components/themed-text.tsx @@ -0,0 +1,60 @@ +import { StyleSheet, Text, type TextProps } from 'react-native'; + +import { useThemeColor } from '../hooks/use-theme-color'; + +export type ThemedTextProps = TextProps & { + lightColor?: string; + darkColor?: string; + type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'; +}; + +export function ThemedText({ + style, + lightColor, + darkColor, + type = 'default', + ...rest +}: ThemedTextProps) { + const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text'); + + return ( + + ); +} + +const styles = StyleSheet.create({ + default: { + fontSize: 16, + lineHeight: 24, + }, + defaultSemiBold: { + fontSize: 16, + lineHeight: 24, + fontWeight: '600', + }, + title: { + fontSize: 32, + fontWeight: 'bold', + lineHeight: 32, + }, + subtitle: { + fontSize: 20, + fontWeight: 'bold', + }, + link: { + lineHeight: 30, + fontSize: 16, + color: '#0a7ea4', + }, +}); diff --git a/my-expo-router-test-app/components/themed-view.tsx b/my-expo-router-test-app/components/themed-view.tsx new file mode 100644 index 00000000..ff550635 --- /dev/null +++ b/my-expo-router-test-app/components/themed-view.tsx @@ -0,0 +1,14 @@ +import { View, type ViewProps } from 'react-native'; + +import { useThemeColor } from '../hooks/use-theme-color'; + +export type ThemedViewProps = ViewProps & { + lightColor?: string; + darkColor?: string; +}; + +export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) { + const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background'); + + return ; +} diff --git a/my-expo-router-test-app/components/ui/collapsible.tsx b/my-expo-router-test-app/components/ui/collapsible.tsx new file mode 100644 index 00000000..d7e33049 --- /dev/null +++ b/my-expo-router-test-app/components/ui/collapsible.tsx @@ -0,0 +1,45 @@ +import { PropsWithChildren, useState } from 'react'; +import { StyleSheet, TouchableOpacity } from 'react-native'; + +import { ThemedText } from '../../components/themed-text'; +import { ThemedView } from '../../components/themed-view'; +import { IconSymbol } from '../../components/ui/icon-symbol'; +import { Colors } from '../../constants/theme'; +import { useColorScheme } from '../../hooks/use-color-scheme'; + +export function Collapsible({ children, title }: PropsWithChildren & { title: string }) { + const [isOpen, setIsOpen] = useState(false); + const theme = useColorScheme() ?? 'light'; + + return ( + + setIsOpen((value) => !value)} + activeOpacity={0.8}> + + + {title} + + {isOpen && {children}} + + ); +} + +const styles = StyleSheet.create({ + heading: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + content: { + marginTop: 6, + marginLeft: 24, + }, +}); diff --git a/my-expo-router-test-app/components/ui/icon-symbol.ios.tsx b/my-expo-router-test-app/components/ui/icon-symbol.ios.tsx new file mode 100644 index 00000000..9177f4da --- /dev/null +++ b/my-expo-router-test-app/components/ui/icon-symbol.ios.tsx @@ -0,0 +1,32 @@ +import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols'; +import { StyleProp, ViewStyle } from 'react-native'; + +export function IconSymbol({ + name, + size = 24, + color, + style, + weight = 'regular', +}: { + name: SymbolViewProps['name']; + size?: number; + color: string; + style?: StyleProp; + weight?: SymbolWeight; +}) { + return ( + + ); +} diff --git a/my-expo-router-test-app/components/ui/icon-symbol.tsx b/my-expo-router-test-app/components/ui/icon-symbol.tsx new file mode 100644 index 00000000..b7ece6b3 --- /dev/null +++ b/my-expo-router-test-app/components/ui/icon-symbol.tsx @@ -0,0 +1,41 @@ +// Fallback for using MaterialIcons on Android and web. + +import MaterialIcons from '@expo/vector-icons/MaterialIcons'; +import { SymbolWeight, SymbolViewProps } from 'expo-symbols'; +import { ComponentProps } from 'react'; +import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native'; + +type IconMapping = Record['name']>; +type IconSymbolName = keyof typeof MAPPING; + +/** + * Add your SF Symbols to Material Icons mappings here. + * - see Material Icons in the [Icons Directory](https://icons.expo.fyi). + * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app. + */ +const MAPPING = { + 'house.fill': 'home', + 'paperplane.fill': 'send', + 'chevron.left.forwardslash.chevron.right': 'code', + 'chevron.right': 'chevron-right', +} as IconMapping; + +/** + * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web. + * This ensures a consistent look across platforms, and optimal resource usage. + * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons. + */ +export function IconSymbol({ + name, + size = 24, + color, + style, +}: { + name: IconSymbolName; + size?: number; + color: string | OpaqueColorValue; + style?: StyleProp; + weight?: SymbolWeight; +}) { + return ; +} diff --git a/my-expo-router-test-app/constants/theme.ts b/my-expo-router-test-app/constants/theme.ts new file mode 100644 index 00000000..f06facd2 --- /dev/null +++ b/my-expo-router-test-app/constants/theme.ts @@ -0,0 +1,53 @@ +/** + * Below are the colors that are used in the app. The colors are defined in the light and dark mode. + * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. + */ + +import { Platform } from 'react-native'; + +const tintColorLight = '#0a7ea4'; +const tintColorDark = '#fff'; + +export const Colors = { + light: { + text: '#11181C', + background: '#fff', + tint: tintColorLight, + icon: '#687076', + tabIconDefault: '#687076', + tabIconSelected: tintColorLight, + }, + dark: { + text: '#ECEDEE', + background: '#151718', + tint: tintColorDark, + icon: '#9BA1A6', + tabIconDefault: '#9BA1A6', + tabIconSelected: tintColorDark, + }, +}; + +export const Fonts = Platform.select({ + ios: { + /** iOS `UIFontDescriptorSystemDesignDefault` */ + sans: 'system-ui', + /** iOS `UIFontDescriptorSystemDesignSerif` */ + serif: 'ui-serif', + /** iOS `UIFontDescriptorSystemDesignRounded` */ + rounded: 'ui-rounded', + /** iOS `UIFontDescriptorSystemDesignMonospaced` */ + mono: 'ui-monospace', + }, + default: { + sans: 'normal', + serif: 'serif', + rounded: 'normal', + mono: 'monospace', + }, + web: { + sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", + serif: "Georgia, 'Times New Roman', serif", + rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", + mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", + }, +}); diff --git a/my-expo-router-test-app/eslint.config.js b/my-expo-router-test-app/eslint.config.js new file mode 100644 index 00000000..5025da68 --- /dev/null +++ b/my-expo-router-test-app/eslint.config.js @@ -0,0 +1,10 @@ +// https://docs.expo.dev/guides/using-eslint/ +const { defineConfig } = require('eslint/config'); +const expoConfig = require('eslint-config-expo/flat'); + +module.exports = defineConfig([ + expoConfig, + { + ignores: ['dist/*'], + }, +]); diff --git a/my-expo-router-test-app/hooks/use-color-scheme.ts b/my-expo-router-test-app/hooks/use-color-scheme.ts new file mode 100644 index 00000000..17e3c63e --- /dev/null +++ b/my-expo-router-test-app/hooks/use-color-scheme.ts @@ -0,0 +1 @@ +export { useColorScheme } from 'react-native'; diff --git a/my-expo-router-test-app/hooks/use-color-scheme.web.ts b/my-expo-router-test-app/hooks/use-color-scheme.web.ts new file mode 100644 index 00000000..7eb1c1b7 --- /dev/null +++ b/my-expo-router-test-app/hooks/use-color-scheme.web.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react'; +import { useColorScheme as useRNColorScheme } from 'react-native'; + +/** + * To support static rendering, this value needs to be re-calculated on the client side for web + */ +export function useColorScheme() { + const [hasHydrated, setHasHydrated] = useState(false); + + useEffect(() => { + setHasHydrated(true); + }, []); + + const colorScheme = useRNColorScheme(); + + if (hasHydrated) { + return colorScheme; + } + + return 'light'; +} diff --git a/my-expo-router-test-app/hooks/use-theme-color.ts b/my-expo-router-test-app/hooks/use-theme-color.ts new file mode 100644 index 00000000..6b30a154 --- /dev/null +++ b/my-expo-router-test-app/hooks/use-theme-color.ts @@ -0,0 +1,21 @@ +/** + * Learn more about light and dark modes: + * https://docs.expo.dev/guides/color-schemes/ + */ + +import { Colors } from '../constants/theme'; +import { useColorScheme } from '../hooks/use-color-scheme'; + +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]; + } +} diff --git a/my-expo-router-test-app/package.json b/my-expo-router-test-app/package.json new file mode 100644 index 00000000..7bdecb1a --- /dev/null +++ b/my-expo-router-test-app/package.json @@ -0,0 +1,47 @@ +{ + "name": "my-expo-router-test-app", + "main": "expo-router/entry", + "version": "1.0.0", + "scripts": { + "start": "expo start", + "reset-project": "node ./scripts/reset-project.js", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web", + "lint": "expo lint" + }, + "dependencies": { + "@expo/vector-icons": "^15.0.3", + "@react-navigation/bottom-tabs": "^7.4.0", + "@react-navigation/elements": "^2.6.3", + "@react-navigation/native": "^7.1.8", + "expo": "~54.0.27", + "expo-constants": "~18.0.11", + "expo-font": "~14.0.10", + "expo-haptics": "~15.0.8", + "expo-image": "~3.0.11", + "expo-linking": "~8.0.10", + "expo-router": "~6.0.17", + "expo-splash-screen": "~31.0.12", + "expo-status-bar": "~3.0.9", + "expo-symbols": "~1.0.8", + "expo-system-ui": "~6.0.9", + "expo-web-browser": "~15.0.10", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-native": "0.81.5", + "react-native-gesture-handler": "~2.28.0", + "react-native-worklets": "0.5.1", + "react-native-reanimated": "~4.1.1", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-web": "~0.21.0" + }, + "devDependencies": { + "@types/react": "~19.1.0", + "typescript": "~5.9.2", + "eslint": "^9.25.0", + "eslint-config-expo": "~10.0.0" + }, + "private": true +} diff --git a/my-expo-router-test-app/scripts/reset-project.js b/my-expo-router-test-app/scripts/reset-project.js new file mode 100755 index 00000000..51dff15a --- /dev/null +++ b/my-expo-router-test-app/scripts/reset-project.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node + +/** + * This script is used to reset the project to a blank state. + * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file. + * You can remove the `reset-project` script from package.json and safely delete this file after running it. + */ + +const fs = require("fs"); +const path = require("path"); +const readline = require("readline"); + +const root = process.cwd(); +const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; +const exampleDir = "app-example"; +const newAppDir = "app"; +const exampleDirPath = path.join(root, exampleDir); + +const indexContent = `import { Text, View } from "react-native"; + +export default function Index() { + return ( + + Edit app/index.tsx to edit this screen. + + ); +} +`; + +const layoutContent = `import { Stack } from "expo-router"; + +export default function RootLayout() { + return ; +} +`; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const moveDirectories = async (userInput) => { + try { + if (userInput === "y") { + // Create the app-example directory + await fs.promises.mkdir(exampleDirPath, { recursive: true }); + console.log(`šŸ“ /${exampleDir} directory created.`); + } + + // Move old directories to new app-example directory or delete them + for (const dir of oldDirs) { + const oldDirPath = path.join(root, dir); + if (fs.existsSync(oldDirPath)) { + if (userInput === "y") { + const newDirPath = path.join(root, exampleDir, dir); + await fs.promises.rename(oldDirPath, newDirPath); + console.log(`āž”ļø /${dir} moved to /${exampleDir}/${dir}.`); + } else { + await fs.promises.rm(oldDirPath, { recursive: true, force: true }); + console.log(`āŒ /${dir} deleted.`); + } + } else { + console.log(`āž”ļø /${dir} does not exist, skipping.`); + } + } + + // Create new /app directory + const newAppDirPath = path.join(root, newAppDir); + await fs.promises.mkdir(newAppDirPath, { recursive: true }); + console.log("\nšŸ“ New /app directory created."); + + // Create index.tsx + const indexPath = path.join(newAppDirPath, "index.tsx"); + await fs.promises.writeFile(indexPath, indexContent); + console.log("šŸ“„ app/index.tsx created."); + + // Create _layout.tsx + const layoutPath = path.join(newAppDirPath, "_layout.tsx"); + await fs.promises.writeFile(layoutPath, layoutContent); + console.log("šŸ“„ app/_layout.tsx created."); + + console.log("\nāœ… Project reset complete. Next steps:"); + console.log( + `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ + userInput === "y" + ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` + : "" + }` + ); + } catch (error) { + console.error(`āŒ Error during script execution: ${error.message}`); + } +}; + +rl.question( + "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", + (answer) => { + const userInput = answer.trim().toLowerCase() || "y"; + if (userInput === "y" || userInput === "n") { + moveDirectories(userInput).finally(() => rl.close()); + } else { + console.log("āŒ Invalid input. Please enter 'Y' or 'N'."); + rl.close(); + } + } +); diff --git a/my-expo-router-test-app/tsconfig.json b/my-expo-router-test-app/tsconfig.json new file mode 100644 index 00000000..658b144e --- /dev/null +++ b/my-expo-router-test-app/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ".expo/types/**/*.ts", + "expo-env.d.ts" +, "App.js" ] +} diff --git a/package.json b/package.json index 5c8f815d..a65d1b40 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "packages/snack-require-context", "packages/snack-sdk", "packages/snack-term", + "packages/snack-cli", "website", "snackager", "snackpub" diff --git a/packages/snack-cli/.gitignore b/packages/snack-cli/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/packages/snack-cli/.gitignore @@ -0,0 +1 @@ +build diff --git a/packages/snack-cli/README.md b/packages/snack-cli/README.md new file mode 100644 index 00000000..927d504b --- /dev/null +++ b/packages/snack-cli/README.md @@ -0,0 +1,48 @@ +# snack-cli + +CLI for creating and managing Expo Snacks from your local file system. + +## Features + +- **Local Development**: Create Snacks from your local files. +- **File Watching**: Automatically updates the Snack when files change. +- **Online Mode**: Automatically sets the Snack to online mode, making it available in Expo Go and the web player. +- **Runtime Logs**: Streams logs (`console.log`, `console.error`, etc.) from connected clients to your terminal. + +## Usage + +### Development + +To use the CLI during development, you can create an alias to the built script: + +```bash +# Build the package first +yarn workspace snack-cli build + +# Create an alias (replace /absolute/path/to with your actual path) +alias snack-cli-dev='/absolute/path/to/expo/snack/packages/snack-cli/build/index.js' + +# Run the CLI +snack-cli-dev start +``` + +### Commands + +#### `start` + +Starts a Snack from the current directory. + +```bash +snack-cli-dev start +``` + +Options: +- `SNACK_SESSION_SECRET`: Set this environment variable to your Expo session secret to save Snacks to your account. + +### Debugging + +To see verbose logs from the CLI, set the `DEBUG` environment variable: + +```bash +DEBUG=snack-cli snack-cli-dev start +``` diff --git a/packages/snack-cli/package.json b/packages/snack-cli/package.json new file mode 100644 index 00000000..a9d0a47e --- /dev/null +++ b/packages/snack-cli/package.json @@ -0,0 +1,40 @@ +{ + "name": "snack-cli", + "version": "0.0.3", + "description": "CLI for Expo Snack", + "main": "build/index.js", + "types": "build/index.d.ts", + "bin": { + "snack-cli": "build/index.js" + }, + "scripts": { + "build": "tsc && yarn mark-executable", + "mark-executable": "chmod +x build/index.js", + "start": "node build/index.js start", + "watch": "tsc -w" + }, + "dependencies": { + "commander": "^11.1.0", + "debug": "^4.4.3", + "dotenv": "^16.3.1", + "glob": "^10.3.10", + "snack-sdk": "6.6.0", + "snack-content": "3.6.0" + }, + "devDependencies": { + "@types/debug": "^4.1.12", + "@types/glob": "^8.1.0", + "@types/node": "^20.10.5", + "typescript": "^5.3.3" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/expo/snack.git", + "directory": "packages/snack-cli" + }, + "bugs": { + "url": "https://github.com/expo/snack/issues" + }, + "author": "Expo ", + "license": "MIT" +} diff --git a/packages/snack-cli/src/commands/save.ts b/packages/snack-cli/src/commands/save.ts new file mode 100644 index 00000000..b89a1fba --- /dev/null +++ b/packages/snack-cli/src/commands/save.ts @@ -0,0 +1,72 @@ +import { Snack } from 'snack-sdk'; +import path from 'path'; +import Debug from 'debug'; +import { readJsonAsync } from '../lib/read-json'; +import { writeJsonAsync } from '../lib/write-json'; +import { randomName } from '../lib/random-name'; +import { loadFiles } from '../lib/load-files'; +import { loadDependencies } from '../lib/load-dependencies'; +import { getRuntimeEndpoint } from '../lib/snack-runtime'; +import { withExtraParams } from '../lib/url-params'; + +const debug = Debug('snack-cli'); + +export async function save(options: { experimentalRuntime?: boolean }) { + const cwd = process.cwd(); + const snackJsonPath = path.join(cwd, '.snack', 'snack.json'); + + const snackJson: { name?: string } = await readJsonAsync(snackJsonPath) ?? {}; + const existingSnackName = snackJson.name; + + const sessionSecret = process.env.SNACK_SESSION_SECRET; + if (!sessionSecret) { + console.warn('No sessionSecret found in env (SNACK_SESSION_SECRET). Snack will be saved anonymously (as read-only).'); + } + + const dependencies = await loadDependencies(cwd); + + const snack = new Snack({ + name: existingSnackName, + dependencies, + user: sessionSecret ? { sessionSecret } : undefined, + verbose: false, + runtimeEndpoint: getRuntimeEndpoint(options) + }); + + let newName: string | null = null; + if (!existingSnackName && sessionSecret) { + newName = randomName(); + snack.setName(newName); + } + + snack.updateFiles(await loadFiles(cwd)); + + let result: Awaited> | null = null; + try { + debug('Uploading files...'); + console.log('Uploading files...'); + result = await snack.saveAsync({ + ignoreUser: !sessionSecret, + }); + debug(`Saved!`, result); + } catch (e) { + console.error('Failed to save snack:', e); + } + if (!result) { + debug('Missing result from saveAsync'); + process.exit(1); + } + + // Save snackId if it's new + if (result.id && sessionSecret && newName) { + // TODO: Add lock to avoid race condition with multiple concurrent saves + snackJson.name = newName; + await writeJsonAsync(snackJsonPath, snackJson); + debug(`Saved snackId to ${snackJsonPath}`); + } + + const finalUrl = withExtraParams(result.url); + + console.log(`Snack saved successfully!`); + console.log(`Available at: ${finalUrl}`); +} diff --git a/packages/snack-cli/src/commands/start.ts b/packages/snack-cli/src/commands/start.ts new file mode 100644 index 00000000..1e1f2ee1 --- /dev/null +++ b/packages/snack-cli/src/commands/start.ts @@ -0,0 +1,104 @@ +import { Snack, SnackDependency, SnackFile } from 'snack-sdk'; +import fs from 'fs'; +import path from 'path'; +import Debug from 'debug'; +import { IGNORE_DIRS, loadFiles } from '../lib/load-files'; +import { parseJson } from '../lib/parse-json'; +import { loadDependencies } from '../lib/load-dependencies'; +import { readJsonAsync } from '../lib/read-json'; +import { getRuntimeEndpoint } from '../lib/snack-runtime'; +import { withExtraParams } from '../lib/url-params'; + +const debug = Debug('snack-cli'); + +export async function start(options: { experimentalRuntime?: boolean }) { + const cwd = process.cwd(); + const snackJsonPath = path.join(cwd, '.snack', 'snack.json'); + + const snackJson: { name?: string } = await readJsonAsync(snackJsonPath) ?? {}; + const existingSnackName = snackJson.name; + + const sessionSecret = process.env.SNACK_SESSION_SECRET || process.env.sessionSecret; + if (!sessionSecret) { + console.warn('No sessionSecret found in env (SNACK_SESSION_SECRET). Snack will be anonymous/read-only if required.'); + } + + const dependencies = await loadDependencies(cwd); + + const snack = new Snack({ + name: existingSnackName, + dependencies, + user: sessionSecret ? { sessionSecret } : undefined, + verbose: false, // We handle logging manually + runtimeEndpoint: getRuntimeEndpoint(options) + }); + snack.setOnline(true); + + const finalUrl = withExtraParams(snack.getState().url); + console.log(`Available at: ${finalUrl}`); + + snack.updateFiles(await loadFiles(cwd)); + + snack.addLogListener((log) => { + const { type, message, connectedClient } = log; + const clientName = connectedClient?.name ? `[${connectedClient.name}] ` : ''; + + if (type === 'error') { + console.error(`${clientName}Error: ${message}`); + } else if (type === 'warn') { + console.warn(`${clientName}Warn: ${message}`); + } else { + console.log(`${clientName}${message}`); + } + }); + + console.log('Watching for file changes...'); + let debounceTimer: NodeJS.Timeout | null = null; + + let packageJsonPath = path.join(cwd, 'package.json'); + + fs.watch(cwd, { recursive: true }, (eventType, filename) => { + if (!filename) return; + + // Check ignores + if (IGNORE_DIRS.some(dir => filename.startsWith(dir))) return; + + debug(`File changed: ${filename}`); + + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(async () => { + const fullPath = path.join(cwd, filename); + if (fs.existsSync(fullPath)) { + if (fs.statSync(fullPath).isFile()) { + const contents = fs.readFileSync(fullPath, 'utf-8'); + snack.updateFiles({ + [filename]: { type: 'CODE', contents } + }); + if (fullPath === packageJsonPath) { + const packageJson: { dependencies?: { [name: string]: string } } = parseJson(contents) ?? {}; + const packageDependencies = packageJson?.dependencies ?? {}; + const currentDependencies = snack.getState().dependencies; + const newDependencies = Object.entries(packageDependencies).reduce((acc, [name, version]) => { + acc[name] = { version: "*" }; // TODO: Handle versions. + return acc; + }, {} as { [name: string]: SnackDependency }) + const dependenciesToRemove = Object + .keys(currentDependencies) + .filter(name => !packageDependencies[name]) + .reduce((acc, name) => { + acc[name] = null; + return acc; + }, {} as { [name: string]: null }); + snack.updateDependencies({ + ...dependenciesToRemove, + ...newDependencies, + }); + } + } + } else { + // Deleted + snack.updateFiles({ [filename]: null }); + } + }, 1000); + }); +} diff --git a/packages/snack-cli/src/index.ts b/packages/snack-cli/src/index.ts new file mode 100644 index 00000000..a02c4b89 --- /dev/null +++ b/packages/snack-cli/src/index.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node +import { Command } from 'commander'; +import { start } from './commands/start'; +import { save } from './commands/save'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const program = new Command(); + +program + .name('snack-cli') + .description('CLI for Expo Snack') + .version('0.0.3'); + +program + .command('start') + .description('Start a Snack from the current directory') + .option('--experimental-runtime', 'Use the experimental runtime endpoint') + .action(start); + +program + .command('save') + .description('Save the current Snack') + .option('--experimental-runtime', 'Use the experimental runtime endpoint') + .action(save); + +program.parse(); diff --git a/packages/snack-cli/src/lib/load-dependencies.ts b/packages/snack-cli/src/lib/load-dependencies.ts new file mode 100644 index 00000000..3007b594 --- /dev/null +++ b/packages/snack-cli/src/lib/load-dependencies.ts @@ -0,0 +1,16 @@ +import { SnackDependencies } from 'snack-sdk'; +import * as path from 'node:path'; +import { readJsonAsync } from './read-json'; +import Debug from 'debug'; + +const debug = Debug('snack-cli:load-dependencies'); + +export async function loadDependencies(cwd: string): Promise { + const packageJson: { dependencies?: { [name: string]: string } } = await readJsonAsync(path.join(cwd, 'package.json')) ?? {}; + const dependencies = Object.entries(packageJson?.dependencies ?? {}).reduce((acc, [name, version]) => { + acc[name] = { version: "*" }; // TODO: Handle versions. + return acc; + }, {} as SnackDependencies); + debug('Loaded dependencies:', dependencies); + return dependencies; +} diff --git a/packages/snack-cli/src/lib/load-files.ts b/packages/snack-cli/src/lib/load-files.ts new file mode 100644 index 00000000..78190694 --- /dev/null +++ b/packages/snack-cli/src/lib/load-files.ts @@ -0,0 +1,47 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { Snack, SnackFile } from 'snack-sdk'; +import { glob } from 'glob'; + +import { isAsset } from './path-is'; + +// Inspired by Snack website blacklist +// https://github.com/expo/snack/blob/317f3fbb1d6b6074884623c84f7c24a3fbfee013/website/src/client/utils/convertDataTransferItemsToFiles.tsx#L26 +export const IGNORE_DIRS = ['node_modules', '.git', '.snack', 'build', 'dist']; + +export async function loadFiles(dir: string) { + const files: { [path: string]: SnackFile } = {}; + + const filePaths = await glob('**/*', { + cwd: dir, + ignore: IGNORE_DIRS.map(d => `${d}/**`), + nodir: true, + }); + + for (const filePath of filePaths) { + const fullPath = path.join(dir, filePath); + let file: SnackFile | null = null; + try { + if (isAsset(filePath)) { + file = { + type: 'ASSET', + contents: new File([await fs.readFile(fullPath)], filePath), + }; + } else { + file = { + type: 'CODE', + contents: await fs.readFile(fullPath, { encoding: 'utf-8' }), + }; + } + } catch (error) { + console.error(`Failed to read file ${fullPath}`, error); + continue; + } + + if (file) { + files[filePath] = file; + } + } + + return files; +}; diff --git a/packages/snack-cli/src/lib/parse-json.ts b/packages/snack-cli/src/lib/parse-json.ts new file mode 100644 index 00000000..72552686 --- /dev/null +++ b/packages/snack-cli/src/lib/parse-json.ts @@ -0,0 +1,12 @@ +import Debug from 'debug'; + +const debug = Debug('snack-cli:parse-json'); + +export function parseJson(contents: string): T | null { + try { + return JSON.parse(contents); + } catch (error) { + debug(`Failed to parse ${contents}`, error); + return null; + } +} diff --git a/packages/snack-cli/src/lib/path-is.ts b/packages/snack-cli/src/lib/path-is.ts new file mode 100644 index 00000000..a76540a7 --- /dev/null +++ b/packages/snack-cli/src/lib/path-is.ts @@ -0,0 +1,18 @@ +// Inspired by Snack website file utilities +// https://github.com/expo/snack/blob/317f3fbb1d6b6074884623c84f7c24a3fbfee013/website/src/client/utils/fileUtilities.tsx#L66 + +export function isScript(name: string): boolean { + return /\.(js|tsx?)$/.test(name); +} + +export function isJson(name: string): boolean { + return name.endsWith('.json'); +} + +export function isMarkdown(name: string): boolean { + return name.endsWith('.md'); +} + +export function isAsset(name: string): boolean { + return !(isScript(name) || isJson(name) || isMarkdown(name)); +} diff --git a/packages/snack-cli/src/lib/random-name.ts b/packages/snack-cli/src/lib/random-name.ts new file mode 100644 index 00000000..6c2a233f --- /dev/null +++ b/packages/snack-cli/src/lib/random-name.ts @@ -0,0 +1,3 @@ +export function randomName(): string { + return Math.random().toString(36).substring(2, 15); +} diff --git a/packages/snack-cli/src/lib/read-json.ts b/packages/snack-cli/src/lib/read-json.ts new file mode 100644 index 00000000..038df39f --- /dev/null +++ b/packages/snack-cli/src/lib/read-json.ts @@ -0,0 +1,18 @@ +import { promises as fs } from 'node:fs'; +import Debug from 'debug'; + +const debug = Debug('snack-cli:read-json'); + +export async function readJsonAsync(path: string): Promise { + try { + const contents = await fs.readFile(path, 'utf-8'); + return JSON.parse(contents); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + debug(`${path} does not exist`); + } else { + debug(`Failed to parse ${path}`, error); + } + return null; + } +} diff --git a/packages/snack-cli/src/lib/snack-runtime.ts b/packages/snack-cli/src/lib/snack-runtime.ts new file mode 100644 index 00000000..cb3d592c --- /dev/null +++ b/packages/snack-cli/src/lib/snack-runtime.ts @@ -0,0 +1,16 @@ +import { env } from 'node:process'; + +// Default SDK 54 (and older) runtime endpoint +import { SNACK_RUNTIME_URL_ENDPOINT as SNACK_DEFAULT_RUNTIME_URL_ENDPOINT } from 'snack-content'; + +// Deployed via Expo snack-experimental-runtime project +// https://github.com/expo/snack/commit/c9a1a296c4a846184333f6a3182754d78bdafd75 +export const SNACK_SDK_55_CANARY_RUNTIME_ENDPOINT = 'u.expo.dev/98eba87d-6d91-4756-8699-dd512ce803d3'; + +export function getRuntimeEndpoint({ + experimentalRuntime = ['1', 'true'].includes(env.SNACK_CLI_EXPERIMENTAL_RUNTIME ?? ''), +}: { + experimentalRuntime?: boolean; +}): string { + return experimentalRuntime ? SNACK_SDK_55_CANARY_RUNTIME_ENDPOINT : SNACK_DEFAULT_RUNTIME_URL_ENDPOINT; +} diff --git a/packages/snack-cli/src/lib/url-params.ts b/packages/snack-cli/src/lib/url-params.ts new file mode 100644 index 00000000..300706ca --- /dev/null +++ b/packages/snack-cli/src/lib/url-params.ts @@ -0,0 +1,6 @@ + +export function withExtraParams(url: string) { + const urlObject = new URL(url); + urlObject.searchParams.set('project-type', 'echo'); + return urlObject.toString(); +} diff --git a/packages/snack-cli/src/lib/write-json.ts b/packages/snack-cli/src/lib/write-json.ts new file mode 100644 index 00000000..cfb3b921 --- /dev/null +++ b/packages/snack-cli/src/lib/write-json.ts @@ -0,0 +1,25 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import Debug from 'debug'; + +const debug = Debug('snack-cli:write-json'); + +export async function writeJsonAsync(file: string, data: T): Promise { + const dir = path.dirname(file); + + // Ensure directory exists + try { + await fs.promises.mkdir(dir, { recursive: true }); + } catch (error) { + debug(`Failed to create directory ${dir}`, error); + throw error; + } + + // Write file + try { + await fs.promises.writeFile(file, JSON.stringify(data, null, 2), { encoding: 'utf-8' }); + } catch (error) { + debug(`Failed to write ${path}`, error); + throw error; + } +} diff --git a/packages/snack-cli/tsconfig.json b/packages/snack-cli/tsconfig.json new file mode 100644 index 00000000..a0d8ad2c --- /dev/null +++ b/packages/snack-cli/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"] +} diff --git a/packages/snack-sdk/src/Session.ts b/packages/snack-sdk/src/Session.ts index 4f6c2e8a..51ee0d45 100644 --- a/packages/snack-sdk/src/Session.ts +++ b/packages/snack-sdk/src/Session.ts @@ -1314,6 +1314,14 @@ export default class Snack { status: 'error', }), })); + + const event: SnackLogEvent = { + type: 'error', + connectedClient: this.state.connectedClients[connectedClientId], + message: error.message, + error, + }; + this.logListeners.forEach((listener) => listener(event)); } catch { this.logger?.error('Failed to parse received error message', message); } diff --git a/smart-red-mixed-nuts/.gitignore b/smart-red-mixed-nuts/.gitignore new file mode 100644 index 00000000..d914c328 --- /dev/null +++ b/smart-red-mixed-nuts/.gitignore @@ -0,0 +1,41 @@ +# 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 + +# generated native folders +/ios +/android diff --git a/smart-red-mixed-nuts/.snack/snack.json b/smart-red-mixed-nuts/.snack/snack.json new file mode 100644 index 00000000..764edd7b --- /dev/null +++ b/smart-red-mixed-nuts/.snack/snack.json @@ -0,0 +1,4 @@ +{ + "snackId": "@krystofwoldrich/7ing4xzj0u9", + "name": "nypclzdflqg" +} \ No newline at end of file diff --git a/smart-red-mixed-nuts/App.js b/smart-red-mixed-nuts/App.js new file mode 100644 index 00000000..11b093c1 --- /dev/null +++ b/smart-red-mixed-nuts/App.js @@ -0,0 +1,35 @@ +import { StyleSheet, Text, View } from 'react-native'; + +// You can import supported modules from npm +import { Card } from 'react-native-paper'; + +// or any files within the Snack +import AssetExample from './components/AssetExample'; + +export default function App() { + return ( + + + Change 4 + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + backgroundColor: '#ecf0f1', + padding: 8, + }, + paragraph: { + margin: 24, + fontSize: 18, + fontWeight: 'bold', + textAlign: 'center', + }, +}); diff --git a/smart-red-mixed-nuts/README.md b/smart-red-mixed-nuts/README.md new file mode 100644 index 00000000..f3a6949b --- /dev/null +++ b/smart-red-mixed-nuts/README.md @@ -0,0 +1,11 @@ +# Sample Snack app + +Open the `App.js` file to start writing some code. You can preview the changes directly on your phone or tablet by scanning the **QR code** or use the iOS or Android emulators. When you're done, click **Save** and share the link! + +When you're ready to see everything that Expo provides (or if you want to use your own editor) you can **Download** your project and use it with [expo cli](https://docs.expo.dev/get-started/installation/#expo-cli)). + +All projects created in Snack are publicly available, so you can easily share the link to this project via link, or embed it on a web page with the `<>` button. + +If you're having problems, you can tweet to us [@expo](https://twitter.com/expo) or ask in our [forums](https://forums.expo.dev/c/expo-dev-tools/61) or [Discord](https://chat.expo.dev/). + +Snack is Open Source. You can find the code on the [GitHub repo](https://github.com/expo/snack). diff --git a/smart-red-mixed-nuts/app.json b/smart-red-mixed-nuts/app.json new file mode 100644 index 00000000..c8daf2f4 --- /dev/null +++ b/smart-red-mixed-nuts/app.json @@ -0,0 +1,29 @@ +{ + "expo": { + "name": "smart-red-mixed-nuts", + "slug": "snack-80798448-5dd3-4fd1-ac45-5425423ccca4", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "newArchEnabled": true, + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "edgeToEdgeEnabled": true + }, + "web": { + "favicon": "./assets/favicon.png" + } + } +} \ No newline at end of file diff --git a/smart-red-mixed-nuts/assets/adaptive-icon.png b/smart-red-mixed-nuts/assets/adaptive-icon.png new file mode 100644 index 00000000..03d6f6b6 Binary files /dev/null and b/smart-red-mixed-nuts/assets/adaptive-icon.png differ diff --git a/smart-red-mixed-nuts/assets/favicon.png b/smart-red-mixed-nuts/assets/favicon.png new file mode 100644 index 00000000..e75f697b Binary files /dev/null and b/smart-red-mixed-nuts/assets/favicon.png differ diff --git a/smart-red-mixed-nuts/assets/icon.png b/smart-red-mixed-nuts/assets/icon.png new file mode 100644 index 00000000..a0b1526f Binary files /dev/null and b/smart-red-mixed-nuts/assets/icon.png differ diff --git a/smart-red-mixed-nuts/assets/snack-icon.png b/smart-red-mixed-nuts/assets/snack-icon.png new file mode 100644 index 00000000..cce03a3b Binary files /dev/null and b/smart-red-mixed-nuts/assets/snack-icon.png differ diff --git a/smart-red-mixed-nuts/assets/splash-icon.png b/smart-red-mixed-nuts/assets/splash-icon.png new file mode 100644 index 00000000..03d6f6b6 Binary files /dev/null and b/smart-red-mixed-nuts/assets/splash-icon.png differ diff --git a/smart-red-mixed-nuts/components/AssetExample.js b/smart-red-mixed-nuts/components/AssetExample.js new file mode 100644 index 00000000..24e5ef0d --- /dev/null +++ b/smart-red-mixed-nuts/components/AssetExample.js @@ -0,0 +1,31 @@ +import { Text, View, StyleSheet, Image } from 'react-native'; + +export default function AssetExample() { + return ( + + + Local files and assets can be imported by dragging and dropping them into the editor + + + + ); +} + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + justifyContent: 'center', + padding: 24, + }, + paragraph: { + margin: 24, + marginTop: 0, + fontSize: 14, + fontWeight: 'bold', + textAlign: 'center', + }, + logo: { + height: 128, + width: 128, + } +}); diff --git a/smart-red-mixed-nuts/package.json b/smart-red-mixed-nuts/package.json new file mode 100644 index 00000000..0598ee97 --- /dev/null +++ b/smart-red-mixed-nuts/package.json @@ -0,0 +1,19 @@ +{ + "license": "0BSD", + "main": "index.js", + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web" + }, + "dependencies": { + "expo": "~54.0.27", + "expo-status-bar": "~3.0.9", + "react": "19.1.0", + "react-native": "0.81.5", + "react-native-paper": "4.9.2", + "@expo/vector-icons": "^15.0.3" + }, + "private": true +} \ No newline at end of file diff --git a/test-snack/.snack/snack.json b/test-snack/.snack/snack.json new file mode 100644 index 00000000..458e84e1 --- /dev/null +++ b/test-snack/.snack/snack.json @@ -0,0 +1,3 @@ +{ + "snackId": "a802fb5b-87b5-49f9-9c79-7b969c8f77e7" +} \ No newline at end of file diff --git a/test-snack/App.js b/test-snack/App.js new file mode 100644 index 00000000..436fc268 --- /dev/null +++ b/test-snack/App.js @@ -0,0 +1,28 @@ +import { StyleSheet, Text, View } from 'react-native'; + +export default function App() { + console.log('Hello Snack Updated!'); + + return ( + + + Hello Snack Updated! + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + backgroundColor: '#ecf0f1', + padding: 8, + }, + paragraph: { + margin: 24, + fontSize: 18, + fontWeight: 'bold', + textAlign: 'center', + }, +}); diff --git a/test-snack/package.json b/test-snack/package.json new file mode 100644 index 00000000..01782700 --- /dev/null +++ b/test-snack/package.json @@ -0,0 +1 @@ +{"name":"test-snack","version":"1.0.0"} diff --git a/yarn.lock b/yarn.lock index f5fbeea4..45484fc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1950,6 +1950,18 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -2326,6 +2338,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@pkgr/utils@^2.3.1": version "2.4.2" resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.2.tgz#9e638bbe9a6a6f165580dc943f138fd3309a2cbc" @@ -2719,6 +2736,13 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== +"@types/debug@^4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/diff@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/diff/-/diff-4.0.2.tgz#2e9bb89f9acc3ab0108f0f3dc4dbdcf2fff8a99c" @@ -2841,6 +2865,14 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/glob@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.1.0.tgz#b63e70155391b0584dce44e7ea25190bbc38f2fc" + integrity sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w== + dependencies: + "@types/minimatch" "^5.1.2" + "@types/node" "*" + "@types/graceful-fs@^4.1.3": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" @@ -3100,11 +3132,21 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/minimatch@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + "@types/minimist@^1.2.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + "@types/node-fetch@^2.5.7": version "2.5.7" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" @@ -3135,6 +3177,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.26.tgz#63d204d136c9916fb4dcd1b50f9740fe86884e47" integrity sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ== +"@types/node@^20.10.5": + version "20.19.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.25.tgz#467da94a2fd966b57cc39c357247d68047611190" + integrity sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ== + dependencies: + undici-types "~6.21.0" + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -4132,6 +4181,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== + ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -4151,6 +4205,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.1.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + ansi-to-html@^0.6.14: version "0.6.14" resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8" @@ -4804,6 +4863,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -5585,6 +5651,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== + commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -5882,6 +5953,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypt@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" @@ -6059,6 +6139,13 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: dependencies: ms "2.1.2" +debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -6397,6 +6484,11 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" +dotenv@^16.3.1: + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + dtrace-provider@~0.8: version "0.8.8" resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" @@ -6450,6 +6542,11 @@ dynamic-dedupe@^0.3.0: dependencies: xtend "^4.0.0" +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" @@ -6505,6 +6602,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" @@ -7614,6 +7716,14 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + form-data@^2.4.0, form-data@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" @@ -8005,6 +8115,18 @@ glob@7.1.6, glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^10.3.10: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^6.0.1: version "6.0.4" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" @@ -9415,6 +9537,15 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jest-changed-files@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" @@ -10434,6 +10565,11 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -10778,6 +10914,13 @@ minimatch@^3.0.2, minimatch@^3.0.5, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -10797,6 +10940,11 @@ minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -10880,6 +11028,11 @@ ms@2.1.2, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + mv@~2: version "2.1.1" resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" @@ -11450,6 +11603,11 @@ pac-resolver@^5.0.0: ip "^1.1.5" netmask "^2.0.2" +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + package-json@^6.3.0: version "6.5.0" resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" @@ -11640,6 +11798,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -13521,6 +13687,11 @@ signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + simple-plist@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.1.1.tgz#54367ca28bc5996a982c325c1c4a4c1a05f4047c" @@ -13891,6 +14062,15 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -13918,6 +14098,15 @@ string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string.prototype.matchall@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" @@ -14004,6 +14193,13 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -14025,6 +14221,13 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -14846,6 +15049,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -15520,6 +15728,15 @@ worker-plugin@^3.0.0: dependencies: loader-utils "^1.1.0" +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -15547,6 +15764,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"