Using other packages
Any package your app ships, in a screen: createImport, versions, and the three places a package goes.
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:
import { cs } from "@backtickjs/core";import { Pressable, Text } from "@backtickjs/react-native";import { impactAsync, LinearGradient } from "./expo.js";export async function Home() {return cs`(<$LinearGradientcolors={["#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:
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:
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.