@forgebook/react-native-web 0.0.0-stage → 0.2.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.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The project's PostCSS configuration run on its style sheets, as Expo does for the web: Tailwind's, which NativeWind's
3
+ * classes come from, among them.
4
+ *
5
+ * @remarks
6
+ * Forgebook compiles style sheets with Lightning CSS, which runs no PostCSS. A React Native project that styles with
7
+ * NativeWind has a `postcss.config` naming Tailwind, and Expo runs it on the web: so does this plugin, before Lightning
8
+ * CSS, on the style sheets of a project that has one. Without a configuration it does nothing and loads nothing;
9
+ * `postcss` is an optional peer, which a project with a configuration has.
10
+ *
11
+ * Tailwind reads the project's files for the classes they use, and says so in its result (`dir-dependency`): an edit
12
+ * to one of those files on the dev server compiles the style sheet again, so a new class has its rule.
13
+ *
14
+ * @module
15
+ */
16
+ import { relative } from "node:path";
17
+ import { isForgebookFile } from "./forgebookFiles.js";
18
+ /** A style sheet PostCSS runs on: not one imported as text or as an address. */
19
+ const STYLE_SHEET = /\.css(?:$|\?)/;
20
+ /** A style sheet's imports that are not compiled. */
21
+ const NOT_COMPILED = /[?&](?:raw|url)\b/;
22
+ /**
23
+ * The folders a PostCSS result depends on, from its messages.
24
+ *
25
+ * @param messages - the result's messages.
26
+ * @returns each folder, and each file, a result names.
27
+ */
28
+ export const dependenciesOf = (messages) => {
29
+ const dirs = [];
30
+ const files = [];
31
+ for (const message of messages) {
32
+ if (typeof message !== "object" || message === null || !("type" in message))
33
+ continue;
34
+ if (message.type === "dir-dependency" && "dir" in message && typeof message.dir === "string")
35
+ dirs.push({ dir: message.dir });
36
+ if (message.type === "dependency" && "file" in message && typeof message.file === "string")
37
+ files.push(message.file);
38
+ }
39
+ return { dirs, files };
40
+ };
41
+ /**
42
+ * The project's PostCSS, loaded from its configuration.
43
+ *
44
+ * @param root - the project's folder.
45
+ * @returns it, or none when the project has no PostCSS configuration.
46
+ */
47
+ const loadPostcss = async (root) => {
48
+ const { default: postcssrc } = await import("postcss-load-config");
49
+ let loaded;
50
+ try {
51
+ loaded = await postcssrc({}, root);
52
+ }
53
+ catch (error) {
54
+ if (error instanceof Error && error.message.startsWith("No PostCSS Config found"))
55
+ return undefined;
56
+ throw error;
57
+ }
58
+ const { default: postcss } = await import("postcss");
59
+ const runner = postcss(loaded.plugins);
60
+ return {
61
+ process: async (css, from) => {
62
+ const result = await runner.process(css, { ...loaded.options, from, to: from });
63
+ return { css: result.css, messages: result.messages };
64
+ }
65
+ };
66
+ };
67
+ /**
68
+ * Whether a file is in a folder.
69
+ *
70
+ * @param file - the file, absolute.
71
+ * @param dir - the folder, absolute.
72
+ */
73
+ const isInside = (file, dir) => {
74
+ const path = relative(dir, file);
75
+ return path !== "" && !path.startsWith("..") && !path.startsWith("/");
76
+ };
77
+ /** The plugin. */
78
+ export const projectPostcss = () => {
79
+ let root = ".";
80
+ let postcss;
81
+ // the style sheets compiled with folders they depend on, by file
82
+ const watched = new Map();
83
+ return {
84
+ name: "forgebook:react-native-web-postcss",
85
+ enforce: "pre",
86
+ configResolved: (config) => {
87
+ root = config.root;
88
+ },
89
+ transform: {
90
+ filter: { id: STYLE_SHEET },
91
+ async handler(code, id) {
92
+ const file = id.split("?", 1)[0] ?? id;
93
+ if (NOT_COMPILED.test(id) || file.includes("/node_modules/") || isForgebookFile(file))
94
+ return null;
95
+ // a configuration that failed to load is read again with the next style sheet, once it is mended
96
+ postcss ??= loadPostcss(root).catch((error) => {
97
+ postcss = undefined;
98
+ throw error;
99
+ });
100
+ const project = await postcss;
101
+ if (project === undefined)
102
+ return null;
103
+ const result = await project.process(code, file);
104
+ const { dirs, files } = dependenciesOf(result.messages);
105
+ for (const dependency of files)
106
+ this.addWatchFile(dependency);
107
+ if (dirs.length > 0)
108
+ watched.set(file, dirs);
109
+ return { code: result.css, map: null };
110
+ }
111
+ },
112
+ // a file Tailwind reads for its classes changed: its style sheets are compiled again with the page's next request
113
+ handleHotUpdate: ({ file, server, modules }) => {
114
+ const sheets = [...watched]
115
+ .filter(([sheet, dirs]) => sheet !== file && dirs.some((each) => isInside(file, each.dir)))
116
+ .flatMap(([sheet]) => [...(server.moduleGraph.getModulesByFile(sheet) ?? [])]);
117
+ for (const sheet of sheets)
118
+ server.moduleGraph.invalidateModule(sheet);
119
+ return sheets.length === 0 ? undefined : [...modules, ...sheets];
120
+ }
121
+ };
122
+ };
@@ -0,0 +1,53 @@
1
+ import type { FrameworkAdapter } from "forgebook/vite/adapter";
2
+ /** Which files an option applies to: a path pattern, a regular expression, or a list of them. */
3
+ export type FilePattern = string | RegExp | readonly (string | RegExp)[];
4
+ /** A Babel plugin or preset, by its module's name, with its options when it takes some: as a Babel configuration writes it. */
5
+ export type BabelItem = string | readonly [name: string, options: Readonly<Record<string, unknown>>];
6
+ /** The Babel plugins and presets a catalogue runs, as a project's `babel.config.js` names them for Metro. */
7
+ export type BabelOptions = {
8
+ /** The plugins (`"react-native-worklets/plugin"` for Reanimated 4, `"react-native-reanimated/plugin"` for 3). */
9
+ readonly plugins?: readonly BabelItem[];
10
+ /**
11
+ * The presets. NativeWind needs none: its JSX runtime (`jsxImportSource`) is enough, and its Babel preset compiles
12
+ * JSX itself and adds Reanimated's plugin a second time.
13
+ */
14
+ readonly presets?: readonly BabelItem[];
15
+ };
16
+ /** The options of `reactNativeWeb()`: Storybook's React Native Web framework's, under the names Forgebook gives them. */
17
+ export type ReactNativeWebOptions = {
18
+ /**
19
+ * The packages written for React Native, by name, besides those of React Native's and Expo's families
20
+ * (`react-native-*`, `@react-native/*`, `expo-*`, `@expo/*`), which always are: a package that imports `react-native`
21
+ * or ships code as Metro reads it (JSX in `.js` files, Flow types). Their code is compiled as Metro compiles it, and
22
+ * their `react-native` is react-native-web on the dev server's Node side too, where the agents' answers read the
23
+ * stories. None by default; Storybook calls them `modulesToTranspile`.
24
+ */
25
+ readonly nativePackages?: readonly string[];
26
+ /**
27
+ * The module JSX is compiled against, for every file the catalogue compiles: `"nativewind"` for NativeWind, whose
28
+ * runtime turns a `className` into styles. React's (`"react"`) by default.
29
+ */
30
+ readonly jsxImportSource?: string;
31
+ /**
32
+ * The Babel plugins and presets run on the files `include` names (the project's own scripts by default), after
33
+ * Vite has resolved them: Reanimated's worklets plugin, NativeWind's preset. They need `@babel/core` and
34
+ * `@rolldown/plugin-babel` installed; without this option, no Babel runs. Storybook's
35
+ * `pluginReactOptions.babel`.
36
+ */
37
+ readonly babel?: BabelOptions;
38
+ /**
39
+ * The files React's plugin (Fast Refresh) and Babel apply to; every script outside `node_modules` by default.
40
+ * Storybook's `pluginReactOptions.include`.
41
+ */
42
+ readonly include?: FilePattern;
43
+ /** The files React's plugin and Babel leave alone; `node_modules` by default. Storybook's `pluginReactOptions.exclude`. */
44
+ readonly exclude?: FilePattern;
45
+ };
46
+ /**
47
+ * React Native in the browser, through react-native-web, as the plugin reads and draws its components.
48
+ *
49
+ * @param options - the other packages written for React Native, the JSX import source, Babel's plugins and presets.
50
+ * @returns the adapter.
51
+ * @throws when `jsxImportSource` is not a package's name.
52
+ */
53
+ export declare const reactNativeWeb: (options?: ReactNativeWebOptions) => FrameworkAdapter;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Forgebook's adapter for React Native, for the Vite plugin: `defineConfig({ framework: reactNativeWeb() })`.
3
+ *
4
+ * @remarks
5
+ * A React Native component is a React component drawn with React Native's primitives: in the browser, react-native-web
6
+ * draws them with the DOM. So the props are read as React's, and the stories drawn with React's renderer inside a root
7
+ * that lays out as React Native's does (`@forgebook/react-native-web/frame`). What React Native adds is Vite's work:
8
+ * the plugins of `nativePlugin.ts`, the project's PostCSS (`projectPostcss.ts`), and the Babel plugins and presets the
9
+ * catalogue names (Reanimated's, NativeWind's), run by `@rolldown/plugin-babel`. A project's Metro and Babel
10
+ * configuration files are not read: the catalogue names what it needs, as Storybook's React Native Web framework does.
11
+ *
12
+ * @module
13
+ */
14
+ import { extname } from "node:path";
15
+ import { react } from "@forgebook/react/vite";
16
+ import { babelPlugins } from "./babelPlugins.js";
17
+ import { nativePlugins } from "./nativePlugin.js";
18
+ import { projectPostcss } from "./projectPostcss.js";
19
+ /** A package's name, scoped or not, as npm allows it. */
20
+ const PACKAGE_NAME = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/;
21
+ /**
22
+ * React's Vite plugin (Fast Refresh) on the catalogue's files. The JSX import source is not its: it would reach
23
+ * Forgebook's own pages too, so `nativePlugin.ts` names it in the project's files.
24
+ *
25
+ * @param options - the adapter's options.
26
+ */
27
+ const reactPlugin = async (options) => {
28
+ const { default: plugin } = await import("@vitejs/plugin-react");
29
+ return plugin({
30
+ ...(options.include === undefined ? {} : { include: [options.include].flat() }),
31
+ ...(options.exclude === undefined ? {} : { exclude: [options.exclude].flat() })
32
+ });
33
+ };
34
+ /**
35
+ * React Native in the browser, through react-native-web, as the plugin reads and draws its components.
36
+ *
37
+ * @param options - the other packages written for React Native, the JSX import source, Babel's plugins and presets.
38
+ * @returns the adapter.
39
+ * @throws when `jsxImportSource` is not a package's name.
40
+ */
41
+ export const reactNativeWeb = (options = {}) => {
42
+ // it is written into a comment of every project file and imported by name: a package's name, nothing else
43
+ if (options.jsxImportSource !== undefined && !PACKAGE_NAME.test(options.jsxImportSource))
44
+ throw new Error(`reactNativeWeb({ jsxImportSource }) takes a package's name: ${options.jsxImportSource} is not one`);
45
+ const base = react();
46
+ return {
47
+ name: "react-native-web",
48
+ // beside this module, with its extension: `.ts` in the sources, `.js` once built for npm
49
+ docsModule: new URL(`./extractDocs${extname(import.meta.url)}`, import.meta.url).href,
50
+ pages: { frame: "@forgebook/react-native-web/frame", code: base.pages.code },
51
+ browserDependencies: [
52
+ ...(base.browserDependencies ?? []),
53
+ "react-native-web",
54
+ ...(options.jsxImportSource === undefined
55
+ ? []
56
+ : [`${options.jsxImportSource}/jsx-runtime`, `${options.jsxImportSource}/jsx-dev-runtime`])
57
+ ],
58
+ vitePlugins: async () => [
59
+ ...nativePlugins(options),
60
+ projectPostcss(),
61
+ ...(await babelPlugins(options)),
62
+ await reactPlugin(options)
63
+ ]
64
+ };
65
+ };
package/package.json CHANGED
@@ -1,6 +1,110 @@
1
1
  {
2
2
  "name": "@forgebook/react-native-web",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Forgebook for React Native: a React Native project's components drawn in the browser through react-native-web, on Vite, their props read as React's.",
5
+ "keywords": [
6
+ "react-native",
7
+ "react-native-web",
8
+ "expo",
9
+ "react",
10
+ "catalogue",
11
+ "stories"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/ensiria/forgebook.git",
17
+ "directory": "packages/react-native-web"
18
+ },
19
+ "type": "module",
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22.18"
25
+ },
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/src/stories.d.ts",
30
+ "default": "./dist/src/stories.js"
31
+ },
32
+ "./docs": {
33
+ "types": "./dist/src/vite/extractDocs.d.ts",
34
+ "default": "./dist/src/vite/extractDocs.js"
35
+ },
36
+ "./frame": {
37
+ "types": "./dist/src/mountFrame.d.ts",
38
+ "default": "./dist/src/mountFrame.js"
39
+ },
40
+ "./frame-renderer": {
41
+ "types": "./dist/src/nativeFrameRenderer.d.ts",
42
+ "default": "./dist/src/nativeFrameRenderer.js"
43
+ },
44
+ "./renderer": {
45
+ "types": "./dist/src/nativeRenderer.d.ts",
46
+ "default": "./dist/src/nativeRenderer.js"
47
+ },
48
+ "./vite": {
49
+ "types": "./dist/src/vite/reactNativeWeb.d.ts",
50
+ "default": "./dist/src/vite/reactNativeWeb.js"
51
+ }
52
+ },
53
+ "dependencies": {
54
+ "@vitejs/plugin-react": "6.1.2",
55
+ "flow-remove-types": "2.338.0",
56
+ "postcss-load-config": "6.0.1",
57
+ "@forgebook/react": "0.2.0"
58
+ },
59
+ "peerDependencies": {
60
+ "@babel/core": "^7.29.0",
61
+ "@rolldown/plugin-babel": "^0.2.4",
62
+ "postcss": "^8.4.0",
63
+ "react": "^18.2.0 || ^19.0.0",
64
+ "react-dom": "^18.2.0 || ^19.0.0",
65
+ "react-native": ">=0.76.0 <1.0.0",
66
+ "react-native-web": "^0.21.0",
67
+ "vite": "^8.0.0",
68
+ "forgebook": "^0.2.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@babel/core": {
72
+ "optional": true
73
+ },
74
+ "@rolldown/plugin-babel": {
75
+ "optional": true
76
+ },
77
+ "postcss": {
78
+ "optional": true
79
+ }
80
+ },
81
+ "devDependencies": {
82
+ "@babel/core": "7.29.7",
83
+ "@rolldown/plugin-babel": "0.2.4",
84
+ "@types/node": "24.19.2",
85
+ "@types/react": "19.3.0",
86
+ "@types/react-dom": "19.3.0",
87
+ "happy-dom": "21.0.4",
88
+ "postcss": "8.5.32",
89
+ "react": "19.3.0",
90
+ "react-dom": "19.3.0",
91
+ "react-native": "0.88.1",
92
+ "react-native-web": "0.21.4",
93
+ "vite": "8.3.5",
94
+ "vitest": "5.0.5",
95
+ "@forgebook/story-format": "0.2.0",
96
+ "@forgebook/typescript-config": "0.0.0",
97
+ "@forgebook/testing": "0.2.0",
98
+ "@forgebook/tools": "0.0.0",
99
+ "forgebook": "0.2.0"
100
+ },
101
+ "publishConfig": {
102
+ "access": "public"
103
+ },
104
+ "scripts": {
105
+ "build": "package-build build",
106
+ "manifest": "package-build manifest",
107
+ "test": "vitest run",
108
+ "typecheck": "tsc -p src && tsc -p src/vite && tsc -p test"
109
+ }
6
110
  }