browser-metro 1.0.5 → 1.0.7

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 ADDED
@@ -0,0 +1,173 @@
1
+ # browser-metro
2
+
3
+ A browser-based JavaScript/TypeScript bundler inspired by [Metro](https://metrobundler.dev/) (React Native's bundler). It runs entirely client-side in a Web Worker with HMR, React Refresh, Expo Router, and source map support.
4
+
5
+ Part of [reactnative.run](https://reactnative.run) - try the [playground](https://reactnative.run/playground).
6
+
7
+ ## Features
8
+
9
+ - **VirtualFS** - in-memory filesystem, no real FS needed
10
+ - **Module resolution** - Node.js-style with configurable extensions
11
+ - **Sucrase transforms** - fast TypeScript/JSX compilation
12
+ - **Plugin system** - pre/post transform hooks, module aliases, shims
13
+ - **HMR** - hot module replacement with React Refresh
14
+ - **Expo Router** - file-based routing with dynamic route HMR
15
+ - **API Routes** - in-browser `+api.ts` via fetch interception
16
+ - **Source maps** - inline combined source maps for accurate errors
17
+ - **npm packages** - on-demand bundling via ESM server
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install browser-metro
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```typescript
28
+ import {
29
+ Bundler, VirtualFS, typescriptTransformer
30
+ } from "browser-metro";
31
+ import type { BundlerConfig, FileMap } from "browser-metro";
32
+
33
+ const files: FileMap = {
34
+ "/index.ts": 'import { greet } from "./utils";\nconsole.log(greet("World"));',
35
+ "/utils.ts": 'export function greet(name: string) { return "Hello, " + name; }',
36
+ };
37
+
38
+ const bundler = new Bundler(new VirtualFS(files), {
39
+ resolver: { sourceExts: ["ts", "tsx", "js", "jsx"] },
40
+ transformer: typescriptTransformer,
41
+ server: { packageServerUrl: "https://esm.reactnative.run" },
42
+ });
43
+
44
+ const code = await bundler.bundle("/index.ts");
45
+ // code is a self-executing bundle with inline source map
46
+ ```
47
+
48
+ ## HMR with React Refresh
49
+
50
+ ```typescript
51
+ import {
52
+ IncrementalBundler, VirtualFS, reactRefreshTransformer
53
+ } from "browser-metro";
54
+
55
+ const bundler = new IncrementalBundler(new VirtualFS(files), {
56
+ resolver: { sourceExts: ["ts", "tsx", "js", "jsx"] },
57
+ transformer: reactRefreshTransformer,
58
+ server: { packageServerUrl: "https://esm.reactnative.run" },
59
+ hmr: { enabled: true, reactRefresh: true },
60
+ });
61
+
62
+ // Initial build
63
+ const initial = await bundler.build("/index.tsx");
64
+
65
+ // On file change - only re-transforms changed files
66
+ const result = await bundler.rebuild([
67
+ { path: "/App.tsx", type: "update" }
68
+ ]);
69
+
70
+ if (result.hmrUpdate && !result.hmrUpdate.requiresReload) {
71
+ // Send to iframe for hot patching
72
+ iframe.postMessage({
73
+ type: "hmr-update",
74
+ updatedModules: result.hmrUpdate.updatedModules,
75
+ removedModules: result.hmrUpdate.removedModules,
76
+ });
77
+ }
78
+ ```
79
+
80
+ ## API
81
+
82
+ ### Bundler
83
+
84
+ One-shot bundler. Creates a single bundle from an entry file.
85
+
86
+ ```typescript
87
+ const bundler = new Bundler(vfs, config);
88
+ const code = await bundler.bundle("/index.ts");
89
+ ```
90
+
91
+ ### IncrementalBundler
92
+
93
+ Watch-mode bundler with HMR. Maintains dependency graph and module cache across rebuilds.
94
+
95
+ ```typescript
96
+ const bundler = new IncrementalBundler(vfs, config);
97
+ const initial = await bundler.build("/index.tsx");
98
+ const update = await bundler.rebuild([{ path: "/App.tsx", type: "update" }]);
99
+ ```
100
+
101
+ ### VirtualFS
102
+
103
+ In-memory filesystem.
104
+
105
+ ```typescript
106
+ const vfs = new VirtualFS(files);
107
+ vfs.read("/index.ts"); // string | undefined
108
+ vfs.write("/new.ts", code); // create or overwrite
109
+ vfs.exists("/index.ts"); // boolean
110
+ vfs.list(); // string[]
111
+ ```
112
+
113
+ ### BundlerConfig
114
+
115
+ ```typescript
116
+ interface BundlerConfig {
117
+ resolver: { sourceExts: string[] };
118
+ transformer: Transformer;
119
+ server: { packageServerUrl: string };
120
+ hmr?: { enabled: boolean; reactRefresh?: boolean };
121
+ plugins?: BundlerPlugin[];
122
+ env?: Record<string, string>;
123
+ }
124
+ ```
125
+
126
+ ### Transformers
127
+
128
+ - `typescriptTransformer` - TS/JSX via Sucrase
129
+ - `reactRefreshTransformer` - adds React Refresh + `module.hot.accept()`
130
+ - `createReactRefreshTransformer(base)` - wrap a custom transformer with React Refresh
131
+
132
+ ### Plugins
133
+
134
+ ```typescript
135
+ interface BundlerPlugin {
136
+ name: string;
137
+ transformSource?(params): { src: string } | null; // before Sucrase
138
+ transformOutput?(params): { code: string } | null; // after Sucrase
139
+ resolveRequest?(context, name): string | null; // custom resolution
140
+ moduleAliases?(): Record<string, string>; // redirect requires
141
+ shimModules?(): Record<string, string>; // inline replacements
142
+ }
143
+ ```
144
+
145
+ ## ESM Package Server
146
+
147
+ browser-metro fetches npm packages from an ESM server that bundles them on-demand with esbuild:
148
+
149
+ ```
150
+ https://esm.reactnative.run/pkg/lodash@4.17.21
151
+ https://esm.reactnative.run/pkg/react-dom@19/client
152
+ ```
153
+
154
+ Packages are cached after first request. All dependencies are externalized for shared runtime instances. Version pinning via `// @externals` metadata prevents transitive dependency mismatches.
155
+
156
+ ## Documentation
157
+
158
+ Full docs at [reactnative.run/docs](https://reactnative.run/docs):
159
+
160
+ - [Architecture](https://reactnative.run/docs/architecture)
161
+ - [HMR & React Refresh](https://reactnative.run/docs/hmr)
162
+ - [Expo Router](https://reactnative.run/docs/expo-router)
163
+ - [API Routes](https://reactnative.run/docs/api-routes)
164
+ - [Plugin System](https://reactnative.run/docs/api/plugins)
165
+ - [Full API Reference](https://reactnative.run/docs/api/bundler)
166
+
167
+ ## Author
168
+
169
+ Built by [Sanket Sahu](https://github.com/sanketsahu) at [RapidNative](https://rapidnative.com).
170
+
171
+ ## License
172
+
173
+ MIT
package/dist/bundler.d.ts CHANGED
@@ -5,7 +5,10 @@ export declare class Bundler {
5
5
  private resolver;
6
6
  private config;
7
7
  private plugins;
8
+ private prefetchedPackages;
8
9
  constructor(fs: VirtualFS, config: BundlerConfig);
10
+ /** Prefetch all dependencies in a single batch request */
11
+ private prefetchDependencies;
9
12
  /** Read tsconfig.json "compilerOptions.paths" from the VirtualFS */
10
13
  private static readTsconfigPaths;
11
14
  /** Run the full pre-transform -> Sucrase -> post-transform pipeline */
package/dist/bundler.js CHANGED
@@ -1,14 +1,56 @@
1
1
  import { Resolver } from "./resolver.js";
2
2
  import { buildCombinedSourceMap, countNewlines, inlineSourceMap, shiftSourceMapOrigLines, } from "./source-map.js";
3
- import { findRequires, rewriteRequires, buildBundlePreamble } from "./utils.js";
3
+ import { findRequires, rewriteRequires, buildBundlePreamble, parseExternalsFromBody, hashDeps, parseDepBundle } from "./utils.js";
4
4
  export class Bundler {
5
5
  constructor(fs, config) {
6
+ this.prefetchedPackages = {};
6
7
  this.fs = fs;
7
8
  this.config = config;
8
9
  const paths = Bundler.readTsconfigPaths(fs);
9
10
  this.resolver = new Resolver(fs, { ...config.resolver, ...(paths && { paths }) });
10
11
  this.plugins = config.plugins ?? [];
11
12
  }
13
+ /** Prefetch all dependencies in a single batch request */
14
+ async prefetchDependencies() {
15
+ const versions = this.getPackageVersions();
16
+ if (Object.keys(versions).length === 0)
17
+ return;
18
+ // Remove aliased and shimmed packages - they're handled client-side
19
+ const aliases = this.getModuleAliases();
20
+ const shims = this.getShimModules();
21
+ for (const name of Object.keys(aliases))
22
+ delete versions[name];
23
+ for (const name of Object.keys(shims))
24
+ delete versions[name];
25
+ // Also remove alias targets that are already in versions (e.g. react-native-web is fetched via alias)
26
+ if (Object.keys(versions).length === 0)
27
+ return;
28
+ const hash = await hashDeps(versions);
29
+ const baseUrl = this.config.server.packageServerUrl;
30
+ try {
31
+ // Try GET first (CDN cacheable)
32
+ const getRes = await fetch(`${baseUrl}/bundle-deps/${hash}`);
33
+ if (getRes.ok) {
34
+ const { packages } = parseDepBundle(await getRes.text());
35
+ this.prefetchedPackages = packages;
36
+ return;
37
+ }
38
+ // POST to build
39
+ const postRes = await fetch(`${baseUrl}/bundle-deps`, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify({ hash, dependencies: versions }),
43
+ });
44
+ if (postRes.ok) {
45
+ const { packages } = parseDepBundle(await postRes.text());
46
+ this.prefetchedPackages = packages;
47
+ }
48
+ }
49
+ catch (err) {
50
+ // Silently fall back to individual fetches
51
+ console.warn("[prefetch] Failed, falling back to individual fetches:", err);
52
+ }
53
+ }
12
54
  /** Read tsconfig.json "compilerOptions.paths" from the VirtualFS */
13
55
  static readTsconfigPaths(fs) {
14
56
  const raw = fs.read("/tsconfig.json");
@@ -146,6 +188,21 @@ export class Bundler {
146
188
  }
147
189
  /** Fetch a pre-bundled npm package from the package server */
148
190
  async fetchPackage(specifier) {
191
+ // Check prefetched registry first (extract base package name from versioned specifier)
192
+ // specifier is like "react@19.1.0" or "react-dom@19.1.0/client"
193
+ let baseName = specifier;
194
+ const atIdx = specifier.indexOf("@", specifier.startsWith("@") ? 1 : 0);
195
+ if (atIdx > 0) {
196
+ baseName = specifier.slice(0, atIdx);
197
+ const afterVersion = specifier.indexOf("/", atIdx + 1);
198
+ if (afterVersion > 0) {
199
+ baseName = specifier.slice(0, atIdx) + specifier.slice(afterVersion);
200
+ }
201
+ }
202
+ if (this.prefetchedPackages[baseName]) {
203
+ return { code: this.prefetchedPackages[baseName], externals: {} };
204
+ }
205
+ // Fallback to individual fetch
149
206
  const url = this.config.server.packageServerUrl + "/pkg/" + specifier;
150
207
  const res = await fetch(url);
151
208
  if (!res.ok) {
@@ -153,18 +210,13 @@ export class Bundler {
153
210
  throw new Error("Failed to fetch package '" + specifier + "' (HTTP " + res.status + ")" + (body ? ": " + body.slice(0, 200) : ""));
154
211
  }
155
212
  const code = await res.text();
156
- let externals = {};
157
- const externalsHeader = res.headers.get("X-Externals");
158
- if (externalsHeader) {
159
- try {
160
- externals = JSON.parse(externalsHeader);
161
- }
162
- catch { }
163
- }
213
+ const externals = parseExternalsFromBody(code);
164
214
  return { code, externals };
165
215
  }
166
216
  /** Build the module map by walking the dependency graph */
167
217
  async buildModuleMap(entryFile) {
218
+ // Prefetch all deps in a single batch request
219
+ await this.prefetchDependencies();
168
220
  const moduleMap = {};
169
221
  const sourceMapMap = {};
170
222
  const visited = {};
@@ -187,7 +239,7 @@ export class Bundler {
187
239
  return;
188
240
  }
189
241
  const source = this.fs.read(filePath);
190
- if (!source) {
242
+ if (source === undefined) {
191
243
  throw new Error("File not found: " + filePath);
192
244
  }
193
245
  // Transform the file (TS -> JS, JSX -> JS, etc.)
@@ -14,6 +14,7 @@ export declare class IncrementalBundler {
14
14
  private entryFile;
15
15
  private packageVersions;
16
16
  private transitiveDepsVersions;
17
+ private prefetchedPackages;
17
18
  constructor(fs: VirtualFS, config: BundlerConfig);
18
19
  /** Read tsconfig.json "compilerOptions.paths" from the VirtualFS */
19
20
  private static readTsconfigPaths;
@@ -34,6 +35,8 @@ export declare class IncrementalBundler {
34
35
  /** Resolve an npm specifier to a versioned form.
35
36
  * Priority: user's package.json > transitive dep versions from manifests > bare name */
36
37
  private resolveNpmSpecifier;
38
+ /** Prefetch all dependencies in a single batch request */
39
+ private prefetchDependencies;
37
40
  /** Fetch a pre-bundled npm package from the package server */
38
41
  private fetchPackage;
39
42
  /**
@@ -3,7 +3,7 @@ import { DependencyGraph } from "./dependency-graph.js";
3
3
  import { ModuleCache } from "./module-cache.js";
4
4
  import { emitHmrBundle, HMR_RUNTIME_TEMPLATE } from "./hmr-runtime.js";
5
5
  import { buildCombinedSourceMap, countNewlines, inlineSourceMap, shiftSourceMapOrigLines, } from "./source-map.js";
6
- import { findRequires, rewriteRequires, hashString, buildBundlePreamble } from "./utils.js";
6
+ import { findRequires, rewriteRequires, hashString, buildBundlePreamble, parseExternalsFromBody, hashDeps, parseDepBundle } from "./utils.js";
7
7
  export class IncrementalBundler {
8
8
  constructor(fs, config) {
9
9
  this.graph = new DependencyGraph();
@@ -13,6 +13,7 @@ export class IncrementalBundler {
13
13
  this.entryFile = null;
14
14
  this.packageVersions = {};
15
15
  this.transitiveDepsVersions = {};
16
+ this.prefetchedPackages = {};
16
17
  this.fs = fs;
17
18
  this.config = config;
18
19
  const paths = IncrementalBundler.readTsconfigPaths(fs);
@@ -162,8 +163,61 @@ export class IncrementalBundler {
162
163
  const subpath = specifier.slice(baseName.length);
163
164
  return baseName + "@" + version + subpath;
164
165
  }
166
+ /** Prefetch all dependencies in a single batch request */
167
+ async prefetchDependencies() {
168
+ if (Object.keys(this.prefetchedPackages).length > 0)
169
+ return; // already prefetched
170
+ const versions = { ...this.packageVersions };
171
+ if (Object.keys(versions).length === 0)
172
+ return;
173
+ // Remove aliased and shimmed packages - they're handled client-side
174
+ const aliases = this.getModuleAliases();
175
+ const shims = this.getShimModules();
176
+ for (const name of Object.keys(aliases))
177
+ delete versions[name];
178
+ for (const name of Object.keys(shims))
179
+ delete versions[name];
180
+ if (Object.keys(versions).length === 0)
181
+ return;
182
+ const hash = await hashDeps(versions);
183
+ const baseUrl = this.config.server.packageServerUrl;
184
+ try {
185
+ const getRes = await fetch(`${baseUrl}/bundle-deps/${hash}`);
186
+ if (getRes.ok) {
187
+ const { packages } = parseDepBundle(await getRes.text());
188
+ this.prefetchedPackages = packages;
189
+ return;
190
+ }
191
+ const postRes = await fetch(`${baseUrl}/bundle-deps`, {
192
+ method: "POST",
193
+ headers: { "Content-Type": "application/json" },
194
+ body: JSON.stringify({ hash, dependencies: versions }),
195
+ });
196
+ if (postRes.ok) {
197
+ const { packages } = parseDepBundle(await postRes.text());
198
+ this.prefetchedPackages = packages;
199
+ }
200
+ }
201
+ catch (err) {
202
+ console.warn("[prefetch] Failed, falling back to individual fetches:", err);
203
+ }
204
+ }
165
205
  /** Fetch a pre-bundled npm package from the package server */
166
206
  async fetchPackage(specifier) {
207
+ // Check prefetched registry first
208
+ let baseName = specifier;
209
+ const atIdx = specifier.indexOf("@", specifier.startsWith("@") ? 1 : 0);
210
+ if (atIdx > 0) {
211
+ baseName = specifier.slice(0, atIdx);
212
+ const afterVersion = specifier.indexOf("/", atIdx + 1);
213
+ if (afterVersion > 0) {
214
+ baseName = specifier.slice(0, atIdx) + specifier.slice(afterVersion);
215
+ }
216
+ }
217
+ if (this.prefetchedPackages[baseName]) {
218
+ return { code: this.prefetchedPackages[baseName], externals: {} };
219
+ }
220
+ // Fallback to individual fetch
167
221
  const url = this.config.server.packageServerUrl + "/pkg/" + specifier;
168
222
  const res = await fetch(url);
169
223
  if (!res.ok) {
@@ -171,14 +225,7 @@ export class IncrementalBundler {
171
225
  throw new Error("Failed to fetch package '" + specifier + "' (HTTP " + res.status + ")" + (body ? ": " + body.slice(0, 200) : ""));
172
226
  }
173
227
  const code = await res.text();
174
- let externals = {};
175
- const externalsHeader = res.headers.get("X-Externals");
176
- if (externalsHeader) {
177
- try {
178
- externals = JSON.parse(externalsHeader);
179
- }
180
- catch { }
181
- }
228
+ const externals = parseExternalsFromBody(code);
182
229
  return { code, externals };
183
230
  }
184
231
  /**
@@ -198,7 +245,7 @@ export class IncrementalBundler {
198
245
  return { localDeps: [], npmDeps: [] };
199
246
  }
200
247
  const source = this.fs.read(filePath);
201
- if (!source) {
248
+ if (source === undefined) {
202
249
  throw new Error("File not found: " + filePath);
203
250
  }
204
251
  const sourceHash = hashString(source);
@@ -394,6 +441,8 @@ export class IncrementalBundler {
394
441
  this.moduleMap = {};
395
442
  this.sourceMapMap = {};
396
443
  this.packageVersions = this.getPackageVersions();
444
+ // Prefetch all deps in a single batch request
445
+ await this.prefetchDependencies();
397
446
  const npmPackagesNeeded = new Set();
398
447
  this.walkDeps(entryFile, npmPackagesNeeded);
399
448
  // Process module aliases: swap sources for targets in the fetch list
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /** Parse externals metadata from a package bundle body.
2
+ * Looks for a `// @externals {...}` comment line near the top.
3
+ * Falls back to empty object if not found. */
4
+ export declare function parseExternalsFromBody(code: string): Record<string, string>;
1
5
  /** Extract all require('...') call targets from source */
2
6
  export declare function findRequires(source: string): string[];
3
7
  /**
@@ -29,3 +33,14 @@ export declare function buildBundlePreamble(env?: Record<string, string>, router
29
33
  export declare function buildRouterShim(): string;
30
34
  /** Fast djb2 hash for cache invalidation */
31
35
  export declare function hashString(str: string): string;
36
+ /** Hash a dependencies object to a stable cache key.
37
+ * Uses SHA-256 (via Web Crypto or Node crypto) truncated to 16 hex chars
38
+ * for collision resistance while keeping URLs short.
39
+ * Includes a version prefix so cache is invalidated when bundling logic changes. */
40
+ export declare function hashDeps(deps: Record<string, string>): Promise<string>;
41
+ /** Parse a dep bundle response into individual package code chunks.
42
+ * Format: `// @dep-start <name>\n..code..\n// @dep-end <name>` */
43
+ export declare function parseDepBundle(code: string): {
44
+ manifest: Record<string, string>;
45
+ packages: Record<string, string>;
46
+ };
package/dist/utils.js CHANGED
@@ -1,6 +1,24 @@
1
1
  const REQUIRE_RE = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
2
2
  // Matches lines that are single-line comments or JSDoc/block comment continuations
3
3
  const COMMENT_LINE_RE = /^\s*(?:\/\/|\/?\*)/;
4
+ const EXTERNALS_RE = /^\/\/ @externals (.+)$/m;
5
+ const DEP_MANIFEST_RE = /^\/\/ @dep-manifest (.+)$/m;
6
+ const DEP_START_RE = /^\/\/ @dep-start (.+)$/gm;
7
+ const DEP_END_RE = /^\/\/ @dep-end (.+)$/gm;
8
+ /** Parse externals metadata from a package bundle body.
9
+ * Looks for a `// @externals {...}` comment line near the top.
10
+ * Falls back to empty object if not found. */
11
+ export function parseExternalsFromBody(code) {
12
+ const match = EXTERNALS_RE.exec(code);
13
+ if (!match)
14
+ return {};
15
+ try {
16
+ return JSON.parse(match[1]);
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
4
22
  /** Extract all require('...') call targets from source */
5
23
  export function findRequires(source) {
6
24
  if (!source)
@@ -206,3 +224,47 @@ export function hashString(str) {
206
224
  }
207
225
  return (hash >>> 0).toString(36);
208
226
  }
227
+ // Must match SERVER_VERSION in reactnative-esm/src/index.ts
228
+ const DEPS_HASH_VERSION = "2";
229
+ /** Hash a dependencies object to a stable cache key.
230
+ * Uses SHA-256 (via Web Crypto or Node crypto) truncated to 16 hex chars
231
+ * for collision resistance while keeping URLs short.
232
+ * Includes a version prefix so cache is invalidated when bundling logic changes. */
233
+ export async function hashDeps(deps) {
234
+ const sorted = Object.keys(deps).sort().map(k => `${k}@${deps[k]}`).join(",");
235
+ const input = `v${DEPS_HASH_VERSION}:${sorted}`;
236
+ // Web Crypto API (works in browsers and workers)
237
+ if (typeof globalThis.crypto?.subtle?.digest === "function") {
238
+ const data = new TextEncoder().encode(input);
239
+ const buf = await crypto.subtle.digest("SHA-256", data);
240
+ const arr = Array.from(new Uint8Array(buf));
241
+ return arr.map(b => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
242
+ }
243
+ // Fallback: djb2 (Node.js without crypto, shouldn't normally happen)
244
+ return hashString(input);
245
+ }
246
+ /** Parse a dep bundle response into individual package code chunks.
247
+ * Format: `// @dep-start <name>\n..code..\n// @dep-end <name>` */
248
+ export function parseDepBundle(code) {
249
+ let manifest = {};
250
+ const manifestMatch = DEP_MANIFEST_RE.exec(code);
251
+ if (manifestMatch) {
252
+ try {
253
+ manifest = JSON.parse(manifestMatch[1]);
254
+ }
255
+ catch { }
256
+ }
257
+ const packages = {};
258
+ const startRe = /^\/\/ @dep-start (.+)$/gm;
259
+ let match;
260
+ while ((match = startRe.exec(code)) !== null) {
261
+ const name = match[1];
262
+ const startIdx = match.index + match[0].length + 1; // skip newline
263
+ const endMarker = `// @dep-end ${name}`;
264
+ const endIdx = code.indexOf(endMarker, startIdx);
265
+ if (endIdx !== -1) {
266
+ packages[name] = code.slice(startIdx, endIdx).trimEnd();
267
+ }
268
+ }
269
+ return { manifest, packages };
270
+ }
package/package.json CHANGED
@@ -1,12 +1,29 @@
1
1
  {
2
2
  "name": "browser-metro",
3
- "version": "1.0.5",
4
- "description": "A browser-based JavaScript/TypeScript bundler with HMR support",
3
+ "version": "1.0.7",
4
+ "description": "A browser-based JavaScript/TypeScript bundler with HMR support, inspired by Metro. Runs entirely client-side.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "dist"
9
9
  ],
10
+ "keywords": [
11
+ "bundler",
12
+ "react-native",
13
+ "metro",
14
+ "hmr",
15
+ "typescript",
16
+ "jsx",
17
+ "browser",
18
+ "virtual-fs",
19
+ "expo-router"
20
+ ],
21
+ "homepage": "https://reactnative.run",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/RapidNative/reactnative-run"
25
+ },
26
+ "author": "Sanket Sahu <sanket@rapidnative.com> (https://github.com/sanketsahu)",
10
27
  "scripts": {
11
28
  "build": "tsc",
12
29
  "dev": "tsc --watch",