@forgebook/react-native-web 0.0.0-stage → 0.2.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ensiria
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 CHANGED
@@ -1,3 +1,191 @@
1
- # Temporary Holding Version
1
+ # @forgebook/react-native-web
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React Native for [Forgebook](https://www.npmjs.com/package/forgebook): a React Native project's components drawn in the
4
+ browser through [react-native-web](https://necolas.github.io/react-native-web/), on Vite, their props read from their
5
+ TypeScript types as React's are.
6
+
7
+ **Requirements:** React 18.2 or 19 · `react-native` 0.76 or later (its types; 0.88 is the one tested) ·
8
+ `react-native-web` 0.21 · `forgebook` · Vite 8 · [MIT licence](https://github.com/ensiria/forgebook/blob/develop/packages/react-native-web/LICENSE)
9
+
10
+ ```sh
11
+ npm install --save-dev forgebook @forgebook/react-native-web react-native-web
12
+ ```
13
+
14
+ `react-native` and `react-native-web` are peers: the project's own versions are the ones drawn.
15
+
16
+ ## A first catalogue
17
+
18
+ ```ts
19
+ // forgebook.config.ts
20
+ import { reactNativeWeb } from "@forgebook/react-native-web/vite";
21
+ import { defineConfig } from "forgebook";
22
+
23
+ export default defineConfig({
24
+ framework: reactNativeWeb(),
25
+ title: "My app",
26
+ stories: ["src/**/*.stories.tsx"]
27
+ });
28
+ ```
29
+
30
+ A story file is a React story file; the types come from this package:
31
+
32
+ ```tsx
33
+ import type { Meta, Story } from "@forgebook/react-native-web";
34
+
35
+ import { ProfileCard } from "./ProfileCard";
36
+
37
+ export default { title: "Crew/Profile card", component: ProfileCard } satisfies Meta<typeof ProfileCard>;
38
+
39
+ /** A member online. */
40
+ export const Online: Story<typeof ProfileCard> = { args: { name: "Ada", role: "First mate", isOnline: true } };
41
+ ```
42
+
43
+ The app's root providers go in the preview, around every story:
44
+
45
+ ```tsx
46
+ // forgebook.preview.tsx
47
+ import type { Preview } from "@forgebook/react-native-web";
48
+ import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context";
49
+
50
+ export default {
51
+ decorators: [
52
+ (Story) => (
53
+ <SafeAreaProvider initialMetrics={initialWindowMetrics}>
54
+ <Story />
55
+ </SafeAreaProvider>
56
+ )
57
+ ]
58
+ } satisfies Preview;
59
+ ```
60
+
61
+ A project on Storybook writes this configuration with `npx forgebook init --from storybook`: it reads
62
+ `@storybook/react-native-web-vite` (its `modulesToTranspile` and `pluginReactOptions` become the options below), the
63
+ older `@storybook/addon-react-native-web`, and on-device Storybook (`.rnstorybook`, or `.storybook` in a project on
64
+ `@storybook/react-native`).
65
+
66
+ ## What it does
67
+
68
+ What Metro does for a phone, Vite does for the catalogue:
69
+
70
+ | | |
71
+ |---|---|
72
+ | `react-native` | resolved to `react-native-web`, exactly: `react-native-svg` and the others keep their name |
73
+ | `.web.tsx`, `.web.ts`, `.web.jsx`, `.web.js`, `.web.mjs` | read before the plain file, as Metro does for the web; `.native` and `.ios` files are never read |
74
+ | JSX in `.js` files, Flow types | compiled in the project's own files and in the packages written for React Native (`react-native-*`, `@react-native/*`, `expo`, `expo-*`, `@expo/*`, and those named in `nativePackages`), on the dev server and in a build |
75
+ | `require("./logo.png")`, `require("./Font.ttf")` | the asset's address, which `Image` and Expo's `useFonts` take, in an ES module (a file that imports or exports; a CommonJS file keeps its `require`); the file is copied into a built catalogue |
76
+ | `__DEV__`, `process.env.EXPO_OS`, `global` | defined as Metro and Expo's Babel preset define them |
77
+ | `Platform.OS` | `"web"`, from react-native-web; `Platform.select` picks `web` |
78
+ | A screen's `flex: 1` | each story is drawn in a root laid out as React Native's root view (a flex column): with `parameters.layout: "fullscreen"`, a screen fills the frame |
79
+ | Props | read from the component's TypeScript types as React's: its own props, not those it takes from React Native (`ViewProps`, `PressableProps`) |
80
+ | Agents | `forgebook story`, `forgebook mcp` and the other commands read React Native stories as React's: the packages written for React Native are pre-bundled on the dev server's Node side too, where Node would otherwise load the real `react-native` |
81
+
82
+ Fonts and icons work as in an Expo web app: `useFonts` and `@expo/vector-icons` load their font files through
83
+ `expo-font`, which runs in the browser.
84
+
85
+ ## Options
86
+
87
+ `reactNativeWeb()` takes Storybook's React Native Web options, under these names:
88
+
89
+ | Option | Storybook's | What it does |
90
+ |---|---|---|
91
+ | `nativePackages` | `modulesToTranspile` | Packages outside the families above that import `react-native` or ship JSX in `.js` or Flow |
92
+ | `jsxImportSource` | `pluginReactOptions.jsxImportSource` | The JSX runtime of the project's files: `"nativewind"` for NativeWind. Forgebook's own pages keep React's |
93
+ | `babel` | `pluginReactOptions.babel` | `{ plugins, presets }`, by module name (and options): run on the project's files, as Metro runs them. Needs `@rolldown/plugin-babel` and `@babel/core` |
94
+ | `include`, `exclude` | `pluginReactOptions.include`, `.exclude` | The files React's plugin (Fast Refresh) and Babel apply to; the project's scripts outside `node_modules` by default |
95
+
96
+ A project's `postcss.config` is run on its style sheets, as Expo does on the web (Forgebook otherwise compiles CSS
97
+ with Lightning CSS alone); `postcss` is then the project's to install, and `jiti` or `tsx` too for a
98
+ `postcss.config.ts`.
99
+
100
+ ### Reanimated
101
+
102
+ ```sh
103
+ npm install --save-dev @rolldown/plugin-babel @babel/core
104
+ ```
105
+
106
+ ```ts
107
+ reactNativeWeb({ babel: { plugins: ["react-native-worklets/plugin"] } }); // Reanimated 4; 3 names "react-native-reanimated/plugin"
108
+ ```
109
+
110
+ The plugin compiles the project's worklets (`useAnimatedStyle`, `useDerivedValue`…), and Reanimated runs them in the
111
+ browser, as its web support does. On the dev server's Node side, where the agents read the stories, Reanimated gets
112
+ an animation frame from a timer, since Node draws none.
113
+
114
+ ### NativeWind
115
+
116
+ NativeWind 4 on Tailwind 3, as its web setup asks: a `tailwind.config.js` with its preset, a `postcss.config.js`
117
+ naming `tailwindcss`, a style sheet with Tailwind's directives imported by the preview, and its JSX runtime:
118
+
119
+ ```ts
120
+ reactNativeWeb({ jsxImportSource: "nativewind" });
121
+ ```
122
+
123
+ NativeWind's Babel preset (`nativewind/babel`), which Metro needs, is not needed here and is best left out: it
124
+ compiles JSX itself, for Forgebook's own linked files too, and adds Reanimated's plugin a second time.
125
+
126
+ ```tsx
127
+ // forgebook.preview.tsx
128
+ import "./global.css";
129
+ ```
130
+
131
+ `dark:` classes follow the catalogue's theme when Tailwind's `darkMode` is `"class"` and the theme switch sets a
132
+ class on the frame: `switches: [{ ...themeSwitch(), apply: { kind: "class", prefix: "" } }]` (`themeSwitch` from
133
+ `forgebook/switches`). With `darkMode: "media"`, NativeWind refuses the theme switch's changes.
134
+
135
+ ### A native module in a story
136
+
137
+ Native code cannot run in a browser, so a story stands in for the module its component calls, as Storybook advises
138
+ for its on-device version. react-native-web's `NativeModules` is a plain object: a story's `beforeEach` puts the
139
+ stand-in there and returns what takes it away.
140
+
141
+ ```tsx
142
+ import { NativeModules } from "react-native";
143
+
144
+ export const Charged: Story<typeof BatteryBadge> = {
145
+ beforeEach: () => {
146
+ NativeModules["HarbourBattery"] = { getLevel: async () => 0.82 };
147
+ return () => {
148
+ Reflect.deleteProperty(NativeModules, "HarbourBattery");
149
+ };
150
+ }
151
+ };
152
+ ```
153
+
154
+ For every story, the same goes in the preview's `beforeEach`. An Expo module of the app's own
155
+ (`requireNativeModule("HarbourBattery")`) throws on the web unless the module has a web file, as Expo's modules
156
+ do: `HarbourBatteryModule.web.ts`, exporting `registerWebModule(...)` of a class that answers in the browser, which
157
+ the catalogue reads before the native file. On the dev server's Node side, where the agents read the stories,
158
+ Expo's `requireNativeModule` gives an empty object rather than throwing, so a missing stand-in fails there at the
159
+ first call. A component whose web version differs more than a stand-in can cover
160
+ gets a `.web.tsx` file the same way.
161
+
162
+ ## What does not work in a browser
163
+
164
+ react-native-web draws React Native's components with the DOM; what needs the phone itself does not draw:
165
+
166
+ - **Native modules.** A component that calls a module of native code (the camera, Bluetooth, a native SDK) through
167
+ `NativeModules`, TurboModules or Expo's `requireNativeModule` throws or gets nothing: native code cannot run in a
168
+ browser. Stand in for the module in its stories (above), or give the component a `.web.tsx` file.
169
+ - **What needs the phone's UI thread.** Reanimated's animations run, as its web support runs them, on the page's
170
+ thread; what has no web version (`react-native-gesture-handler`'s native gestures, a worklet run on another thread
171
+ with `runOnRuntime`) does not.
172
+ - **React Native's own files** (`react-native/Libraries/…`): they have no web version, and an import of one fails with
173
+ a message naming it.
174
+ - **A project's `babel.config.js` and `metro.config.js`** are not read: the catalogue names its Babel plugins and JSX
175
+ source (above); Metro's resolver options have no counterpart. `jsxRuntime: "classic"` is not offered.
176
+ - **What react-native-web leaves out**: the older `accessibilityState` is not drawn (write `aria-checked`,
177
+ `aria-disabled`… as React Native 0.71 and later accept), and the components and APIs its
178
+ [compatibility table](https://necolas.github.io/react-native-web/docs/react-native-compatibility/) marks as missing.
179
+ - **Safe areas** are zero in a browser window: a story that reads them gets what its `SafeAreaProvider`'s
180
+ `initialMetrics` say.
181
+ - **Expo's packages** compiled from TypeScript file by file import types as modules: those imports are let be
182
+ `undefined`, as Metro does, and a build names each (`SHIMMED_EXPORT`). This holds for the whole catalogue: an
183
+ export missing anywhere is `undefined` with that warning, not a failed build.
184
+
185
+ | Import | What it is |
186
+ |---|---|
187
+ | `@forgebook/react-native-web` | `Meta`, `Story`, `Decorator`, `Preview`, `LogicMeta`, `LogicStory`: React's story types |
188
+ | `@forgebook/react-native-web/vite` | `reactNativeWeb()`, the framework adapter of `forgebook.config.ts` |
189
+
190
+ The rest of its exports are what the catalogue loads itself. Getting started, the catalogue's features and its
191
+ commands: [Forgebook's README](https://github.com/ensiria/forgebook/blob/develop/packages/forgebook/README.md).
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The frame page of a React Native catalogue: Forgebook's frame, drawing its stories with React through
3
+ * react-native-web, each in a root laid out as React Native's. Called by the page the plugin generates from the
4
+ * catalogue's configuration, not by an app.
5
+ *
6
+ * @module
7
+ */
8
+ import { type FrameConfig } from "forgebook/frame";
9
+ /**
10
+ * Shows the story the address names in the given element, drawn with React Native's components.
11
+ *
12
+ * @param container - the element the story renders into.
13
+ * @param config - the sources and the languages, as the shell's configuration gives them.
14
+ */
15
+ export declare const mountFrame: (container: HTMLElement, config: Omit<FrameConfig, "renderer">) => Promise<void>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The frame page of a React Native catalogue: Forgebook's frame, drawing its stories with React through
3
+ * react-native-web, each in a root laid out as React Native's. Called by the page the plugin generates from the
4
+ * catalogue's configuration, not by an app.
5
+ *
6
+ * @module
7
+ */
8
+ import { mountFrame as mountForgebookFrame } from "forgebook/frame";
9
+ import { nativeFrameRenderer } from "./nativeFrameRenderer.js";
10
+ /**
11
+ * Shows the story the address names in the given element, drawn with React Native's components.
12
+ *
13
+ * @param container - the element the story renders into.
14
+ * @param config - the sources and the languages, as the shell's configuration gives them.
15
+ */
16
+ export const mountFrame = (container, config) => mountForgebookFrame(container, { ...config, renderer: nativeFrameRenderer });
@@ -0,0 +1,5 @@
1
+ import type { FrameRenderer } from "forgebook/renderer";
2
+ /** The root's attribute, which the tests and a catalogue's style sheet can find it by. */
3
+ export declare const NATIVE_ROOT_ATTRIBUTE = "data-forgebook-native-root";
4
+ /** React Native, through react-native-web, as the frame draws with it. */
5
+ export declare const nativeFrameRenderer: FrameRenderer;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Forgebook's renderer for React Native in the browser, as the frame draws with it: React's frame renderer, each story
3
+ * drawn inside a root that lays out as React Native's root view does.
4
+ *
5
+ * @remarks
6
+ * On a phone every screen sits in a root view that is a flex column filling the window, so a screen written
7
+ * `style={{ flex: 1 }}` fills it. A canvas of the frame is a plain block, where `flex: 1` means nothing and the screen
8
+ * collapses to its content. The root is that column: the whole frame's height in a `fullscreen` story, its content's
9
+ * height otherwise, so a `centered` story is still centred in its canvas. It is an element of the renderer's own inside the canvas, so the
10
+ * frame's canvas stays as the frame made it.
11
+ *
12
+ * The server render is in `nativeRenderer.ts`, which the frame page does not import, so React's server renderer never
13
+ * reaches it.
14
+ *
15
+ * @module
16
+ */
17
+ import { reactFrameRenderer } from "@forgebook/react/frame-renderer";
18
+ /** The root's attribute, which the tests and a catalogue's style sheet can find it by. */
19
+ export const NATIVE_ROOT_ATTRIBUTE = "data-forgebook-native-root";
20
+ /**
21
+ * A root for a story, as React Native's root view lays out: a flex column, as tall as the frame for a full-screen
22
+ * story.
23
+ *
24
+ * @param canvas - the canvas it goes in.
25
+ * @param layout - the story's layout.
26
+ */
27
+ const nativeRoot = (canvas, layout) => {
28
+ const root = canvas.ownerDocument.createElement("div");
29
+ root.setAttribute(NATIVE_ROOT_ATTRIBUTE, "");
30
+ root.style.display = "flex";
31
+ root.style.flexDirection = "column";
32
+ root.style.flex = "1 1 auto";
33
+ // a fullscreen canvas is as tall as the frame: the root takes that height for a screen's `flex: 1` to fill. Only
34
+ // there: a centred canvas's own minimum is room to centre the story in, which the root must leave free
35
+ if (layout === "fullscreen")
36
+ root.style.minHeight = "inherit";
37
+ canvas.append(root);
38
+ return root;
39
+ };
40
+ /** React Native, through react-native-web, as the frame draws with it. */
41
+ export const nativeFrameRenderer = {
42
+ mount: (canvas, drawing, onError) => {
43
+ const root = nativeRoot(canvas, drawing.story.layout);
44
+ const mounted = reactFrameRenderer.mount(root, drawing, onError);
45
+ return {
46
+ drawn: mounted.drawn,
47
+ update: mounted.update,
48
+ unmount: () => {
49
+ mounted.unmount();
50
+ root.remove();
51
+ }
52
+ };
53
+ }
54
+ };
@@ -0,0 +1,3 @@
1
+ import type { StoryRenderer } from "forgebook/renderer";
2
+ /** React Native, through react-native-web, as the frame draws with it and the server tests render with it. */
3
+ export declare const nativeRenderer: StoryRenderer;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Forgebook's renderer for React Native in the browser, whole: the frame's drawing (`nativeFrameRenderer.ts`) and
3
+ * React's server render, for the generated server tests, which draws no root: a server has no frame to lay out.
4
+ *
5
+ * @remarks
6
+ * The frame page imports `nativeFrameRenderer.ts` alone, so React's server renderer never reaches it.
7
+ *
8
+ * @module
9
+ */
10
+ import { reactRenderer } from "@forgebook/react/renderer";
11
+ import { nativeFrameRenderer } from "./nativeFrameRenderer.js";
12
+ /** React Native, through react-native-web, as the frame draws with it and the server tests render with it. */
13
+ export const nativeRenderer = {
14
+ ...nativeFrameRenderer,
15
+ renderToString: reactRenderer.renderToString
16
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The story types for a React Native project (`import type { Meta, Story } from "@forgebook/react-native-web"`):
3
+ * React's, since a React Native component is a React component and its story is a React story.
4
+ *
5
+ * @remarks
6
+ * Named here so a project installs one package and its story files name the platform they are written for; each is
7
+ * React's type under the same name, so a story file can move between the two without a change. Types only.
8
+ *
9
+ * @module
10
+ */
11
+ import type { AnyComponent, AnyFunction, NoComponent, Decorator as ReactDecorator, LogicMeta as ReactLogicMeta, LogicStory as ReactLogicStory, Meta as ReactMeta, Preview as ReactPreview, Story as ReactStory } from "@forgebook/react";
12
+ /** A React Native story file's default export: `{ title, component, args… } satisfies Meta<typeof Card>`. */
13
+ export type Meta<TComponent extends AnyComponent = NoComponent> = ReactMeta<TComponent>;
14
+ /** A React Native story: a named export of a story file. */
15
+ export type Story<TComponent extends AnyComponent = NoComponent> = ReactStory<TComponent>;
16
+ /** The default export of a story file about a hook, written `satisfies LogicMeta<typeof useCart>`. */
17
+ export type LogicMeta<TFunction extends AnyFunction> = ReactLogicMeta<TFunction>;
18
+ /** A story about a hook: a `render` that draws a scene using it. */
19
+ export type LogicStory = ReactLogicStory;
20
+ /** Wraps a React Native story: a provider (safe areas, a theme), a container of a given size. */
21
+ export type Decorator<TArgs> = ReactDecorator<TArgs>;
22
+ /**
23
+ * A React Native catalogue's preview (`forgebook.preview.tsx`): decorators around every story (the providers the app
24
+ * puts at its root, a `SafeAreaProvider` among them), parameters under every meta's, a setup before any story.
25
+ */
26
+ export type Preview = ReactPreview;
File without changes
@@ -0,0 +1,24 @@
1
+ /**
2
+ * React Native's way of naming an image or a font, `require("./logo.png")`, written as the import a web bundler reads:
3
+ * the asset's address. Pure: the plugin (`nativePlugin.ts`) parses a module and applies it, the tests call it.
4
+ *
5
+ * @remarks
6
+ * Metro turns `require("./logo.png")` into an asset's number; react-native-web's `Image` and Expo's font loader take
7
+ * an address as well, so the call becomes the asset's default import, which Vite makes its address (and copies the
8
+ * file into a built catalogue). Only a module that is already an ES module is rewritten, since an import in a
9
+ * CommonJS file would change what it is; a module's own code keeps its lines and columns: each call is replaced by a
10
+ * name padded to the call's length and the imports go at the end (imports are hoisted), so its source map needs no
11
+ * change.
12
+ *
13
+ * @module
14
+ */
15
+ /** The files a `require` names as an asset rather than a module: images, fonts, sounds and videos. */
16
+ export declare const ASSET_FILE: RegExp;
17
+ /**
18
+ * A module's `require`s of assets written as imports of their addresses.
19
+ *
20
+ * @param code - the module's code.
21
+ * @param program - its tree, as the parser gives it.
22
+ * @returns the code, or none when nothing is rewritten (no asset is required, or the module is CommonJS).
23
+ */
24
+ export declare const importedAssets: (code: string, program: unknown) => string | undefined;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * React Native's way of naming an image or a font, `require("./logo.png")`, written as the import a web bundler reads:
3
+ * the asset's address. Pure: the plugin (`nativePlugin.ts`) parses a module and applies it, the tests call it.
4
+ *
5
+ * @remarks
6
+ * Metro turns `require("./logo.png")` into an asset's number; react-native-web's `Image` and Expo's font loader take
7
+ * an address as well, so the call becomes the asset's default import, which Vite makes its address (and copies the
8
+ * file into a built catalogue). Only a module that is already an ES module is rewritten, since an import in a
9
+ * CommonJS file would change what it is; a module's own code keeps its lines and columns: each call is replaced by a
10
+ * name padded to the call's length and the imports go at the end (imports are hoisted), so its source map needs no
11
+ * change.
12
+ *
13
+ * @module
14
+ */
15
+ import { codeLiteral } from "forgebook/code-literal";
16
+ /** The files a `require` names as an asset rather than a module: images, fonts, sounds and videos. */
17
+ export const ASSET_FILE = /\.(?:png|jpe?g|gif|webp|bmp|svg|ttf|otf|woff2?|mp3|wav|m4a|aac|mp4|webm|mov)$/i;
18
+ /** The statements that make a module an ES module. */
19
+ const MODULE_STATEMENTS = new Set([
20
+ "ImportDeclaration",
21
+ "ExportNamedDeclaration",
22
+ "ExportDefaultDeclaration",
23
+ "ExportAllDeclaration"
24
+ ]);
25
+ /** Whether a value is a syntax node. */
26
+ const isNode = (value) => typeof value === "object" &&
27
+ value !== null &&
28
+ "type" in value &&
29
+ typeof value.type === "string" &&
30
+ "start" in value &&
31
+ typeof value.start === "number" &&
32
+ "end" in value &&
33
+ typeof value.end === "number";
34
+ /** A node's property, by name: the children are read without knowing each node's shape. */
35
+ const propertyOf = (node, name) => Object.entries(node).find(([key]) => key === name)?.[1];
36
+ /**
37
+ * The asset a call requires: `require("./logo.png")` with one text naming an asset.
38
+ *
39
+ * @param node - a call.
40
+ * @returns the asset's specifier, or none for any other call.
41
+ */
42
+ const requiredAsset = (node) => {
43
+ const callee = propertyOf(node, "callee");
44
+ const args = propertyOf(node, "arguments");
45
+ if (!isNode(callee) || callee.type !== "Identifier" || propertyOf(callee, "name") !== "require")
46
+ return undefined;
47
+ if (!Array.isArray(args) || args.length !== 1)
48
+ return undefined;
49
+ const [argument] = args;
50
+ if (!isNode(argument) || argument.type !== "Literal")
51
+ return undefined;
52
+ const value = propertyOf(argument, "value");
53
+ return typeof value === "string" && ASSET_FILE.test(value) ? value : undefined;
54
+ };
55
+ /**
56
+ * The nodes under a node, in the order the module writes them: its properties that are nodes or lists of nodes.
57
+ *
58
+ * @param node - the node.
59
+ */
60
+ const childrenOf = (node) => Object.entries(node)
61
+ .filter(([key]) => key !== "type")
62
+ .flatMap(([, child]) => (Array.isArray(child) ? child : [child]))
63
+ .filter(isNode);
64
+ /**
65
+ * Every `require` of an asset in a tree, in the order the module writes them.
66
+ *
67
+ * @param root - the module's tree.
68
+ */
69
+ const assetRequires = (root) => {
70
+ const found = [];
71
+ const pending = [root];
72
+ for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
73
+ const specifier = node.type === "CallExpression" ? requiredAsset(node) : undefined;
74
+ if (specifier === undefined)
75
+ pending.push(...childrenOf(node));
76
+ else
77
+ found.push({ start: node.start, end: node.end, specifier });
78
+ }
79
+ return found.sort((left, right) => left.start - right.start);
80
+ };
81
+ /**
82
+ * Whether a module is an ES module: it imports or exports.
83
+ *
84
+ * @param program - the module's tree.
85
+ */
86
+ const isEsModule = (program) => {
87
+ const body = propertyOf(program, "body");
88
+ return (Array.isArray(body) &&
89
+ body.some((statement) => isNode(statement) && MODULE_STATEMENTS.has(statement.type)));
90
+ };
91
+ /**
92
+ * The start of the names the imports take: one the module does not use, so no name it declares is taken twice.
93
+ *
94
+ * @param code - the module's code.
95
+ */
96
+ const namePrefix = (code) => {
97
+ let prefix = "__fbAsset";
98
+ while (code.includes(prefix))
99
+ prefix = `_${prefix}`;
100
+ return prefix;
101
+ };
102
+ /**
103
+ * A name in place of a call, keeping its line breaks, so every later line stays where it was: the name padded to the
104
+ * call's first line, spaces on its others (a first line shorter than the name, `require(` alone, moves only what
105
+ * follows the call on its last line).
106
+ *
107
+ * @param call - the call's text.
108
+ * @param name - the name.
109
+ */
110
+ const inPlaceOf = (call, name) => {
111
+ const [first = "", ...rest] = call.split("\n");
112
+ // the shortest call, `require("a.png")`, is longer than any name up to a hundred thousand assets
113
+ const firstLine = first.length >= name.length ? name.padEnd(first.length, " ") : name;
114
+ return [firstLine, ...rest.map((line) => " ".repeat(line.length))].join("\n");
115
+ };
116
+ /**
117
+ * A module's `require`s of assets written as imports of their addresses.
118
+ *
119
+ * @param code - the module's code.
120
+ * @param program - its tree, as the parser gives it.
121
+ * @returns the code, or none when nothing is rewritten (no asset is required, or the module is CommonJS).
122
+ */
123
+ export const importedAssets = (code, program) => {
124
+ if (!isNode(program) || !code.includes("require") || !isEsModule(program))
125
+ return undefined;
126
+ const calls = assetRequires(program);
127
+ if (calls.length === 0)
128
+ return undefined;
129
+ const prefix = namePrefix(code);
130
+ const names = new Map();
131
+ let rewritten = "";
132
+ let at = 0;
133
+ for (const call of calls) {
134
+ const name = names.get(call.specifier) ?? `${prefix}${names.size}`;
135
+ names.set(call.specifier, name);
136
+ rewritten += code.slice(at, call.start) + inPlaceOf(code.slice(call.start, call.end), name);
137
+ at = call.end;
138
+ }
139
+ const imports = [...names].map(([specifier, name]) => `import ${name} from ${codeLiteral(specifier)};`);
140
+ return `${rewritten}${code.slice(at)}\n${imports.join("\n")}\n`;
141
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The Babel plugins and presets a React Native catalogue names (Reanimated's worklets plugin, NativeWind's preset), run
3
+ * by `@rolldown/plugin-babel`, loaded only when the catalogue names some.
4
+ *
5
+ * @remarks
6
+ * Metro runs Babel on every file; Vite runs none. Most of what Babel does for React Native, Vite does faster
7
+ * (`nativePlugin.ts`), but a library's own Babel plugin rewrites code no other tool understands: Reanimated's turns a
8
+ * function into a worklet. So the catalogue names the plugins it needs, as Storybook's React Native Web framework asks
9
+ * (`pluginReactOptions.babel`), and they run on the scripts outside `node_modules` by default: the project's, and
10
+ Forgebook's own only where they are linked rather than installed (a monorepo that develops Forgebook), where a plugin
11
+ meant for the project's code reads them too. `@babel/core` and
12
+ * `@rolldown/plugin-babel` are optional peers: a catalogue without Babel installs neither.
13
+ *
14
+ * @module
15
+ */
16
+ import type { PluginOption } from "vite";
17
+ import type { ReactNativeWebOptions } from "./reactNativeWeb.ts";
18
+ /**
19
+ * The Babel plugin of a catalogue's options: none without `babel`.
20
+ *
21
+ * @param options - the adapter's options.
22
+ * @throws when `babel` names plugins but `@rolldown/plugin-babel` is not installed, saying what to install.
23
+ */
24
+ export declare const babelPlugins: (options: ReactNativeWebOptions) => Promise<PluginOption[]>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A plugin or preset as Babel takes it: its name, or its name and options.
3
+ *
4
+ * @param item - the item.
5
+ */
6
+ const babelItem = (item) => typeof item === "string" ? item : [item[0], item[1]];
7
+ /**
8
+ * The Babel plugin of a catalogue's options: none without `babel`.
9
+ *
10
+ * @param options - the adapter's options.
11
+ * @throws when `babel` names plugins but `@rolldown/plugin-babel` is not installed, saying what to install.
12
+ */
13
+ export const babelPlugins = async (options) => {
14
+ const { babel } = options;
15
+ const plugins = (babel?.plugins ?? []).map(babelItem);
16
+ const presets = (babel?.presets ?? []).map(babelItem);
17
+ if (plugins.length === 0 && presets.length === 0)
18
+ return [];
19
+ let load;
20
+ try {
21
+ load = await import("@rolldown/plugin-babel");
22
+ }
23
+ catch {
24
+ throw new Error("reactNativeWeb({ babel }) runs Babel through @rolldown/plugin-babel: install it with @babel/core " +
25
+ "(npm install --save-dev @rolldown/plugin-babel @babel/core)");
26
+ }
27
+ return [
28
+ await load.default({
29
+ plugins,
30
+ presets,
31
+ ...(options.include === undefined ? {} : { include: [options.include].flat() }),
32
+ ...(options.exclude === undefined ? {} : { exclude: [options.exclude].flat() })
33
+ })
34
+ ];
35
+ };
@@ -0,0 +1,7 @@
1
+ import type { ExtractFileComponent, ExtractStoryComponent } from "forgebook/vite/adapter";
2
+ /** The module a new story file imports its types from. */
3
+ export declare const storyTypesModule = "@forgebook/react-native-web";
4
+ /** Reads a story file's component, as React's adapter reads it. */
5
+ export declare const extractStoryComponent: ExtractStoryComponent;
6
+ /** Reads a component a file exports, as React's adapter reads it. */
7
+ export declare const extractFileComponent: ExtractFileComponent;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The docs module of a React Native catalogue: React's, since a React Native component is a React component, with the
3
+ * story types a new story file imports being this package's (`forgebook new` writes `@forgebook/react-native-web`).
4
+ *
5
+ * @remarks
6
+ * React's reader lists the props the project declares and leaves out those it takes from `node_modules`: a component
7
+ * whose props extend `ViewProps` or `PressableProps` shows its own, not React Native's hundred.
8
+ *
9
+ * @module
10
+ */
11
+ import { extractFileComponent as extractReactFileComponent, extractStoryComponent as extractReactStoryComponent } from "@forgebook/react/docs";
12
+ /** The module a new story file imports its types from. */
13
+ export const storyTypesModule = "@forgebook/react-native-web";
14
+ /** Reads a story file's component, as React's adapter reads it. */
15
+ export const extractStoryComponent = (storyFile) => extractReactStoryComponent(storyFile);
16
+ /** Reads a component a file exports, as React's adapter reads it. */
17
+ export const extractFileComponent = (file, exportName) => extractReactFileComponent(file, exportName);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Whether a file is in one of Forgebook's packages.
3
+ *
4
+ * @param file - the file's path, absolute, its query left out.
5
+ */
6
+ export declare const isForgebookFile: (file: string) => boolean;