Using other packages

Any package your app ships, in a screen: createImport, versions, and the three places a package goes.

server/expo.ts
import { createImport } from "@backtickjs/core";
import type { impactAsync as ImpactAsync } from "expo-haptics";
import type { LinearGradient as ExpoLinearGradient } from "expo-linear-gradient";
// What the app provides from these packages, typed as the packages type
// them, for any script to splice.
export const impactAsync = createImport<typeof ImpactAsync>({
name: "impactAsync",
from: "expo-haptics",
version: "~57.0.0",
});
export const LinearGradient = createImport<typeof ExpoLinearGradient>({
name: "LinearGradient",
from: "expo-linear-gradient",
version: "~57.0.0",
});

A screen can use any package your app ships. createImport names an export of a package, typed as the package types it, and splices like anything else: $impactAsync(), <$LinearGradient>.

Three places, one package

A package a screen uses has to be in all three places:

1. The screen: spliced, as any value or component:

server/Home.tsx
import { cs } from "@backtickjs/core";
import { Pressable, Text } from "@backtickjs/react-native";
import { impactAsync, LinearGradient } from "./expo.js";
export async function Home() {
return cs`(
<$LinearGradient
colors={["#7c3aed", "#0891b2"]}
style={{ flex: 1, padding: 24 }}
>
<$Pressable onPress={() => $impactAsync()}>
<$Text style={{ color: "#fff", fontSize: 24 }}>Tap to feel it</$Text>
</$Pressable>
</$LinearGradient>
)`;
}

2. Your server: the versions the app provides, for the bundler to check against each import's range:

server/index.tsx
import { createServer } from "node:http";
import { createRequire } from "node:module";
import { bundler } from "@backtickjs/bundler";
import { Home } from "./Home.js";
// The packages the app provides to screens, each at the version it's built
// with.
const require = createRequire(import.meta.url);
const packageVersions = {
react: require("react/package.json").version,
"react-native": require("react-native/package.json").version,
"expo-haptics": require("expo-haptics/package.json").version,
"expo-linear-gradient": require("expo-linear-gradient/package.json").version,
};
createServer(async (request, response) => {
const bundle = await bundler.build({ input: <Home />, packageVersions });
response.setHeader("content-type", "text/javascript");
response.end(bundle.generate({ format: "cjs" }).code);
}).listen(3000);

3. Your app: the modules a screen may require, handed to evaluate:

App.tsx
import { evaluate } from "@backtickjs/react-native-client";
import * as ExpoHaptics from "expo-haptics";
import * as ExpoLinearGradient from "expo-linear-gradient";
import * as React from "react";
import * as JSXRuntime from "react/jsx-runtime";
import * as ReactNative from "react-native";
// What a screen may require: the packages this app was built with.
export const modules = {
react: React,
"react/jsx-runtime": JSXRuntime,
"react-native": ReactNative,
"expo-haptics": ExpoHaptics,
"expo-linear-gradient": ExpoLinearGradient,
};
export async function fetchScreen(url: string): Promise<React.ReactNode> {
const response = await fetch(url);
return evaluate(await response.text(), modules) as React.ReactNode;
}

Install the package in your app as usual, npx expo install expo-haptics, so it's built into the app and its native code ships with it.

Versions are checked

createImport's version is the range of the package that the export works with. When the bundle is built, the bundler checks it against what your server says the app provides, and refuses a screen the app can't run:

Can't import `impactAsync` from "expo-haptics": the client provides react@19.2.3, react-native@0.86.3.
Can't import `impactAsync` from "expo-haptics": it needs expo-haptics@~57.0.0, and the client provides expo-haptics@58.0.1.

The first is a package missing from packageVersions; the second, an app built with a version outside the range. Serving apps of several versions, give each its own packageVersions, and each gets a screen it can run.