@rn-ave/metro-plugin 0.1.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 ADDED
@@ -0,0 +1,27 @@
1
+ # @rn-ave/metro-plugin
2
+
3
+ Metro plugin for `rn-ave`. Enables "Open in Cursor" functionality.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @rn-ave/metro-plugin
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```js
14
+ // metro.config.js
15
+ const { withAVE } = require('@rn-ave/metro-plugin');
16
+
17
+ const config = {
18
+ // your metro config
19
+ };
20
+
21
+ module.exports = withAVE(config);
22
+ ```
23
+
24
+ ## Features
25
+
26
+ - **Cursor Bridge**: Opens Cursor deeplinks from the host machine (useful for simulators).
27
+ - **File Opening**: Standard endpoint for opening files at specific lines.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Metro plugin for rn-ave
3
+ * Provides server middleware for opening files in Cursor editor
4
+ */
5
+ export interface OpenFileRequest {
6
+ file: string;
7
+ absolutePath?: string;
8
+ lineNumber?: number;
9
+ line?: number;
10
+ column?: number;
11
+ }
12
+ /**
13
+ * Metro configuration helper
14
+ * Wraps your Metro config to add rn-ave middleware
15
+ */
16
+ export declare function withAVE<T extends object>(metroConfig: T): T;
package/dist/index.js ADDED
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ /**
3
+ * Metro plugin for rn-ave
4
+ * Provides server middleware for opening files in Cursor editor
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.withAVE = withAVE;
44
+ const path = __importStar(require("path"));
45
+ const fs = __importStar(require("fs"));
46
+ const child_process_1 = require("child_process");
47
+ // @ts-ignore - launch-editor doesn't have types
48
+ const launch_editor_1 = __importDefault(require("launch-editor"));
49
+ /**
50
+ * Format file path with line and column for launch-editor
51
+ */
52
+ function formatFilePathWithPosition(filePath, lineNumber, column) {
53
+ if (lineNumber) {
54
+ if (column) {
55
+ return `${filePath}:${lineNumber}:${column}`;
56
+ }
57
+ return `${filePath}:${lineNumber}`;
58
+ }
59
+ return filePath;
60
+ }
61
+ /**
62
+ * Open a file in Cursor editor
63
+ */
64
+ async function openInEditor(request) {
65
+ const cwd = process.cwd();
66
+ // Resolve file path
67
+ let filePath = request.absolutePath || request.file;
68
+ if (!path.isAbsolute(filePath)) {
69
+ filePath = path.resolve(cwd, filePath);
70
+ }
71
+ // Check if file exists
72
+ if (!fs.existsSync(filePath)) {
73
+ return {
74
+ success: false,
75
+ error: `File not found: ${filePath}`,
76
+ };
77
+ }
78
+ const lineNumber = request.lineNumber || request.line;
79
+ const column = request.column;
80
+ const fileWithPosition = formatFilePathWithPosition(filePath, lineNumber, column);
81
+ return new Promise((resolve) => {
82
+ try {
83
+ (0, launch_editor_1.default)(fileWithPosition, 'cursor', // Always use Cursor
84
+ (fileName, errorMsg) => {
85
+ const errorMessage = errorMsg || 'Unknown error launching editor';
86
+ resolve({ success: false, error: errorMessage });
87
+ });
88
+ setTimeout(() => {
89
+ resolve({ success: true });
90
+ }, 100);
91
+ }
92
+ catch (error) {
93
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
94
+ resolve({ success: false, error: `Failed to launch editor: ${errorMessage}` });
95
+ }
96
+ });
97
+ }
98
+ // Endpoints
99
+ const LAUNCH_EDITOR_ENDPOINT = '/__inspect-open-in-editor';
100
+ const CURSOR_PROMPT_ENDPOINT = '/__rn_ave__/cursor-prompt';
101
+ const STATUS_ENDPOINT = '/__rn_ave__/status';
102
+ function parseQueryParams(url) {
103
+ const queryIndex = url.indexOf('?');
104
+ if (queryIndex === -1)
105
+ return {};
106
+ const queryString = url.slice(queryIndex + 1);
107
+ const params = {};
108
+ for (const pair of queryString.split('&')) {
109
+ const [key, value] = pair.split('=');
110
+ if (key) {
111
+ params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '';
112
+ }
113
+ }
114
+ return params;
115
+ }
116
+ /**
117
+ * Create the Metro server middleware
118
+ */
119
+ function createInspectorMiddleware() {
120
+ return async function inspectorMiddleware(req, res, next) {
121
+ const urlPath = req.url?.split('?')[0];
122
+ // Open in editor endpoint
123
+ if (urlPath === LAUNCH_EDITOR_ENDPOINT && req.method === 'GET') {
124
+ const params = parseQueryParams(req.url || '');
125
+ if (!params.file && !params.fileName) {
126
+ res.writeHead(400, { 'Content-Type': 'application/json' });
127
+ res.end(JSON.stringify({ success: false, error: 'Missing file parameter' }));
128
+ return;
129
+ }
130
+ const request = {
131
+ file: params.file || params.fileName,
132
+ lineNumber: params.line ? parseInt(params.line, 10) : undefined,
133
+ column: params.column ? parseInt(params.column, 10) : undefined,
134
+ };
135
+ const result = await openInEditor(request);
136
+ res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' });
137
+ res.end(JSON.stringify(result));
138
+ return;
139
+ }
140
+ // Status endpoint
141
+ if (urlPath === STATUS_ENDPOINT && req.method === 'GET') {
142
+ res.writeHead(200, { 'Content-Type': 'application/json' });
143
+ res.end(JSON.stringify({ status: 'ok', version: '1.0.0', editor: 'cursor' }));
144
+ return;
145
+ }
146
+ // Cursor prompt endpoint - opens deeplink on Mac side
147
+ if (urlPath === CURSOR_PROMPT_ENDPOINT && req.method === 'POST') {
148
+ let body = '';
149
+ req.on('data', (chunk) => { body += chunk.toString(); });
150
+ req.on('end', () => {
151
+ try {
152
+ const { promptText } = JSON.parse(body);
153
+ if (!promptText || typeof promptText !== 'string') {
154
+ res.writeHead(400, { 'Content-Type': 'application/json' });
155
+ res.end(JSON.stringify({ success: false, error: 'Missing promptText' }));
156
+ return;
157
+ }
158
+ // Build Cursor deeplink URL
159
+ const baseUrl = 'cursor://anysphere.cursor-deeplink/prompt';
160
+ const encodedText = encodeURIComponent(promptText);
161
+ const deeplinkUrl = `${baseUrl}?text=${encodedText}`;
162
+ // Use execFile to open the deeplink on macOS
163
+ (0, child_process_1.execFile)('open', [deeplinkUrl], (error) => {
164
+ if (error) {
165
+ res.writeHead(500, { 'Content-Type': 'application/json' });
166
+ res.end(JSON.stringify({ success: false, error: error.message }));
167
+ }
168
+ else {
169
+ res.writeHead(200, { 'Content-Type': 'application/json' });
170
+ res.end(JSON.stringify({ success: true }));
171
+ }
172
+ });
173
+ }
174
+ catch (e) {
175
+ res.writeHead(400, { 'Content-Type': 'application/json' });
176
+ res.end(JSON.stringify({ success: false, error: 'Invalid request body' }));
177
+ }
178
+ });
179
+ return;
180
+ }
181
+ next();
182
+ };
183
+ }
184
+ /**
185
+ * Metro configuration helper
186
+ * Wraps your Metro config to add rn-ave middleware
187
+ */
188
+ function withAVE(metroConfig) {
189
+ const middleware = createInspectorMiddleware();
190
+ const serverConfig = metroConfig.server || {};
191
+ const existingEnhanceMiddleware = serverConfig.enhanceMiddleware;
192
+ return {
193
+ ...metroConfig,
194
+ server: {
195
+ ...serverConfig,
196
+ enhanceMiddleware: (middleware2, server) => {
197
+ let enhanced = middleware2;
198
+ if (existingEnhanceMiddleware) {
199
+ enhanced = existingEnhanceMiddleware(middleware2, server);
200
+ }
201
+ return (req, res, next) => {
202
+ middleware(req, res, () => { enhanced(req, res, next); });
203
+ };
204
+ },
205
+ },
206
+ };
207
+ }
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@rn-ave/metro-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Metro plugin for rn-ave - enables 'Open in Cursor' functionality",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "clean": "rm -rf dist",
14
+ "dev": "tsc --watch"
15
+ },
16
+ "keywords": ["react-native", "metro", "ave", "visual-edit", "cursor", "open-in-editor"],
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "launch-editor": "^2.9.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^20.0.0",
23
+ "typescript": "^5.0.0"
24
+ }
25
+ }