Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ snackpub/build

# dotenv
.env*local

runtime/dist-*
4 changes: 4 additions & 0 deletions packages/snack-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@
"expo-camera": "~55.0.0",
"expo-constants": "~55.0.0",
"expo-crypto": "~55.0.0",
"expo-device": "~55.0.0",
"expo-file-system": "~55.0.0",
"expo-haptics": "~55.0.0",
"expo-keep-awake": "~55.0.0",
"expo-splash-screen": "~55.0.0",
"expo-status-bar": "~55.0.0",
Expand All @@ -72,7 +74,9 @@
"expo-camera": "~55.0.0",
"expo-constants": "~55.0.0",
"expo-crypto": "~55.0.0",
"expo-device": "~55.0.0",
"expo-file-system": "~55.0.0",
"expo-haptics": "~55.0.0",
"expo-keep-awake": "~55.0.0",
"expo-splash-screen": "~55.0.0",
"expo-status-bar": "~55.0.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/snack-runtime/src/aliases/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const aliases: { [key: string]: any } = {
// Renamed `@react-native-community` packages
'@react-native-community/async-storage': require('@react-native-async-storage/async-storage'),

// Common Expo packages preloaded for immediate use (no snackager fetch needed)
'expo-device': require('expo-device'),
'expo-haptics': require('expo-haptics'),

// Common packages that are included for easy of use
'prop-types': require('prop-types'),

Expand Down
17 changes: 17 additions & 0 deletions packages/snack-runtime/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4929,6 +4929,13 @@ expo-crypto@~55.0.0:
resolved "https://registry.yarnpkg.com/expo-crypto/-/expo-crypto-55.0.3.tgz#1a6a57c612497d8c6afe6a917cbead7caf87e77f"
integrity sha512-05gfOjhhCgAX3WX+uCNfdlx9IQawOftXIPfOwhG01s9LcjJzks8WK86rWH3E8DsMLKpWBbJ/RaG0X38xbDfxkg==

expo-device@~55.0.0:
version "55.0.16"
resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-55.0.16.tgz#56ecc44152036f1ce3c95a65e723fbc941f12d63"
integrity sha512-o6eQjO2reoniXpos0FnPcrAVMYUfFPcIUdMRUUpKwQys7cmTJBjJLbOo+SuctVXUrsHUm6zyoKI7nX3C3lpqJw==
dependencies:
ua-parser-js "^0.7.33"

expo-file-system@~55.0.0:
version "55.0.3"
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-55.0.3.tgz#6c63c0160fd1a2541320e2c88ce3d085511fcd2b"
Expand All @@ -4946,6 +4953,11 @@ expo-font@~55.0.4:
dependencies:
fontfaceobserver "^2.1.0"

expo-haptics@~55.0.0:
version "55.0.14"
resolved "https://registry.yarnpkg.com/expo-haptics/-/expo-haptics-55.0.14.tgz#9532ba088ee7eae561ad0ef5552c78f33161998a"
integrity sha512-KjDItBsA9mi1f5nRwf8g1wOdfEcLHwvEdt5Jl1sMCDETR/homcGOl+F3QIiPOl/PRlbGVieQsjTtF4DGtHOj6g==

expo-keep-awake@~55.0.0:
version "55.0.2"
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-55.0.2.tgz#e7637231ebd4aef008ffe49df53df47317796862"
Expand Down Expand Up @@ -9269,6 +9281,11 @@ typescript@^5.9.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6"
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==

ua-parser-js@^0.7.33:
version "0.7.41"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.41.tgz#9f6dee58c389e8afababa62a4a2dc22edb69a452"
integrity sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==

unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
Expand Down
43 changes: 25 additions & 18 deletions runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,45 @@
"@babel/polyfill": "^7.8.3",
"@expo/vector-icons": "^15.0.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@shopify/react-native-skia": "2.4.18",
"@react-navigation/drawer": "^7.5.0",
"@react-navigation/native": "^7.1.8",
"assert": "^2.1.0",
"buffer": "^6.0.3",
"canvaskit-wasm": "0.38.0",
"diff": "^5.0.0",
"escape-string-regexp": "^5.0.0",
"expo": "^55.0.2",
"expo-asset": "~55.0.7",
"expo-camera": "~55.0.9",
"expo-constants": "~55.0.7",
"expo-crypto": "~55.0.8",
"expo-file-system": "~55.0.9",
"expo-font": "~55.0.4",
"expo-keep-awake": "~55.0.4",
"expo-linking": "~55.0.7",
"expo-router": "~55.0.2",
"expo-splash-screen": "~55.0.9",
"expo-status-bar": "~55.0.4",
"expo-updates": "~55.0.11",
"expo": "55.0.15",
"expo-asset": "~55.0.15",
"expo-camera": "~55.0.15",
"expo-constants": "~55.0.14",
"expo-crypto": "~55.0.14",
"expo-device": "~55.0.15",
"expo-file-system": "~55.0.16",
"expo-font": "~55.0.6",
"expo-haptics": "~55.0.14",
"expo-image": "~55.0.8",
"expo-keep-awake": "~55.0.6",
"expo-linear-gradient": "~55.0.13",
"expo-linking": "~55.0.13",
"expo-router": "~55.0.12",
"expo-splash-screen": "~55.0.18",
"expo-status-bar": "~55.0.5",
"expo-updates": "~55.0.20",
"expo-video": "~55.0.15",
"path": "^0.12.7",
"prop-types": "^15.7.2",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.2",
"react-native": "0.83.5",
"react-native-gesture-handler": "~2.30.0",
"react-native-pager-view": "8.0.0",
"react-native-reanimated": "~4.2.1",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
"react-native-view-shot": "~4.0.3",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.7.2",
"react-native-view-shot": "4.0.3",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.7.4",
"snack-babel-standalone": "file:../packages/snack-babel-standalone",
"snack-content": "file:../packages/snack-content",
"snack-require-context": "file:../packages/snack-require-context",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
diff --git a/node_modules/react-native/Libraries/ReactNative/requireNativeComponent.js b/node_modules/react-native/Libraries/ReactNative/requireNativeComponent.js
index e9be731..b834f63 100644
index b9c8d57..90ee128 100644
--- a/node_modules/react-native/Libraries/ReactNative/requireNativeComponent.js
+++ b/node_modules/react-native/Libraries/ReactNative/requireNativeComponent.js
@@ -29,4 +29,17 @@ const requireNativeComponent = <T>(uiViewClassName: string): HostComponent<T> =>
@@ -33,4 +33,17 @@ const requireNativeComponent = <T: {...}>(
getNativeComponentAttributes(uiViewClassName),
): any): HostComponent<T>);

-export default requireNativeComponent;
+/* export default requireNativeComponent; */
+
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js
index 95355d5..634bd25 100644
index 348831d..38d9578 100644
--- a/node_modules/react-native-web/dist/exports/Image/index.js
+++ b/node_modules/react-native-web/dist/exports/Image/index.js
@@ -117,7 +117,7 @@ function resolveAssetUri(source) {
@@ -119,7 +119,7 @@ function resolveAssetUri(source) {
scale = asset.scales.reduce((prev, curr) => Math.abs(curr - preferredScale) < Math.abs(prev - preferredScale) ? curr : prev);
}
var scaleSuffix = scale !== 1 ? "@" + scale + "x" : '';
Expand Down
9 changes: 8 additions & 1 deletion runtime/src/Messaging.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ export const init = (deviceId: string) => {
if (Platform.OS === 'web') {
transportClass = require('./transports/RuntimeTransportImplWebPlayer').default;
} else {
transportClass = require('./transports/RuntimeTransportImplSocketIO').default;
// Use direct native transport for embedded snacks (lessons, playground, demo)
// when available — avoids Snackpub WebSocket roundtrip entirely.
const Embedded = require('./transports/RuntimeTransportImplEmbedded');
if (Embedded.isAvailable()) {
transportClass = Embedded.default;
} else {
transportClass = require('./transports/RuntimeTransportImplSocketIO').default;
}
}
transport = new transportClass(device);
};
Expand Down
9 changes: 9 additions & 0 deletions runtime/src/aliases/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ const aliases: { [key: string]: any } = {
// Renamed `@react-native-community` packages
'@react-native-community/async-storage': require('@react-native-async-storage/async-storage'),

// Common Expo packages preloaded for immediate use (no snackager fetch needed)
'expo-device': require('expo-device'),
'expo-haptics': require('expo-haptics'),
'expo-image': require('expo-image'),
'expo-linear-gradient': require('expo-linear-gradient'),
'expo-status-bar': require('expo-status-bar'),
'expo-video': require('expo-video'),
'@shopify/react-native-skia': require('@shopify/react-native-skia'),

// Common packages that are included for easy of use
'prop-types': require('prop-types'),

Expand Down
103 changes: 103 additions & 0 deletions runtime/src/transports/RuntimeTransportImplEmbedded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { requireOptionalNativeModule } from 'expo-modules-core';
import type { EventSubscription } from 'expo-modules-core';

import type { Device, RuntimeMessagePayload, RuntimeTransport } from './RuntimeTransport';
import * as Logger from '../Logger';

type Listener = (payload: RuntimeMessagePayload) => void;

type SnackDirectTransportModule = {
isAvailable: boolean;
subscribe(channel: string): void;
unsubscribe(): void;
publish(message: object): void;
addListener(
eventName: 'onMessage',
listener: (event: { message: any }) => void,
): EventSubscription;
};

/**
* Native module lives in Expo Go.
*
* Contract this transport relies on:
* - `isAvailable` is a Property (not a Function), so `nativeModule.isAvailable === true` works.
* - `subscribe(channel)` synchronously emits the CODE message via `onMessage` —
* our listener must be registered first.
* - `publish(message)` is safe to call before `subscribe()` and after `unsubscribe()`
* (no-op or drop, must not throw).
* - `unsubscribe()` is idempotent and safe with no active subscription.
*/
const nativeModule =
requireOptionalNativeModule<SnackDirectTransportModule>('SnackDirectTransport');

/**
* Returns true when the SnackDirectTransport native module is present
* and an embedded session (lesson, playground, demo) is active.
*/
export function isAvailable(): boolean {
return nativeModule?.isAvailable === true;
}

/**
* Transport for embedded snacks in Expo Go.
*
* Communicates directly with the Expo Go kernel via the SnackDirectTransport
* native module — no Snackpub WebSocket involved. CODE is delivered
* synchronously on subscribe(), giving ~0ms latency.
*/
export default class RuntimeTransportImplEmbedded implements RuntimeTransport {
private readonly device: Device;
private readonly listeners: Listener[] = [];
private subscription: { remove(): void } | null = null;

constructor(device: Device) {
this.device = device;
}

subscribe(channel: string) {
this.unsubscribe();

if (!nativeModule) {
Logger.comm_error('SnackDirectTransport native module not available');
return;
}

Logger.comm('Subscribing via embedded transport');

// Register event listener BEFORE calling native subscribe,
// so the immediately-delivered CODE message is received.
this.subscription = nativeModule.addListener('onMessage', (event) => {
this.listeners.forEach((listener) => listener({ message: event.message }));
});

// Native module immediately delivers CODE message via onMessage event
nativeModule.subscribe(channel);
}

unsubscribe() {
if (this.subscription) {
Logger.comm('Unsubscribing from embedded transport');
this.subscription.remove();
this.subscription = null;
}

nativeModule?.unsubscribe();
}

listen(listener: (payload: RuntimeMessagePayload) => void) {
this.listeners.push(listener);
}

publish(message: object) {
// For embedded snacks, most published messages are no-ops.
// Console logs go through LogBox, RESEND_CODE is unnecessary since
// CODE was already delivered via subscribe().
nativeModule?.publish({ ...message, device: this.device });
}

isConnected(): boolean {
// Embedded transport is always "connected" — it's in-process communication
return true;
}
}
Loading
Loading