@mickeypause/react-native-preview-cli 2.0.0 → 2.0.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/README.md CHANGED
@@ -3,12 +3,19 @@
3
3
  CLI bridge for `@mickeypause/react-native-preview`.
4
4
 
5
5
  ```bash
6
+ npm install --global @mickeypause/react-native-preview-cli
7
+ cd your-expo-project
8
+ preview init
6
9
  preview start --ios
7
10
  preview list
8
11
  preview select PreviewCard
9
12
  preview clear
10
13
  ```
11
14
 
15
+ `init` detects the current Expo project, installs only `@mickeypause/react-native-preview` locally, and updates Babel and Metro configuration. It intentionally does not modify application source files; add `PreviewHost` and registrations manually.
16
+
17
+ `init --dry-run` previews the changes without writing files or installing packages.
18
+
12
19
  `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
20
 
14
21
  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 CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  const {
3
+ initProject,
3
4
  requestSnapshot,
4
5
  resolvePreview,
5
6
  sendCommand,
@@ -10,6 +11,24 @@ async function main() {
10
11
  const args = process.argv.slice(2);
11
12
  const command = args.shift();
12
13
  const value = command === 'select' ? args.shift() : undefined;
14
+ if (command === 'init') {
15
+ const packageManagerIndex = args.indexOf('--package-manager');
16
+ const packageManager = packageManagerIndex === -1 ? undefined : args[packageManagerIndex + 1];
17
+ const result = initProject({
18
+ dryRun: args.includes('--dry-run'),
19
+ packageManager,
20
+ });
21
+ console.log(`[react-native-preview] ${result.dryRun ? 'Would initialize' : 'Initialized'} ${result.projectRoot}`);
22
+ console.log(`[react-native-preview] package manager: ${result.packageManager}`);
23
+ if (!result.installed) console.log(`[react-native-preview] ${result.dryRun ? 'Would install' : 'Installed'} @mickeypause/react-native-preview locally`);
24
+ if (result.changedFiles.length > 0) {
25
+ console.log(`${result.dryRun ? 'Would update' : 'Updated'}:`);
26
+ for (const file of result.changedFiles) console.log(` ${file}`);
27
+ }
28
+ console.log('Manual steps remaining:');
29
+ for (const step of result.manualSteps) console.log(` - ${step}`);
30
+ return;
31
+ }
13
32
  if (command === 'start') {
14
33
  const platform = args.includes('--ios') ? 'ios' : args.includes('--android') ? 'android' : null;
15
34
  if (!platform) throw new Error('Choose a platform: preview start --ios or preview start --android');
@@ -45,7 +64,7 @@ async function main() {
45
64
  await sendCommand({ type: 'clear' });
46
65
  return;
47
66
  }
48
- throw new Error('Usage: preview start --ios|--android, preview list, preview select <name-or-id>, preview clear');
67
+ throw new Error('Usage: preview init [--dry-run] [--package-manager <npm|yarn|pnpm|bun>], preview start --ios|--android, preview list, preview select <name-or-id>, preview clear');
49
68
  }
50
69
 
51
70
  main().catch((error) => {
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './protocol';
2
2
  export { DEFAULT_PORT, SESSION_FILE, PreviewBridgeServer, findAvailablePort, hostForPlatform, isExpoProject, readSession, startPreviewSession, } from './server';
3
3
  export { requestSnapshot, resolvePreview, sendCommand } from './client';
4
+ export { BABEL_PLUGIN, METRO_PACKAGE, RUNTIME_PACKAGE, initProject, type InitOptions, type InitResult, } from './init';
5
+ export { detectPackageManager, findProjectRoot, type PackageManager } from './project';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
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;
17
+ exports.findProjectRoot = exports.detectPackageManager = exports.initProject = exports.RUNTIME_PACKAGE = exports.METRO_PACKAGE = exports.BABEL_PLUGIN = 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
18
  __exportStar(require("./protocol"), exports);
19
19
  var server_1 = require("./server");
20
20
  Object.defineProperty(exports, "DEFAULT_PORT", { enumerable: true, get: function () { return server_1.DEFAULT_PORT; } });
@@ -29,3 +29,11 @@ var client_1 = require("./client");
29
29
  Object.defineProperty(exports, "requestSnapshot", { enumerable: true, get: function () { return client_1.requestSnapshot; } });
30
30
  Object.defineProperty(exports, "resolvePreview", { enumerable: true, get: function () { return client_1.resolvePreview; } });
31
31
  Object.defineProperty(exports, "sendCommand", { enumerable: true, get: function () { return client_1.sendCommand; } });
32
+ var init_1 = require("./init");
33
+ Object.defineProperty(exports, "BABEL_PLUGIN", { enumerable: true, get: function () { return init_1.BABEL_PLUGIN; } });
34
+ Object.defineProperty(exports, "METRO_PACKAGE", { enumerable: true, get: function () { return init_1.METRO_PACKAGE; } });
35
+ Object.defineProperty(exports, "RUNTIME_PACKAGE", { enumerable: true, get: function () { return init_1.RUNTIME_PACKAGE; } });
36
+ Object.defineProperty(exports, "initProject", { enumerable: true, get: function () { return init_1.initProject; } });
37
+ var project_1 = require("./project");
38
+ Object.defineProperty(exports, "detectPackageManager", { enumerable: true, get: function () { return project_1.detectPackageManager; } });
39
+ Object.defineProperty(exports, "findProjectRoot", { enumerable: true, get: function () { return project_1.findProjectRoot; } });
package/dist/init.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { type PackageManager } from './project';
2
+ export declare const RUNTIME_PACKAGE = "@mickeypause/react-native-preview";
3
+ export declare const BABEL_PLUGIN = "@mickeypause/react-native-preview/babel-plugin";
4
+ export declare const METRO_PACKAGE = "@mickeypause/react-native-preview/metro";
5
+ export interface InitOptions {
6
+ projectRoot?: string;
7
+ packageManager?: PackageManager;
8
+ dryRun?: boolean;
9
+ runCommand?: (command: string, args: string[], projectRoot: string) => void;
10
+ }
11
+ export interface InitResult {
12
+ projectRoot: string;
13
+ packageManager: PackageManager;
14
+ dryRun: boolean;
15
+ installed: boolean;
16
+ changedFiles: string[];
17
+ manualSteps: string[];
18
+ }
19
+ export declare function initProject(options?: InitOptions): InitResult;
package/dist/init.js ADDED
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.METRO_PACKAGE = exports.BABEL_PLUGIN = exports.RUNTIME_PACKAGE = void 0;
4
+ exports.initProject = initProject;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const node_child_process_1 = require("node:child_process");
8
+ const project_1 = require("./project");
9
+ exports.RUNTIME_PACKAGE = '@mickeypause/react-native-preview';
10
+ exports.BABEL_PLUGIN = `${exports.RUNTIME_PACKAGE}/babel-plugin`;
11
+ exports.METRO_PACKAGE = `${exports.RUNTIME_PACKAGE}/metro`;
12
+ const defaultRunCommand = (command, args, projectRoot) => {
13
+ const result = (0, node_child_process_1.spawnSync)(command, args, { cwd: projectRoot, stdio: 'inherit' });
14
+ if (result.error)
15
+ throw result.error;
16
+ if (result.status !== 0)
17
+ throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status ?? 'unknown'}.`);
18
+ };
19
+ function initProject(options = {}) {
20
+ const projectRoot = (0, project_1.findProjectRoot)(options.projectRoot);
21
+ if (!(0, project_1.isExpoProject)(projectRoot)) {
22
+ throw new Error('preview init currently supports Expo projects only.');
23
+ }
24
+ const packageManager = options.packageManager || (0, project_1.detectPackageManager)(projectRoot);
25
+ if (!['npm', 'yarn', 'pnpm', 'bun'].includes(packageManager)) {
26
+ throw new Error(`Unsupported package manager: ${packageManager}`);
27
+ }
28
+ const packageJson = (0, project_1.readProjectPackageJson)(projectRoot);
29
+ const installed = Boolean(packageJson.dependencies?.[exports.RUNTIME_PACKAGE] || packageJson.devDependencies?.[exports.RUNTIME_PACKAGE]);
30
+ const changedFiles = [];
31
+ const dryRun = options.dryRun === true;
32
+ const runCommand = options.runCommand || defaultRunCommand;
33
+ if (!installed) {
34
+ if (!dryRun)
35
+ runCommand(packageManager, installArgs(packageManager), projectRoot);
36
+ }
37
+ updateConfig((0, node_path_1.join)(projectRoot, 'babel.config.js'), createBabelConfig, updateBabelConfig, changedFiles, dryRun);
38
+ updateConfig((0, node_path_1.join)(projectRoot, 'metro.config.js'), createMetroConfig, updateMetroConfig, changedFiles, dryRun);
39
+ return {
40
+ projectRoot,
41
+ packageManager,
42
+ dryRun,
43
+ installed,
44
+ changedFiles,
45
+ manualSteps: [
46
+ 'Add PreviewHost inside your existing provider tree.',
47
+ 'Register a component with registerPreview(...) or create a *.preview.tsx file.',
48
+ 'Run preview start --ios or preview start --android.',
49
+ ],
50
+ };
51
+ }
52
+ function installArgs(packageManager) {
53
+ switch (packageManager) {
54
+ case 'yarn': return ['add', exports.RUNTIME_PACKAGE];
55
+ case 'pnpm': return ['add', exports.RUNTIME_PACKAGE];
56
+ case 'bun': return ['add', exports.RUNTIME_PACKAGE];
57
+ case 'npm': return ['install', exports.RUNTIME_PACKAGE];
58
+ }
59
+ }
60
+ function updateConfig(filePath, create, update, changedFiles, dryRun) {
61
+ const exists = (0, node_fs_1.existsSync)(filePath);
62
+ const current = exists ? (0, node_fs_1.readFileSync)(filePath, 'utf8') : '';
63
+ const updated = exists ? update(current) : create();
64
+ if (exists && updated === current)
65
+ return;
66
+ changedFiles.push(filePath);
67
+ if (!dryRun)
68
+ (0, node_fs_1.writeFileSync)(filePath, updated, 'utf8');
69
+ }
70
+ function createBabelConfig() {
71
+ return `module.exports = function (api) {\n api.cache(true);\n return {\n presets: ['babel-preset-expo'],\n plugins: ['${exports.BABEL_PLUGIN}'],\n };\n};\n`;
72
+ }
73
+ function updateBabelConfig(content) {
74
+ if (content.includes(exports.BABEL_PLUGIN))
75
+ return content;
76
+ if (content.includes('plugins:')) {
77
+ const pluginsStart = content.indexOf('[', content.indexOf('plugins:'));
78
+ const pluginsEnd = findClosingBracket(content, pluginsStart);
79
+ if (pluginsStart === -1 || pluginsEnd === -1)
80
+ throw new Error('Could not safely update Babel plugins.');
81
+ const existing = content.slice(pluginsStart + 1, pluginsEnd).trim();
82
+ const separator = existing && !existing.endsWith(',') ? ',' : '';
83
+ const insertion = existing
84
+ ? `${separator}\n '${exports.BABEL_PLUGIN}',`
85
+ : `\n '${exports.BABEL_PLUGIN}',\n `;
86
+ return `${content.slice(0, pluginsEnd)}${insertion}${content.slice(pluginsEnd)}`;
87
+ }
88
+ const objectStart = content.indexOf('return {') >= 0
89
+ ? content.indexOf('return {') + 'return {'.length
90
+ : content.indexOf('module.exports = {') >= 0
91
+ ? content.indexOf('module.exports = {') + 'module.exports = {'.length
92
+ : -1;
93
+ if (objectStart === -1)
94
+ throw new Error('Could not safely update Babel config. Add the preview Babel plugin manually.');
95
+ return `${content.slice(0, objectStart)}\n plugins: ['${exports.BABEL_PLUGIN}'],${content.slice(objectStart)}`;
96
+ }
97
+ function createMetroConfig() {
98
+ return `const { getDefaultConfig } = require('expo/metro-config');\nconst { withPreviewDiscovery } = require('${exports.METRO_PACKAGE}');\n\nmodule.exports = withPreviewDiscovery(getDefaultConfig(__dirname));\n`;
99
+ }
100
+ function updateMetroConfig(content) {
101
+ if (content.includes('withPreviewDiscovery'))
102
+ return content;
103
+ const importLine = `const { withPreviewDiscovery } = require('${exports.METRO_PACKAGE}');\n`;
104
+ const withImport = `${importLine}${content}`;
105
+ const directConfig = content.match(/module\.exports\s*=\s*([^;]+);/);
106
+ if (!directConfig)
107
+ throw new Error('Could not safely update Metro config. Add withPreviewDiscovery(config) manually.');
108
+ const expression = directConfig[1].trim();
109
+ return `${withImport.slice(0, importLine.length + directConfig.index)}module.exports = withPreviewDiscovery(${expression});${content.slice(directConfig.index + directConfig[0].length)}`;
110
+ }
111
+ function findClosingBracket(content, openingIndex) {
112
+ if (openingIndex < 0)
113
+ return -1;
114
+ let depth = 0;
115
+ for (let index = openingIndex; index < content.length; index += 1) {
116
+ if (content[index] === '[')
117
+ depth += 1;
118
+ if (content[index] === ']') {
119
+ depth -= 1;
120
+ if (depth === 0)
121
+ return index;
122
+ }
123
+ }
124
+ return -1;
125
+ }
@@ -0,0 +1,11 @@
1
+ export type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
2
+ interface ProjectPackageJson {
3
+ dependencies?: Record<string, string>;
4
+ devDependencies?: Record<string, string>;
5
+ packageManager?: string;
6
+ }
7
+ export declare function findProjectRoot(start?: string): string;
8
+ export declare function readProjectPackageJson(projectRoot: string): ProjectPackageJson;
9
+ export declare function isExpoProject(projectRoot: string): boolean;
10
+ export declare function detectPackageManager(projectRoot: string): PackageManager;
11
+ export {};
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findProjectRoot = findProjectRoot;
4
+ exports.readProjectPackageJson = readProjectPackageJson;
5
+ exports.isExpoProject = isExpoProject;
6
+ exports.detectPackageManager = detectPackageManager;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ function findProjectRoot(start = process.cwd()) {
10
+ let directory = start;
11
+ while (true) {
12
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(directory, 'package.json')))
13
+ return directory;
14
+ const parent = (0, node_path_1.dirname)(directory);
15
+ if (parent === directory)
16
+ throw new Error(`No package.json found from ${start}. Run preview init inside a project.`);
17
+ directory = parent;
18
+ }
19
+ }
20
+ function readProjectPackageJson(projectRoot) {
21
+ return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(projectRoot, 'package.json'), 'utf8'));
22
+ }
23
+ function isExpoProject(projectRoot) {
24
+ const packageJson = readProjectPackageJson(projectRoot);
25
+ return Boolean(packageJson.dependencies?.expo || packageJson.devDependencies?.expo);
26
+ }
27
+ function detectPackageManager(projectRoot) {
28
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, 'pnpm-lock.yaml')))
29
+ return 'pnpm';
30
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, 'yarn.lock')))
31
+ return 'yarn';
32
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, 'bun.lock')) || (0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, 'bun.lockb')))
33
+ return 'bun';
34
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(projectRoot, 'package-lock.json')))
35
+ return 'npm';
36
+ const packageManager = readProjectPackageJson(projectRoot).packageManager?.split('@')[0];
37
+ if (packageManager === 'pnpm' || packageManager === 'yarn' || packageManager === 'bun' || packageManager === 'npm') {
38
+ return packageManager;
39
+ }
40
+ return 'npm';
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mickeypause/react-native-preview-cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "CLI for the React Native preview bridge",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",