@mickeypause/react-native-preview-cli 2.0.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.
- package/README.md +14 -0
- package/bin/preview.js +54 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +56 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +31 -0
- package/dist/protocol.d.ts +33 -0
- package/dist/protocol.js +2 -0
- package/dist/server.d.ts +39 -0
- package/dist/server.js +172 -0
- package/package.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @mickeypause/react-native-preview-cli
|
|
2
|
+
|
|
3
|
+
CLI bridge for `@mickeypause/react-native-preview`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
preview start --ios
|
|
7
|
+
preview list
|
|
8
|
+
preview select PreviewCard
|
|
9
|
+
preview clear
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`start` supports Expo projects on iOS simulators and Android emulators. It writes `.react-native-preview/session.json`, owns the local WebSocket bridge, and passes the selected endpoint through `EXPO_PUBLIC_REACT_NATIVE_PREVIEW_BRIDGE_URL`.
|
|
13
|
+
|
|
14
|
+
The published `bin/preview.js` file is the executable npm CLI entrypoint. It is intentionally a thin launcher over the compiled `dist` API and is kept executable so npm can expose the `preview` command.
|
package/bin/preview.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const {
|
|
3
|
+
requestSnapshot,
|
|
4
|
+
resolvePreview,
|
|
5
|
+
sendCommand,
|
|
6
|
+
startPreviewSession,
|
|
7
|
+
} = require('../dist/index.js');
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const command = args.shift();
|
|
12
|
+
const value = command === 'select' ? args.shift() : undefined;
|
|
13
|
+
if (command === 'start') {
|
|
14
|
+
const platform = args.includes('--ios') ? 'ios' : args.includes('--android') ? 'android' : null;
|
|
15
|
+
if (!platform) throw new Error('Choose a platform: preview start --ios or preview start --android');
|
|
16
|
+
const { session, server, app } = await startPreviewSession({ platform });
|
|
17
|
+
console.log(`[react-native-preview] bridge listening at ${session.url}`);
|
|
18
|
+
console.log(`[react-native-preview] app endpoint ${session.appUrl}`);
|
|
19
|
+
const cleanup = async () => {
|
|
20
|
+
await server.close();
|
|
21
|
+
app.kill('SIGTERM');
|
|
22
|
+
process.exit(0);
|
|
23
|
+
};
|
|
24
|
+
process.once('SIGINT', cleanup);
|
|
25
|
+
process.once('SIGTERM', cleanup);
|
|
26
|
+
await new Promise((resolve) => app.once('exit', resolve));
|
|
27
|
+
await server.close();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (command === 'list') {
|
|
31
|
+
const snapshot = await requestSnapshot();
|
|
32
|
+
for (const preview of snapshot.previews) {
|
|
33
|
+
const selected = preview.id === snapshot.selectedId ? '*' : ' ';
|
|
34
|
+
console.log(`${selected} ${preview.name} (${preview.id})\n ${preview.source}:${preview.line}:${preview.column}`);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (command === 'select') {
|
|
39
|
+
if (!value) throw new Error('Usage: preview select <name-or-id>');
|
|
40
|
+
const snapshot = await requestSnapshot();
|
|
41
|
+
await sendCommand({ type: 'select', id: resolvePreview(snapshot, value) });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (command === 'clear') {
|
|
45
|
+
await sendCommand({ type: 'clear' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw new Error('Usage: preview start --ios|--android, preview list, preview select <name-or-id>, preview clear');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
main().catch((error) => {
|
|
52
|
+
console.error(`[react-native-preview] ${error.message}`);
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BridgeMessage, PreviewSnapshot } from './protocol';
|
|
2
|
+
export declare function requestSnapshot(url?: string): Promise<PreviewSnapshot>;
|
|
3
|
+
export declare function sendCommand(message: Extract<BridgeMessage, {
|
|
4
|
+
type: 'select' | 'clear';
|
|
5
|
+
}>, url?: string): Promise<void>;
|
|
6
|
+
export declare function resolvePreview(snapshot: PreviewSnapshot, selector: string): string;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.requestSnapshot = requestSnapshot;
|
|
7
|
+
exports.sendCommand = sendCommand;
|
|
8
|
+
exports.resolvePreview = resolvePreview;
|
|
9
|
+
const ws_1 = __importDefault(require("ws"));
|
|
10
|
+
const server_1 = require("./server");
|
|
11
|
+
function requestSnapshot(url = (0, server_1.readSession)().url) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const socket = new ws_1.default(url);
|
|
14
|
+
const timeout = setTimeout(() => {
|
|
15
|
+
socket.close();
|
|
16
|
+
reject(new Error('Timed out waiting for the preview registry.'));
|
|
17
|
+
}, 3000);
|
|
18
|
+
socket.on('message', (data) => {
|
|
19
|
+
const message = JSON.parse(String(data));
|
|
20
|
+
if (message.type === 'registry-snapshot' || message.type === 'registry-updated') {
|
|
21
|
+
clearTimeout(timeout);
|
|
22
|
+
socket.close();
|
|
23
|
+
resolve(message.snapshot);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
socket.on('error', (error) => {
|
|
27
|
+
clearTimeout(timeout);
|
|
28
|
+
reject(error);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function sendCommand(message, url = (0, server_1.readSession)().url) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const socket = new ws_1.default(url);
|
|
35
|
+
socket.once('open', () => {
|
|
36
|
+
socket.send(JSON.stringify(message));
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
socket.close();
|
|
39
|
+
resolve();
|
|
40
|
+
}, 50);
|
|
41
|
+
});
|
|
42
|
+
socket.once('error', reject);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function resolvePreview(snapshot, selector) {
|
|
46
|
+
const byId = snapshot.previews.find((preview) => preview.id === selector);
|
|
47
|
+
if (byId)
|
|
48
|
+
return byId.id;
|
|
49
|
+
const matches = snapshot.previews.filter((preview) => preview.name === selector);
|
|
50
|
+
if (matches.length === 0)
|
|
51
|
+
throw new Error(`Preview not found: ${selector}`);
|
|
52
|
+
if (matches.length > 1) {
|
|
53
|
+
throw new Error(`Preview name is ambiguous: ${selector}. Use one of: ${matches.map((preview) => preview.id).join(', ')}`);
|
|
54
|
+
}
|
|
55
|
+
return matches[0].id;
|
|
56
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.sendCommand = exports.resolvePreview = exports.requestSnapshot = exports.startPreviewSession = exports.readSession = exports.isExpoProject = exports.hostForPlatform = exports.findAvailablePort = exports.PreviewBridgeServer = exports.SESSION_FILE = exports.DEFAULT_PORT = void 0;
|
|
18
|
+
__exportStar(require("./protocol"), exports);
|
|
19
|
+
var server_1 = require("./server");
|
|
20
|
+
Object.defineProperty(exports, "DEFAULT_PORT", { enumerable: true, get: function () { return server_1.DEFAULT_PORT; } });
|
|
21
|
+
Object.defineProperty(exports, "SESSION_FILE", { enumerable: true, get: function () { return server_1.SESSION_FILE; } });
|
|
22
|
+
Object.defineProperty(exports, "PreviewBridgeServer", { enumerable: true, get: function () { return server_1.PreviewBridgeServer; } });
|
|
23
|
+
Object.defineProperty(exports, "findAvailablePort", { enumerable: true, get: function () { return server_1.findAvailablePort; } });
|
|
24
|
+
Object.defineProperty(exports, "hostForPlatform", { enumerable: true, get: function () { return server_1.hostForPlatform; } });
|
|
25
|
+
Object.defineProperty(exports, "isExpoProject", { enumerable: true, get: function () { return server_1.isExpoProject; } });
|
|
26
|
+
Object.defineProperty(exports, "readSession", { enumerable: true, get: function () { return server_1.readSession; } });
|
|
27
|
+
Object.defineProperty(exports, "startPreviewSession", { enumerable: true, get: function () { return server_1.startPreviewSession; } });
|
|
28
|
+
var client_1 = require("./client");
|
|
29
|
+
Object.defineProperty(exports, "requestSnapshot", { enumerable: true, get: function () { return client_1.requestSnapshot; } });
|
|
30
|
+
Object.defineProperty(exports, "resolvePreview", { enumerable: true, get: function () { return client_1.resolvePreview; } });
|
|
31
|
+
Object.defineProperty(exports, "sendCommand", { enumerable: true, get: function () { return client_1.sendCommand; } });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface PreviewDescriptor {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
source: string;
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
moduleId: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PreviewSnapshot {
|
|
10
|
+
previews: PreviewDescriptor[];
|
|
11
|
+
selectedId: string | null;
|
|
12
|
+
}
|
|
13
|
+
export type BridgeMessage = {
|
|
14
|
+
type: 'hello';
|
|
15
|
+
protocolVersion: 1;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'registry-snapshot';
|
|
18
|
+
snapshot: PreviewSnapshot;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'registry-updated';
|
|
21
|
+
snapshot: PreviewSnapshot;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'select';
|
|
24
|
+
id: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'clear';
|
|
27
|
+
} | {
|
|
28
|
+
type: 'selection-changed';
|
|
29
|
+
id: string | null;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'error';
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
package/dist/protocol.js
ADDED
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
export declare const DEFAULT_PORT = 45678;
|
|
3
|
+
export declare const SESSION_FILE = ".react-native-preview/session.json";
|
|
4
|
+
export interface PreviewSession {
|
|
5
|
+
port: number;
|
|
6
|
+
url: string;
|
|
7
|
+
appUrl: string;
|
|
8
|
+
platform: 'ios' | 'android';
|
|
9
|
+
}
|
|
10
|
+
export interface StartOptions {
|
|
11
|
+
platform: 'ios' | 'android';
|
|
12
|
+
port?: number;
|
|
13
|
+
projectRoot?: string;
|
|
14
|
+
sessionFile?: string;
|
|
15
|
+
expoCommand?: string;
|
|
16
|
+
spawnApp?: (command: string, args: string[], options: Parameters<typeof spawn>[2]) => ChildProcess;
|
|
17
|
+
}
|
|
18
|
+
export declare function isExpoProject(projectRoot?: string): boolean;
|
|
19
|
+
export declare function findAvailablePort(preferred?: number, host?: string): Promise<number>;
|
|
20
|
+
export declare class PreviewBridgeServer {
|
|
21
|
+
private readonly port;
|
|
22
|
+
private readonly host;
|
|
23
|
+
private readonly clients;
|
|
24
|
+
private readonly webSocketServer;
|
|
25
|
+
private snapshot;
|
|
26
|
+
constructor(port: number, host?: string);
|
|
27
|
+
get url(): string;
|
|
28
|
+
close(): Promise<void>;
|
|
29
|
+
private handle;
|
|
30
|
+
private send;
|
|
31
|
+
private broadcast;
|
|
32
|
+
}
|
|
33
|
+
export declare function startPreviewSession(options: StartOptions): Promise<{
|
|
34
|
+
session: PreviewSession;
|
|
35
|
+
server: PreviewBridgeServer;
|
|
36
|
+
app: ChildProcess;
|
|
37
|
+
}>;
|
|
38
|
+
export declare function readSession(sessionFile?: string): PreviewSession;
|
|
39
|
+
export declare function hostForPlatform(platform: 'ios' | 'android'): string;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PreviewBridgeServer = exports.SESSION_FILE = exports.DEFAULT_PORT = void 0;
|
|
37
|
+
exports.isExpoProject = isExpoProject;
|
|
38
|
+
exports.findAvailablePort = findAvailablePort;
|
|
39
|
+
exports.startPreviewSession = startPreviewSession;
|
|
40
|
+
exports.readSession = readSession;
|
|
41
|
+
exports.hostForPlatform = hostForPlatform;
|
|
42
|
+
const node_net_1 = require("node:net");
|
|
43
|
+
const node_fs_1 = require("node:fs");
|
|
44
|
+
const node_path_1 = require("node:path");
|
|
45
|
+
const node_child_process_1 = require("node:child_process");
|
|
46
|
+
const ws_1 = __importStar(require("ws"));
|
|
47
|
+
exports.DEFAULT_PORT = 45678;
|
|
48
|
+
exports.SESSION_FILE = '.react-native-preview/session.json';
|
|
49
|
+
function isExpoProject(projectRoot = process.cwd()) {
|
|
50
|
+
try {
|
|
51
|
+
const packageJson = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(projectRoot, 'package.json'), 'utf8'));
|
|
52
|
+
return Boolean(packageJson.dependencies?.expo || packageJson.devDependencies?.expo);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function findAvailablePort(preferred = exports.DEFAULT_PORT, host = '127.0.0.1') {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const server = (0, node_net_1.createServer)();
|
|
61
|
+
server.once('error', (error) => {
|
|
62
|
+
if (error.code === 'EADDRINUSE') {
|
|
63
|
+
findAvailablePort(preferred + 1, host).then(resolve, reject);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
reject(error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
server.listen(preferred, host, () => {
|
|
70
|
+
const address = server.address();
|
|
71
|
+
const port = typeof address === 'object' && address ? address.port : preferred;
|
|
72
|
+
server.close(() => resolve(port));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
class PreviewBridgeServer {
|
|
77
|
+
constructor(port, host = '127.0.0.1') {
|
|
78
|
+
this.port = port;
|
|
79
|
+
this.host = host;
|
|
80
|
+
this.clients = new Set();
|
|
81
|
+
this.snapshot = { previews: [], selectedId: null };
|
|
82
|
+
this.webSocketServer = new ws_1.Server({ port, host });
|
|
83
|
+
this.webSocketServer.on('connection', (socket) => {
|
|
84
|
+
this.clients.add(socket);
|
|
85
|
+
socket.on('message', (data) => this.handle(socket, String(data)));
|
|
86
|
+
socket.on('close', () => this.clients.delete(socket));
|
|
87
|
+
this.send(socket, { type: 'registry-snapshot', snapshot: this.snapshot });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
get url() {
|
|
91
|
+
return `ws://${this.host}:${this.port}`;
|
|
92
|
+
}
|
|
93
|
+
close() {
|
|
94
|
+
for (const client of this.clients)
|
|
95
|
+
client.close();
|
|
96
|
+
return new Promise((resolve, reject) => this.webSocketServer.close((error) => error ? reject(error) : resolve()));
|
|
97
|
+
}
|
|
98
|
+
handle(sender, raw) {
|
|
99
|
+
let message;
|
|
100
|
+
try {
|
|
101
|
+
message = JSON.parse(raw);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
this.send(sender, { type: 'error', message: 'Invalid bridge message.' });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (message.type === 'registry-snapshot' || message.type === 'registry-updated') {
|
|
108
|
+
const selectedId = this.snapshot.previews.some((preview) => preview.id === this.snapshot.selectedId)
|
|
109
|
+
? this.snapshot.selectedId
|
|
110
|
+
: message.snapshot.selectedId;
|
|
111
|
+
this.snapshot = { ...message.snapshot, selectedId };
|
|
112
|
+
this.broadcast({ ...message, snapshot: this.snapshot });
|
|
113
|
+
}
|
|
114
|
+
else if (message.type === 'selection-changed') {
|
|
115
|
+
this.snapshot = { ...this.snapshot, selectedId: message.id };
|
|
116
|
+
this.broadcast(message);
|
|
117
|
+
}
|
|
118
|
+
else if (message.type === 'select') {
|
|
119
|
+
this.snapshot = { ...this.snapshot, selectedId: message.id };
|
|
120
|
+
this.broadcast(message);
|
|
121
|
+
}
|
|
122
|
+
else if (message.type === 'clear') {
|
|
123
|
+
this.snapshot = { ...this.snapshot, selectedId: null };
|
|
124
|
+
this.broadcast(message);
|
|
125
|
+
}
|
|
126
|
+
else if (message.type === 'hello') {
|
|
127
|
+
this.send(sender, { type: 'registry-snapshot', snapshot: this.snapshot });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
send(socket, message) {
|
|
131
|
+
if (socket.readyState === ws_1.default.OPEN)
|
|
132
|
+
socket.send(JSON.stringify(message));
|
|
133
|
+
}
|
|
134
|
+
broadcast(message) {
|
|
135
|
+
for (const client of this.clients)
|
|
136
|
+
this.send(client, message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.PreviewBridgeServer = PreviewBridgeServer;
|
|
140
|
+
async function startPreviewSession(options) {
|
|
141
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
142
|
+
if (!isExpoProject(projectRoot)) {
|
|
143
|
+
throw new Error(`Expo project not detected in ${projectRoot}. Bare React Native launch is not supported yet.`);
|
|
144
|
+
}
|
|
145
|
+
const port = await findAvailablePort(options.port || Number(process.env.REACT_NATIVE_PREVIEW_PORT) || exports.DEFAULT_PORT);
|
|
146
|
+
const server = new PreviewBridgeServer(port);
|
|
147
|
+
const appHost = options.platform === 'android' ? '10.0.2.2' : '127.0.0.1';
|
|
148
|
+
const appUrl = `ws://${appHost}:${port}`;
|
|
149
|
+
const session = { port, url: server.url, appUrl, platform: options.platform };
|
|
150
|
+
const sessionFile = options.sessionFile || (0, node_path_1.join)(projectRoot, exports.SESSION_FILE);
|
|
151
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(sessionFile), { recursive: true });
|
|
152
|
+
(0, node_fs_1.writeFileSync)(sessionFile, JSON.stringify(session, null, 2));
|
|
153
|
+
const command = options.expoCommand || 'npx';
|
|
154
|
+
const args = command === 'npx'
|
|
155
|
+
? ['expo', 'start', '--dev-client', `--${options.platform}`]
|
|
156
|
+
: ['start', '--dev-client', `--${options.platform}`];
|
|
157
|
+
const spawnApp = options.spawnApp || ((cmd, cmdArgs, spawnOptions) => (0, node_child_process_1.spawn)(cmd, cmdArgs, spawnOptions));
|
|
158
|
+
const app = spawnApp(command, args, {
|
|
159
|
+
cwd: projectRoot,
|
|
160
|
+
env: { ...process.env, EXPO_PUBLIC_REACT_NATIVE_PREVIEW_BRIDGE_URL: appUrl },
|
|
161
|
+
stdio: 'inherit',
|
|
162
|
+
});
|
|
163
|
+
return { session, server, app };
|
|
164
|
+
}
|
|
165
|
+
function readSession(sessionFile = (0, node_path_1.join)(process.cwd(), exports.SESSION_FILE)) {
|
|
166
|
+
if (!(0, node_fs_1.existsSync)(sessionFile))
|
|
167
|
+
throw new Error(`No preview session found at ${sessionFile}. Run preview start first.`);
|
|
168
|
+
return JSON.parse((0, node_fs_1.readFileSync)(sessionFile, 'utf8'));
|
|
169
|
+
}
|
|
170
|
+
function hostForPlatform(platform) {
|
|
171
|
+
return platform === 'android' ? '10.0.2.2' : '127.0.0.1';
|
|
172
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mickeypause/react-native-preview-cli",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "CLI for the React Native preview bridge",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": { "preview": "bin/preview.js" },
|
|
8
|
+
"files": ["dist", "bin"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.json",
|
|
11
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"dependencies": { "ws": "^8.18.3" },
|
|
15
|
+
"devDependencies": { "@types/node": "^24.10.1", "typescript": "^5.9.3" }
|
|
16
|
+
}
|