README
Snack SDK
The Expo Snack SDK. Use this to create a custom web interface for https://snack.expo.io/. For brevity, these docs assume you are targeting the most recent version of Expo SDK
Have a problem with the code in this repository? Please file issues & bug reports at https://github.com/expo/expo. Thanks!
Interested in integrating the Snack SDK into your project? Check out https://forums.expo.io/c/snack for assistance and to receive updates about any upgrades to the SDK.
Documentation
User Guide
Creating a new session
import { SnackSession } from 'snack-sdk';
let session = new SnackSession({
files?: ExpoSnackFiles,
sdkVersion?: SDKVersion,
sessionId?: string,
verbose?: boolean,
});
await session.startAsync();
files
is a map of all of the files included in the project. The filenames should be the full path from the project root.
sdkVersion
determines what version of React Native is used on the mobile client. Defaults to 15.0.0
which maps to React Native 0.42.0. If you specify a different version, make sure to save that version along with the code. Code from one SDK version is not guaranteed to work on others. To use multiple files, you must use SDK 21 or above.
sessionId
can be specified if you want a consistent url. This is a global namespace so make sure to use a UUID or scope it somehow if you use this.
Connecting to your user's phone
Getting the URL for the mobile client
let url = await session.getUrlAsync();
This url will open the current Snack Session in the Expo client when opened on a phone. You can create a QR code from this link or send it to the phone in another way. See example/
for how to turn this into a QR code.
Recently in Development
The Expo App includes an ID displayed at the bottom of the Projects tab. Once this ID has been reported to the session, your project will appear in the "Recently in Development" section on the Projects tab for as long as the session remains active.
Example:
session.setDeviceId('XXXX-XXXX')
Updating the code
const files = {
'App.js': { contents: 'code here, this is entry point', type: 'CODE'},
'folder/file.js': { contents: 'this file is in /folder', type: 'CODE'},
'image.png': { content: 'remote location of asset', type: 'ASSET'},
}
await session.sendCodeAsync(files: Object);
This will push the new code to each connected mobile client. Any new clients that connect will also get the new code.
For SDK 21 and above, you can have multiple files in a Snack. This includes support for folders and relative path imports. See the example above on sending data. The entry point for running a Snack is app.js
, which is required to be included in any project provided to the SnackSession constructor or sendCodeAsync calls.
You'll also be able to send Assets (images, fonts, etc.). To do this include the asset in the files object, with key being file name and value being the remote location where this asset is stored.
Uploading assets
const remoteAddressOfAssetFile = await session.uploadAssetAsync(file);
where file is a Javascript File
object.
Dependencies
When using Exp SDK 25 and above, you'll be able to specify arbitrary NPM modules to use with your app.
Example:
await session.addDependencies({"lodash": "4.17.10"}); // add a specific version
await session.addDependencies({"lodash": "*"}); // or resolve to the most recent
To detect all dependencies needed to evaluate a file,
Example:
import { dependencyUtils } from 'snack-sdk';
let dependencies = Object.keys(dependencyUtils.findModuleDependencies('import lodash from "lodash";'));
// ['lodash']
Differences between dependencies in Snack and npm
When developing a project on your local file system, you are probably used to:
- Adding the dependency you want to you package.json
- Running
npm install
which will copy the published package to your local filesystem - Importing or requiring any files included with that package into your project
- Trusting your bundler to remove any unneeded code before publishing your project
- Evaluating the project, along with any code from referenced dependencies, on a device
which means that once we add lodash to our dependencies these are the same
import zip from 'lodash/zip';
import { zip } from 'lodash';
With Snack, we have prioritized taking an arbitrary dependency and have it available in the running experience as quickly as possible. To achieve that, only the parts of the package that are exported in the module's main entry point are available. Tthe process looks like:
- Adding the dependency to snack's :
session.addDependency({'my-package': 'version'})
- Bundling and minimizing the dependency
- Evaluating the project code, along side any referenced dependencies
which means that
import zip from 'lodash/zip';
is not available once you import lodash
You will need to import 'lodash/zip' to a new package, which will give you a package identical to lodash-zip
Saving the code to Expo's servers
let saveResult = await session.saveAsync();
console.log(saveResult);
// This will print: `{"id":"abc123","url":"https://expo.io/@snack/abc123"}`
This will upload the current code to Expo's servers and return a url that points to that version of the code.
Downloading Code
const downloadURL = await session.downloadAsync();
console.log(downloadURL);
// This will print: { url: "https://expo.io/--/api/v2/snack/download/snackIDHere" }
This will return a link to our server with a .zip
of your Snack project. You'll be able to run this exported project using exp
or XDE.
Listening for events
Here are the Flow types for the error, log, and presence listeners:
type SnackSession = {
addErrorListener: (listener: ExpoErrorListener) => ExpoSubscription,
addLogListener: (listener: ExpoLogListener) => ExpoSubscription,
addPresenceListener: (listener: ExpoPresenceListener) => ExpoSubscription,
};
type ExpoSubscription = {
remove: () => void,
};
// Called with empty array if errors have been resolved
type ExpoErrorListener = (errors: Array<ExpoError>) => void;
type ExpoLogListener = (log: ExpoDeviceLog) => void;
type ExpoPresenceListener = (event: ExpoPresenceEvent) => void;
type ExpoError = {
device?: ExpoDevice,
startLine?: number,
endLine?: number,
startColumn?: number,
endColumn?: number,
message: string,
stack?: string,
};
// `console.log`, `console.warn`, `console.error`
type ExpoDeviceLog = {
device: ExpoDevice,
method: 'log' | 'warn' | 'error',
message: string,
arguments: any, // the raw fields that were passed to the console.* call
};
type ExpoDevice = {
name: string,
id: string,
};
type ExpoPresenceStatus = 'join' | 'leave';
type ExpoPresenceEvent = {
device: ExpoDevice,
status: ExpoPresenceStatus,
};
Each of these listeners is optional. Here's an example of using a log listener:
let logSubscription = session.addLogListener((log) => {
console.log(JSON.stringify(log));
// This will print: `{"device":{"id":"b9384faf-504f-4c41-a7ab-6344f0102456","name":"SM-G930U"},"method":"log","message":"hello!","arguments":["hello!"]}`
// on the web if `console.log('hello!')` is run from the code on the phone.
});
// later on...
logSubscription.remove();
// future `console.log`s on the phone will not trigger the listener
An error listener:
let errorSubscription = session.addErrorListener((error) => {
console.log(JSON.stringify(error));
// This will print:
// `[]`
// when there is no error and
// `[{"message":"unknown: Unexpected token (7:7)...","endLine":7,"startLine":7,"endColumn":7,"startColumn":7}]`
// when there is an error. The `message` field is truncated in this document.
});
// later on...
errorSubscription.remove();
A presence listener:
let presenceSubscription = session.addPresenceListener((presence) => {
console.log(JSON.stringify(presence));
// This will print:
// `{"device":{"id":"b9384faf-504f-4c41-a7ab-6344f0102456","name":"SM-G930U"},"status":"join"}`
// when a device is connected and
// `{"device":{"id":"b9384faf-504f-4c41-a7ab-6344f0102456","name":"SM-G930U"},"status":"leave"}`
// when a device disconnects.
});
// later on...
presenceSubscription.remove();
Please read the Flow types above for all possible fields returned in these listeners.
Stopping the session
await session.stopAsync();
This will shut down the PubNub connection.