@backtickjs/react-native-client 0.1.8 → 0.1.9
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/README.md +29 -17
- package/dist/index.d.ts +26 -14
- package/dist/index.js +34 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +51 -17
package/README.md
CHANGED
|
@@ -12,20 +12,18 @@ npm install @backtickjs/react-native-client
|
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
-
Give `evaluate` the modules a bundle may require,
|
|
15
|
+
Give `evaluate` the module map: the modules a bundle may require, each loaded the first time one does. 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
16
|
|
|
17
17
|
```tsx
|
|
18
18
|
import { evaluate } from "@backtickjs/react-native-client";
|
|
19
|
-
import * as React from "react";
|
|
20
19
|
import { Suspense, use, useState } from "react";
|
|
21
|
-
import
|
|
22
|
-
import * as ReactNative from "react-native";
|
|
20
|
+
import { ActivityIndicator } from "react-native";
|
|
23
21
|
|
|
24
22
|
// What a screen may require: the packages this app was built with.
|
|
25
|
-
const
|
|
26
|
-
react:
|
|
27
|
-
"react/jsx-runtime":
|
|
28
|
-
"react-native":
|
|
23
|
+
const moduleMap = {
|
|
24
|
+
react: () => require("react"),
|
|
25
|
+
"react/jsx-runtime": () => require("react/jsx-runtime"),
|
|
26
|
+
"react-native": () => require("react-native"),
|
|
29
27
|
};
|
|
30
28
|
|
|
31
29
|
async function fetchScreen(url: string) {
|
|
@@ -33,7 +31,7 @@ async function fetchScreen(url: string) {
|
|
|
33
31
|
if (!response.ok) {
|
|
34
32
|
throw new Error(`${url} answered ${response.status}.`);
|
|
35
33
|
}
|
|
36
|
-
return evaluate(await response.text(),
|
|
34
|
+
return evaluate(await response.text(), moduleMap) as React.ReactNode;
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
export default function App() {
|
|
@@ -41,7 +39,7 @@ export default function App() {
|
|
|
41
39
|
fetchScreen("https://api.example.com/screens/home"),
|
|
42
40
|
);
|
|
43
41
|
return (
|
|
44
|
-
<Suspense fallback={<
|
|
42
|
+
<Suspense fallback={<ActivityIndicator />}>
|
|
45
43
|
<Screen screen={screen} />
|
|
46
44
|
</Suspense>
|
|
47
45
|
);
|
|
@@ -54,13 +52,13 @@ function Screen({ screen }: { screen: Promise<React.ReactNode> }) {
|
|
|
54
52
|
|
|
55
53
|
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
54
|
|
|
57
|
-
The server bundles for the versions the app runs: it passes them to `bundler.build` as `
|
|
55
|
+
The server bundles for the versions the app runs: it passes them to `bundler.build` as `modules`, so they should match the packages in your module map.
|
|
58
56
|
|
|
59
57
|
## API
|
|
60
58
|
|
|
61
|
-
### `evaluate(code: string,
|
|
59
|
+
### `evaluate(code: string, moduleMap: ModuleMap): unknown`
|
|
62
60
|
|
|
63
|
-
Runs `code` as a CommonJS module, with `require` answered from `
|
|
61
|
+
Runs `code` as a CommonJS module, with `require` answered from `moduleMap`, and returns what it exports: for a Backtick screen, the element to draw. Each module loads the first time a bundle requires it, once for every bundle run with the same map. A bundle that requires a specifier missing from `moduleMap` throws:
|
|
64
62
|
|
|
65
63
|
```
|
|
66
64
|
The bundle requires "expo-haptics", which this app doesn't provide.
|
|
@@ -68,13 +66,27 @@ The bundle requires "expo-haptics", which this app doesn't provide.
|
|
|
68
66
|
|
|
69
67
|
Errors thrown while the bundle runs propagate to the caller.
|
|
70
68
|
|
|
71
|
-
### `
|
|
69
|
+
### `ModuleMap`
|
|
72
70
|
|
|
73
71
|
```ts
|
|
74
|
-
type
|
|
72
|
+
type ModuleMap = Readonly<Record<string, () => unknown>>;
|
|
75
73
|
```
|
|
76
74
|
|
|
77
|
-
What a bundle may require, by specifier, each as its module
|
|
75
|
+
What a bundle may require, by specifier, each as a function that returns its module. It does for an app what a web page's import map does, with modules for values instead of URLs. At least `react`, `react/jsx-runtime` and `react-native`; add any other package the app chooses to offer its screens. Keep the functions' `require` calls inside them: Metro still bundles each one, and Node tools can then read the specifiers without loading React Native.
|
|
76
|
+
|
|
77
|
+
### `buildIdFrom(extra: unknown, platform: string): string`
|
|
78
|
+
|
|
79
|
+
This build's id on `platform`, read from the app config's `extra.buildIds`, which an `app.config.ts` fills with [`@backtickjs/builds`](https://github.com/backtickjs/backtick/tree/main/packages/builds). The id names the modules the build carries, so the server bundles only screens it can run. Send it to the server however the app reaches it, as a header or a GraphQL variable:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { buildIdFrom } from "@backtickjs/react-native-client";
|
|
83
|
+
import Constants from "expo-constants";
|
|
84
|
+
import { Platform } from "react-native";
|
|
85
|
+
|
|
86
|
+
const buildId = buildIdFrom(Constants.expoConfig?.extra, Platform.OS);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
It throws when the config has no build id for the platform, as the app starts: no screen could be drawn for a build without one.
|
|
78
90
|
|
|
79
91
|
## Security
|
|
80
92
|
|
|
@@ -84,7 +96,7 @@ What a bundle may require, by specifier, each as its module namespace. At least
|
|
|
84
96
|
|
|
85
97
|
- [`@backtickjs/core`](https://github.com/backtickjs/backtick/tree/main/packages/core): `cs` and the types for writing screens
|
|
86
98
|
- [`@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/
|
|
99
|
+
- [`@backtickjs/react-native`](https://github.com/backtickjs/backtick/tree/main/adapters/react-native): React Native's components and APIs, to splice into scripts
|
|
88
100
|
- [`create-backtick-app`](https://github.com/backtickjs/backtick/tree/main/packages/create-backtick-app): an Expo app with a Backtick server beside it
|
|
89
101
|
- [How it works](https://backtickjs.com/docs/how-it-works)
|
|
90
102
|
- [Errors](https://backtickjs.com/docs/errors)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What a bundle may
|
|
3
|
-
*
|
|
4
|
-
* runtime:
|
|
2
|
+
* What a bundle may import, by specifier, and how this app loads each: a
|
|
3
|
+
* function returning the module, called the first time a bundle requires it.
|
|
4
|
+
* At least React, React Native and React's JSX runtime:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* const modules = {
|
|
11
|
-
* "react": React,
|
|
12
|
-
* "react/jsx-runtime": JSXRuntime,
|
|
13
|
-
* "react-native": ReactNative,
|
|
6
|
+
* const moduleMap = {
|
|
7
|
+
* react: () => require("react"),
|
|
8
|
+
* "react/jsx-runtime": () => require("react/jsx-runtime"),
|
|
9
|
+
* "react-native": () => require("react-native"),
|
|
14
10
|
* };
|
|
11
|
+
*
|
|
12
|
+
* What a web page's import map does, for an app: the values are modules,
|
|
13
|
+
* not URLs.
|
|
14
|
+
* Loaded lazily, so a module no screen uses never loads, and Node can read
|
|
15
|
+
* the specifiers without loading React Native.
|
|
15
16
|
*/
|
|
16
|
-
export type
|
|
17
|
+
export type ModuleMap = Readonly<Record<string, () => unknown>>;
|
|
17
18
|
/**
|
|
18
|
-
* A bundle's code, run as CommonJS with `require` answered from `
|
|
19
|
+
* A bundle's code, run as CommonJS with `require` answered from `moduleMap`:
|
|
19
20
|
* what it exports, the screen to draw. What a web page's import map and
|
|
20
21
|
* script tag do for a browser, this does for an app; fetching the bundle, and
|
|
21
22
|
* when, is the app's. A bundle requiring a module the app doesn't provide
|
|
22
23
|
* throws.
|
|
23
24
|
*/
|
|
24
|
-
export declare function evaluate(code: string,
|
|
25
|
+
export declare function evaluate(code: string, moduleMap: ModuleMap): unknown;
|
|
26
|
+
/**
|
|
27
|
+
* This build's id on `platform`, from the app config's `extra`, where an
|
|
28
|
+
* `app.config.ts` puts `buildIds` from `@backtickjs/builds`: it names the
|
|
29
|
+
* modules the build carries, for the server to bundle screens it can run.
|
|
30
|
+
* Send it however the app reaches its server: a header, a GraphQL variable.
|
|
31
|
+
* Throws for an app config without it, as the app starts: no screen could be
|
|
32
|
+
* drawn for a build without an id.
|
|
33
|
+
*
|
|
34
|
+
* const buildId = buildIdFrom(Constants.expoConfig?.extra, Platform.OS);
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildIdFrom(extra: unknown, platform: string): string;
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,48 @@
|
|
|
1
|
+
// Each map's modules, once loaded: one instance of each, as `require` gives.
|
|
2
|
+
const loaded = new WeakMap();
|
|
1
3
|
/**
|
|
2
|
-
* A bundle's code, run as CommonJS with `require` answered from `
|
|
4
|
+
* A bundle's code, run as CommonJS with `require` answered from `moduleMap`:
|
|
3
5
|
* what it exports, the screen to draw. What a web page's import map and
|
|
4
6
|
* script tag do for a browser, this does for an app; fetching the bundle, and
|
|
5
7
|
* when, is the app's. A bundle requiring a module the app doesn't provide
|
|
6
8
|
* throws.
|
|
7
9
|
*/
|
|
8
|
-
export function evaluate(code,
|
|
9
|
-
|
|
10
|
+
export function evaluate(code, moduleMap) {
|
|
11
|
+
let modules = loaded.get(moduleMap);
|
|
12
|
+
if (modules === undefined) {
|
|
13
|
+
modules = new Map();
|
|
14
|
+
loaded.set(moduleMap, modules);
|
|
15
|
+
}
|
|
10
16
|
const require = (specifier) => {
|
|
11
|
-
if (!Object.hasOwn(
|
|
17
|
+
if (!Object.hasOwn(moduleMap, specifier)) {
|
|
12
18
|
throw new Error(`The bundle requires "${specifier}", which this app doesn't provide.`);
|
|
13
19
|
}
|
|
14
|
-
|
|
20
|
+
if (!modules.has(specifier)) {
|
|
21
|
+
modules.set(specifier, moduleMap[specifier]());
|
|
22
|
+
}
|
|
23
|
+
return modules.get(specifier);
|
|
15
24
|
};
|
|
25
|
+
const module = { exports: {} };
|
|
16
26
|
new Function("module", "exports", "require", code)(module, module.exports, require);
|
|
17
27
|
return module.exports;
|
|
18
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* This build's id on `platform`, from the app config's `extra`, where an
|
|
31
|
+
* `app.config.ts` puts `buildIds` from `@backtickjs/builds`: it names the
|
|
32
|
+
* modules the build carries, for the server to bundle screens it can run.
|
|
33
|
+
* Send it however the app reaches its server: a header, a GraphQL variable.
|
|
34
|
+
* Throws for an app config without it, as the app starts: no screen could be
|
|
35
|
+
* drawn for a build without an id.
|
|
36
|
+
*
|
|
37
|
+
* const buildId = buildIdFrom(Constants.expoConfig?.extra, Platform.OS);
|
|
38
|
+
*/
|
|
39
|
+
export function buildIdFrom(extra, platform) {
|
|
40
|
+
const ids = extra
|
|
41
|
+
?.buildIds;
|
|
42
|
+
const id = ids?.[platform];
|
|
43
|
+
if (typeof id !== "string") {
|
|
44
|
+
throw new Error(`No build id for ${platform} in the app config: is app.config.ts adding buildIds from @backtickjs/builds?`);
|
|
45
|
+
}
|
|
46
|
+
return id;
|
|
47
|
+
}
|
|
19
48
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,6EAA6E;AAC7E,MAAM,MAAM,GAAG,IAAI,OAAO,EAAmC,CAAC;AAE9D;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,SAAoB;IACzD,IAAI,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC;QACpB,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAW,EAAE;QAC7C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CACb,wBAAwB,SAAS,oCAAoC,CACtE,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5B,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,CAAE,EAAE,CAAC,CAAC;QAClD,CAAC;QACD,OAAO,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAChC,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,EAAE,OAAO,EAAE,EAAa,EAAE,CAAC;IAC1C,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;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc,EAAE,QAAgB;IAC1D,MAAM,GAAG,GAAI,KAA4D;QACvE,EAAE,QAAQ,CAAC;IACb,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;IAC3B,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,mBAAmB,QAAQ,+EAA+E,CAC3G,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,37 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What a bundle may
|
|
3
|
-
*
|
|
4
|
-
* runtime:
|
|
2
|
+
* What a bundle may import, by specifier, and how this app loads each: a
|
|
3
|
+
* function returning the module, called the first time a bundle requires it.
|
|
4
|
+
* At least React, React Native and React's JSX runtime:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* const modules = {
|
|
11
|
-
* "react": React,
|
|
12
|
-
* "react/jsx-runtime": JSXRuntime,
|
|
13
|
-
* "react-native": ReactNative,
|
|
6
|
+
* const moduleMap = {
|
|
7
|
+
* react: () => require("react"),
|
|
8
|
+
* "react/jsx-runtime": () => require("react/jsx-runtime"),
|
|
9
|
+
* "react-native": () => require("react-native"),
|
|
14
10
|
* };
|
|
11
|
+
*
|
|
12
|
+
* What a web page's import map does, for an app: the values are modules,
|
|
13
|
+
* not URLs.
|
|
14
|
+
* Loaded lazily, so a module no screen uses never loads, and Node can read
|
|
15
|
+
* the specifiers without loading React Native.
|
|
15
16
|
*/
|
|
16
|
-
export type
|
|
17
|
+
export type ModuleMap = Readonly<Record<string, () => unknown>>;
|
|
18
|
+
|
|
19
|
+
// Each map's modules, once loaded: one instance of each, as `require` gives.
|
|
20
|
+
const loaded = new WeakMap<ModuleMap, Map<string, unknown>>();
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
|
-
* A bundle's code, run as CommonJS with `require` answered from `
|
|
23
|
+
* A bundle's code, run as CommonJS with `require` answered from `moduleMap`:
|
|
20
24
|
* what it exports, the screen to draw. What a web page's import map and
|
|
21
25
|
* script tag do for a browser, this does for an app; fetching the bundle, and
|
|
22
26
|
* when, is the app's. A bundle requiring a module the app doesn't provide
|
|
23
27
|
* throws.
|
|
24
28
|
*/
|
|
25
|
-
export function evaluate(code: string,
|
|
26
|
-
|
|
29
|
+
export function evaluate(code: string, moduleMap: ModuleMap): unknown {
|
|
30
|
+
let modules = loaded.get(moduleMap);
|
|
31
|
+
if (modules === undefined) {
|
|
32
|
+
modules = new Map();
|
|
33
|
+
loaded.set(moduleMap, modules);
|
|
34
|
+
}
|
|
27
35
|
const require = (specifier: string): unknown => {
|
|
28
|
-
if (!Object.hasOwn(
|
|
36
|
+
if (!Object.hasOwn(moduleMap, specifier)) {
|
|
29
37
|
throw new Error(
|
|
30
38
|
`The bundle requires "${specifier}", which this app doesn't provide.`,
|
|
31
39
|
);
|
|
32
40
|
}
|
|
33
|
-
|
|
41
|
+
if (!modules.has(specifier)) {
|
|
42
|
+
modules.set(specifier, moduleMap[specifier]!());
|
|
43
|
+
}
|
|
44
|
+
return modules.get(specifier);
|
|
34
45
|
};
|
|
46
|
+
const module = { exports: {} as unknown };
|
|
35
47
|
new Function("module", "exports", "require", code)(
|
|
36
48
|
module,
|
|
37
49
|
module.exports,
|
|
@@ -39,3 +51,25 @@ export function evaluate(code: string, modules: Modules): unknown {
|
|
|
39
51
|
);
|
|
40
52
|
return module.exports;
|
|
41
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* This build's id on `platform`, from the app config's `extra`, where an
|
|
57
|
+
* `app.config.ts` puts `buildIds` from `@backtickjs/builds`: it names the
|
|
58
|
+
* modules the build carries, for the server to bundle screens it can run.
|
|
59
|
+
* Send it however the app reaches its server: a header, a GraphQL variable.
|
|
60
|
+
* Throws for an app config without it, as the app starts: no screen could be
|
|
61
|
+
* drawn for a build without an id.
|
|
62
|
+
*
|
|
63
|
+
* const buildId = buildIdFrom(Constants.expoConfig?.extra, Platform.OS);
|
|
64
|
+
*/
|
|
65
|
+
export function buildIdFrom(extra: unknown, platform: string): string {
|
|
66
|
+
const ids = (extra as { buildIds?: Record<string, unknown> } | undefined)
|
|
67
|
+
?.buildIds;
|
|
68
|
+
const id = ids?.[platform];
|
|
69
|
+
if (typeof id !== "string") {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`No build id for ${platform} in the app config: is app.config.ts adding buildIds from @backtickjs/builds?`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return id;
|
|
75
|
+
}
|