@backtickjs/react-native-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
- package/src/index.ts +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jean-François Bisson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @backtickjs/react-native-client
|
|
2
|
+
|
|
3
|
+
Runs a [Backtick](https://backtickjs.com) server's screens in a React Native app. Your server runs its server components per request and answers with a bundle: one JavaScript file holding the screen's data and its compiled client scripts. `evaluate` runs that bundle with the app's own React and React Native and returns the screen to draw. It is what a web page's import map and script tag do for a browser, in under 1 KB.
|
|
4
|
+
|
|
5
|
+
Fetching the bundle stays with the app, on purpose: headers, auth, caching and when to fetch again are yours.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @backtickjs/react-native-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Give `evaluate` the modules a bundle may require, then fetch a screen and draw it. Hold the request, a promise, in state in a component that doesn't suspend, so it outlives the render that waits for it, and draw it with React's `use` under `<Suspense>`:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { evaluate } from "@backtickjs/react-native-client";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
import { Suspense, use, useState } from "react";
|
|
21
|
+
import * as JSXRuntime from "react/jsx-runtime";
|
|
22
|
+
import * as ReactNative from "react-native";
|
|
23
|
+
|
|
24
|
+
// What a screen may require: the packages this app was built with.
|
|
25
|
+
const modules = {
|
|
26
|
+
react: React,
|
|
27
|
+
"react/jsx-runtime": JSXRuntime,
|
|
28
|
+
"react-native": ReactNative,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function fetchScreen(url: string) {
|
|
32
|
+
const response = await fetch(url);
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`${url} answered ${response.status}.`);
|
|
35
|
+
}
|
|
36
|
+
return evaluate(await response.text(), modules) as React.ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function App() {
|
|
40
|
+
const [screen] = useState(() =>
|
|
41
|
+
fetchScreen("https://api.example.com/screens/home"),
|
|
42
|
+
);
|
|
43
|
+
return (
|
|
44
|
+
<Suspense fallback={<ReactNative.ActivityIndicator />}>
|
|
45
|
+
<Screen screen={screen} />
|
|
46
|
+
</Suspense>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Screen({ screen }: { screen: Promise<React.ReactNode> }) {
|
|
51
|
+
return use(screen);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Wrap the `<Suspense>` in an error boundary to show a screen that couldn't be fetched or run. To fetch again, set a new promise in state. The React Native template of [create-backtick-app](https://github.com/backtickjs/backtick/tree/main/packages/create-backtick-app) has the full version, with the boundary and a refetch whenever the development server restarts.
|
|
56
|
+
|
|
57
|
+
The server bundles for the versions the app runs: it passes them to `bundler.build` as `packageVersions`, so they should match the packages you put in `modules`.
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
### `evaluate(code: string, modules: Modules): unknown`
|
|
62
|
+
|
|
63
|
+
Runs `code` as a CommonJS module, with `require` answered from `modules`, and returns what it exports: for a Backtick screen, the element to draw. A bundle that requires a specifier missing from `modules` throws:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
The bundle requires "expo-haptics", which this app doesn't provide.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Errors thrown while the bundle runs propagate to the caller.
|
|
70
|
+
|
|
71
|
+
### `Modules`
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
type Modules = Readonly<Record<string, unknown>>;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
What a bundle may require, by specifier, each as its module namespace. At least `react`, `react/jsx-runtime` and `react-native`; add any other package the app chooses to offer its screens.
|
|
78
|
+
|
|
79
|
+
## Security
|
|
80
|
+
|
|
81
|
+
`evaluate` runs the bundle with `new Function`, which works on Hermes and JavaScriptCore. A bundle is code with the app's full abilities, so the server is trusted: serve screens over HTTPS from an endpoint you control.
|
|
82
|
+
|
|
83
|
+
## Related
|
|
84
|
+
|
|
85
|
+
- [`@backtickjs/core`](https://github.com/backtickjs/backtick/tree/main/packages/core): `cs` and the types for writing screens
|
|
86
|
+
- [`@backtickjs/bundler`](https://github.com/backtickjs/backtick/tree/main/packages/bundler): builds the bundles this runs
|
|
87
|
+
- [`@backtickjs/react-native`](https://github.com/backtickjs/backtick/tree/main/packages/react-native): React Native's components and APIs, to splice into scripts
|
|
88
|
+
- [`create-backtick-app`](https://github.com/backtickjs/backtick/tree/main/packages/create-backtick-app): an Expo app with a Backtick server beside it
|
|
89
|
+
- [Documentation](https://backtickjs.com/docs)
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a bundle may require, by specifier: the packages the app was built
|
|
3
|
+
* with, each as its module. At least React, React Native and React's JSX
|
|
4
|
+
* runtime:
|
|
5
|
+
*
|
|
6
|
+
* import * as React from "react";
|
|
7
|
+
* import * as JSXRuntime from "react/jsx-runtime";
|
|
8
|
+
* import * as ReactNative from "react-native";
|
|
9
|
+
*
|
|
10
|
+
* const modules = {
|
|
11
|
+
* "react": React,
|
|
12
|
+
* "react/jsx-runtime": JSXRuntime,
|
|
13
|
+
* "react-native": ReactNative,
|
|
14
|
+
* };
|
|
15
|
+
*/
|
|
16
|
+
export type Modules = Readonly<Record<string, unknown>>;
|
|
17
|
+
/**
|
|
18
|
+
* A bundle's code, run as CommonJS with `require` answered from `modules`:
|
|
19
|
+
* what it exports, the screen to draw. What a web page's import map and
|
|
20
|
+
* script tag do for a browser, this does for an app; fetching the bundle, and
|
|
21
|
+
* when, is the app's. A bundle requiring a module the app doesn't provide
|
|
22
|
+
* throws.
|
|
23
|
+
*/
|
|
24
|
+
export declare function evaluate(code: string, modules: Modules): unknown;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bundle's code, run as CommonJS with `require` answered from `modules`:
|
|
3
|
+
* what it exports, the screen to draw. What a web page's import map and
|
|
4
|
+
* script tag do for a browser, this does for an app; fetching the bundle, and
|
|
5
|
+
* when, is the app's. A bundle requiring a module the app doesn't provide
|
|
6
|
+
* throws.
|
|
7
|
+
*/
|
|
8
|
+
export function evaluate(code, modules) {
|
|
9
|
+
const module = { exports: {} };
|
|
10
|
+
const require = (specifier) => {
|
|
11
|
+
if (!Object.hasOwn(modules, specifier)) {
|
|
12
|
+
throw new Error(`The bundle requires "${specifier}", which this app doesn't provide.`);
|
|
13
|
+
}
|
|
14
|
+
return modules[specifier];
|
|
15
|
+
};
|
|
16
|
+
new Function("module", "exports", "require", code)(module, module.exports, require);
|
|
17
|
+
return module.exports;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,OAAgB;IACrD,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,EAAa,EAAE,CAAC;IAC1C,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAW,EAAE;QAC7C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;YACvC,MAAM,IAAI,KAAK,CACb,wBAAwB,SAAS,oCAAoC,CACtE,CAAC;QACJ,CAAC;QACD,OAAO,OAAO,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC,CAAC;IACF,IAAI,QAAQ,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAChD,MAAM,EACN,MAAM,CAAC,OAAO,EACd,OAAO,CACR,CAAC;IACF,OAAO,MAAM,CAAC,OAAO,CAAC;AACxB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@backtickjs/react-native-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Runs a Backtick server's screens in a React Native app, with the app's own packages",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"backtick",
|
|
7
|
+
"react-native",
|
|
8
|
+
"server-driven-ui",
|
|
9
|
+
"server-components",
|
|
10
|
+
"client",
|
|
11
|
+
"evaluate"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://backtickjs.com",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/backtickjs/backtick/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/backtickjs/backtick.git",
|
|
20
|
+
"directory": "packages/react-native-client"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Jean-François Bisson <jf@backtickjs.com>",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"!dist/._cache",
|
|
34
|
+
"src"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"prettier": "^3.6.2",
|
|
41
|
+
"typescript": "^6.0.3"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -b tsconfig.build.json",
|
|
45
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
46
|
+
"format": "prettier --write --ignore-unknown .",
|
|
47
|
+
"format:check": "prettier --check --ignore-unknown .",
|
|
48
|
+
"clean": "rm -rf node_modules dist .turbo"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a bundle may require, by specifier: the packages the app was built
|
|
3
|
+
* with, each as its module. At least React, React Native and React's JSX
|
|
4
|
+
* runtime:
|
|
5
|
+
*
|
|
6
|
+
* import * as React from "react";
|
|
7
|
+
* import * as JSXRuntime from "react/jsx-runtime";
|
|
8
|
+
* import * as ReactNative from "react-native";
|
|
9
|
+
*
|
|
10
|
+
* const modules = {
|
|
11
|
+
* "react": React,
|
|
12
|
+
* "react/jsx-runtime": JSXRuntime,
|
|
13
|
+
* "react-native": ReactNative,
|
|
14
|
+
* };
|
|
15
|
+
*/
|
|
16
|
+
export type Modules = Readonly<Record<string, unknown>>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A bundle's code, run as CommonJS with `require` answered from `modules`:
|
|
20
|
+
* what it exports, the screen to draw. What a web page's import map and
|
|
21
|
+
* script tag do for a browser, this does for an app; fetching the bundle, and
|
|
22
|
+
* when, is the app's. A bundle requiring a module the app doesn't provide
|
|
23
|
+
* throws.
|
|
24
|
+
*/
|
|
25
|
+
export function evaluate(code: string, modules: Modules): unknown {
|
|
26
|
+
const module = { exports: {} as unknown };
|
|
27
|
+
const require = (specifier: string): unknown => {
|
|
28
|
+
if (!Object.hasOwn(modules, specifier)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`The bundle requires "${specifier}", which this app doesn't provide.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return modules[specifier];
|
|
34
|
+
};
|
|
35
|
+
new Function("module", "exports", "require", code)(
|
|
36
|
+
module,
|
|
37
|
+
module.exports,
|
|
38
|
+
require,
|
|
39
|
+
);
|
|
40
|
+
return module.exports;
|
|
41
|
+
}
|