@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 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, 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>`:
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 * as JSXRuntime from "react/jsx-runtime";
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 modules = {
26
- react: React,
27
- "react/jsx-runtime": JSXRuntime,
28
- "react-native": ReactNative,
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(), modules) as React.ReactNode;
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={<ReactNative.ActivityIndicator />}>
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 `packageVersions`, so they should match the packages you put in `modules`.
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, modules: Modules): unknown`
59
+ ### `evaluate(code: string, moduleMap: ModuleMap): unknown`
62
60
 
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:
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
- ### `Modules`
69
+ ### `ModuleMap`
72
70
 
73
71
  ```ts
74
- type Modules = Readonly<Record<string, unknown>>;
72
+ type ModuleMap = Readonly<Record<string, () => unknown>>;
75
73
  ```
76
74
 
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.
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/packages/react-native): React Native's components and APIs, to splice into scripts
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 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:
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
- * 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,
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 Modules = Readonly<Record<string, unknown>>;
17
+ export type ModuleMap = Readonly<Record<string, () => unknown>>;
17
18
  /**
18
- * A bundle's code, run as CommonJS with `require` answered from `modules`:
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, modules: Modules): unknown;
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 `modules`:
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, modules) {
9
- const module = { exports: {} };
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(modules, specifier)) {
17
+ if (!Object.hasOwn(moduleMap, specifier)) {
12
18
  throw new Error(`The bundle requires "${specifier}", which this app doesn't provide.`);
13
19
  }
14
- return modules[specifier];
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":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtickjs/react-native-client",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Runs a Backtick server's screens in a React Native app, with the app's own packages",
5
5
  "keywords": [
6
6
  "backtick",
package/src/index.ts CHANGED
@@ -1,37 +1,49 @@
1
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:
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
- * 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,
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 Modules = Readonly<Record<string, unknown>>;
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 `modules`:
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, modules: Modules): unknown {
26
- const module = { exports: {} as unknown };
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(modules, specifier)) {
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
- return modules[specifier];
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
+ }