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"