rn-studio 0.2.1 → 0.3.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.
Files changed (49) hide show
  1. package/bin/rn-studio-init.js +173 -0
  2. package/bin/rn-studio-server.js +143 -15
  3. package/dist/StudioProvider.d.ts +5 -18
  4. package/dist/StudioProvider.d.ts.map +1 -1
  5. package/dist/StudioProvider.js +190 -41
  6. package/dist/StudioProvider.js.map +1 -1
  7. package/dist/ast/AstEngine.d.ts.map +1 -1
  8. package/dist/ast/AstEngine.js +17 -0
  9. package/dist/ast/AstEngine.js.map +1 -1
  10. package/dist/ast/PreviewState.d.ts +41 -0
  11. package/dist/ast/PreviewState.d.ts.map +1 -0
  12. package/dist/ast/PreviewState.js +159 -0
  13. package/dist/ast/PreviewState.js.map +1 -0
  14. package/dist/ast/UndoStack.d.ts +18 -0
  15. package/dist/ast/UndoStack.d.ts.map +1 -0
  16. package/dist/ast/UndoStack.js +105 -0
  17. package/dist/ast/UndoStack.js.map +1 -0
  18. package/dist/components/AddPropertyModal.d.ts +19 -0
  19. package/dist/components/AddPropertyModal.d.ts.map +1 -0
  20. package/dist/components/AddPropertyModal.js +174 -0
  21. package/dist/components/AddPropertyModal.js.map +1 -0
  22. package/dist/components/InspectorPanel.js +101 -6
  23. package/dist/components/InspectorPanel.js.map +1 -1
  24. package/dist/components/SelectionOverlay.d.ts.map +1 -1
  25. package/dist/components/SelectionOverlay.js +5 -0
  26. package/dist/components/SelectionOverlay.js.map +1 -1
  27. package/dist/components/StyleEditor.d.ts +5 -3
  28. package/dist/components/StyleEditor.d.ts.map +1 -1
  29. package/dist/components/StyleEditor.js +45 -11
  30. package/dist/components/StyleEditor.js.map +1 -1
  31. package/dist/data/styleProperties.d.ts +26 -0
  32. package/dist/data/styleProperties.d.ts.map +1 -0
  33. package/dist/data/styleProperties.js +142 -0
  34. package/dist/data/styleProperties.js.map +1 -0
  35. package/dist/types.d.ts +37 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/utils/autoScroll.d.ts +15 -0
  38. package/dist/utils/autoScroll.d.ts.map +1 -0
  39. package/dist/utils/autoScroll.js +132 -0
  40. package/dist/utils/autoScroll.js.map +1 -0
  41. package/dist/utils/findFiberBySource.d.ts +17 -0
  42. package/dist/utils/findFiberBySource.d.ts.map +1 -0
  43. package/dist/utils/findFiberBySource.js +76 -0
  44. package/dist/utils/findFiberBySource.js.map +1 -0
  45. package/dist/utils/persistence.d.ts +12 -0
  46. package/dist/utils/persistence.d.ts.map +1 -0
  47. package/dist/utils/persistence.js +44 -0
  48. package/dist/utils/persistence.js.map +1 -0
  49. package/package.json +3 -2
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ /**
4
+ * rn-studio init
5
+ *
6
+ * Zero-config bootstrap for consumer apps. Run:
7
+ *
8
+ * npx rn-studio init
9
+ *
10
+ * This command:
11
+ * 1. Adds `rn-studio/babel-plugin` to the project's babel.config.js
12
+ * (gated on `process.env.NODE_ENV !== 'production'`)
13
+ * 2. Adds a `"studio": "rn-studio-server"` script to package.json
14
+ * 3. Prints the exact snippet to paste into App.tsx
15
+ */
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const cwd = process.cwd();
20
+ const INDIGO = '\x1b[38;5;111m';
21
+ const GREEN = '\x1b[32m';
22
+ const GREY = '\x1b[90m';
23
+ const BOLD = '\x1b[1m';
24
+ const RESET = '\x1b[0m';
25
+
26
+ function log(msg) { console.log(msg); }
27
+ function ok(msg) { console.log(`${GREEN}✓${RESET} ${msg}`); }
28
+ function info(msg) { console.log(`${INDIGO}→${RESET} ${msg}`); }
29
+ function warn(msg) { console.log(`${GREY}! ${msg}${RESET}`); }
30
+
31
+ function banner() {
32
+ console.log('');
33
+ console.log(` ${INDIGO}${BOLD}rn-studio init${RESET}`);
34
+ console.log(` ${GREY}Live UI editor for React Native${RESET}`);
35
+ console.log('');
36
+ }
37
+
38
+ /* ───────────────── package.json ───────────────── */
39
+ function patchPackageJson() {
40
+ const pkgPath = path.join(cwd, 'package.json');
41
+ if (!fs.existsSync(pkgPath)) {
42
+ warn('No package.json found — skipping script injection.');
43
+ return;
44
+ }
45
+ const raw = fs.readFileSync(pkgPath, 'utf-8');
46
+ const pkg = JSON.parse(raw);
47
+ pkg.scripts = pkg.scripts || {};
48
+
49
+ if (pkg.scripts.studio === 'rn-studio-server') {
50
+ ok('package.json already has a "studio" script.');
51
+ return;
52
+ }
53
+ if (pkg.scripts.studio && pkg.scripts.studio !== 'rn-studio-server') {
54
+ warn(`package.json has a different "studio" script (${pkg.scripts.studio}). Leaving untouched.`);
55
+ return;
56
+ }
57
+ pkg.scripts.studio = 'rn-studio-server';
58
+ // Preserve two-space indentation convention.
59
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
60
+ ok('Added `"studio": "rn-studio-server"` to package.json scripts.');
61
+ }
62
+
63
+ /* ───────────────── babel.config.js ───────────────── */
64
+ function patchBabelConfig() {
65
+ const candidates = [
66
+ 'babel.config.js',
67
+ 'babel.config.cjs',
68
+ 'babel.config.mjs',
69
+ '.babelrc.js',
70
+ '.babelrc',
71
+ ];
72
+ const found = candidates
73
+ .map((c) => path.join(cwd, c))
74
+ .find((p) => fs.existsSync(p));
75
+
76
+ if (!found) {
77
+ warn('No babel.config.js found — creating one.');
78
+ const fresh = `module.exports = {
79
+ presets: ['module:@react-native/babel-preset'],
80
+ plugins: [
81
+ ...(process.env.NODE_ENV !== 'production' ? ['rn-studio/babel-plugin'] : []),
82
+ ],
83
+ };
84
+ `;
85
+ fs.writeFileSync(path.join(cwd, 'babel.config.js'), fresh, 'utf-8');
86
+ ok('Created babel.config.js with rn-studio plugin registered.');
87
+ return;
88
+ }
89
+
90
+ const content = fs.readFileSync(found, 'utf-8');
91
+ if (content.indexOf('rn-studio/babel-plugin') !== -1) {
92
+ ok(`${path.basename(found)} already references rn-studio/babel-plugin.`);
93
+ return;
94
+ }
95
+
96
+ // Attempt a light, string-based injection. If the file has a
97
+ // `plugins: [ ... ]` array, append the spread expression; otherwise
98
+ // add a new plugins property after the presets line.
99
+ let patched;
100
+ if (/plugins\s*:\s*\[/.test(content)) {
101
+ patched = content.replace(
102
+ /plugins\s*:\s*\[/,
103
+ `plugins: [\n ...(process.env.NODE_ENV !== 'production' ? ['rn-studio/babel-plugin'] : []),`,
104
+ );
105
+ } else {
106
+ patched = content.replace(
107
+ /presets\s*:\s*\[[^\]]*\]\s*,?/,
108
+ (m) => `${m.replace(/,?\s*$/, ',')}\n plugins: [\n ...(process.env.NODE_ENV !== 'production' ? ['rn-studio/babel-plugin'] : []),\n ],`,
109
+ );
110
+ }
111
+
112
+ if (patched === content) {
113
+ warn(`Could not auto-patch ${path.basename(found)} — add this manually:`);
114
+ console.log(
115
+ ` plugins: [\n ...(process.env.NODE_ENV !== 'production' ? ['rn-studio/babel-plugin'] : []),\n ],`,
116
+ );
117
+ return;
118
+ }
119
+
120
+ fs.writeFileSync(found, patched, 'utf-8');
121
+ ok(`Patched ${path.basename(found)} to include rn-studio/babel-plugin.`);
122
+ }
123
+
124
+ /* ───────────────── App.tsx snippet ───────────────── */
125
+ function printAppSnippet() {
126
+ console.log('');
127
+ info('Add this to your App.tsx (or wherever you mount the root):');
128
+ console.log('');
129
+ console.log(`${GREY}────────────────────────────────────────────────${RESET}`);
130
+ console.log(`${INDIGO}import${RESET} { StudioProvider } ${INDIGO}from${RESET} 'rn-studio';`);
131
+ console.log('');
132
+ console.log(`${INDIGO}export default function${RESET} App() {`);
133
+ console.log(' return (');
134
+ console.log(` <StudioProvider enabled={__DEV__} bubblePosition="bottom-right">`);
135
+ console.log(' <YourApp />');
136
+ console.log(' </StudioProvider>');
137
+ console.log(' );');
138
+ console.log('}');
139
+ console.log(`${GREY}────────────────────────────────────────────────${RESET}`);
140
+ console.log('');
141
+ }
142
+
143
+ /* ───────────────── run instructions ───────────────── */
144
+ function printRunInstructions() {
145
+ console.log(`${BOLD}Next steps:${RESET}`);
146
+ console.log('');
147
+ console.log(` ${GREY}#${RESET} Terminal 1 — Metro`);
148
+ console.log(` ${INDIGO}npx${RESET} react-native start`);
149
+ console.log('');
150
+ console.log(` ${GREY}#${RESET} Terminal 2 — rn-studio server`);
151
+ console.log(` ${INDIGO}npm run${RESET} studio`);
152
+ console.log('');
153
+ console.log(
154
+ `${GREEN}✓ Setup complete.${RESET} Launch your app, tap the floating bubble,\n then tap any component to edit its styles live.`,
155
+ );
156
+ console.log('');
157
+ }
158
+
159
+ /* ───────────────── main ───────────────── */
160
+ function main() {
161
+ banner();
162
+ try {
163
+ patchPackageJson();
164
+ patchBabelConfig();
165
+ printAppSnippet();
166
+ printRunInstructions();
167
+ } catch (err) {
168
+ console.error(`\n${GREY}!${RESET} rn-studio init failed:`, err && err.message);
169
+ process.exit(1);
170
+ }
171
+ }
172
+
173
+ main();
@@ -5,19 +5,23 @@
5
5
  *
6
6
  * Run alongside Metro: npm run studio
7
7
  *
8
- * Responsibilities:
9
- * 1. Listen on ws://localhost:7878 for messages from the rn-studio runtime.
10
- * 2. On STYLE_CHANGE, dispatch to the AST engine which rewrites the
11
- * source file. Metro's Fast Refresh instantly propagates the edit.
8
+ * Handles:
9
+ * - STYLE_CHANGE AST engine rewrites the source file
10
+ * - UNDO / REDO → pops/pushes the in-memory edit stack
11
+ * - STACK_STATE broadcast so clients can enable/disable buttons
12
12
  */
13
13
  const { WebSocketServer } = require('ws');
14
14
 
15
- let rewriteStyle;
15
+ let AstEngine;
16
+ let UndoStack;
17
+ let PreviewState;
16
18
  try {
17
- ({ rewriteStyle } = require('../dist/ast/AstEngine'));
19
+ AstEngine = require('../dist/ast/AstEngine');
20
+ UndoStack = require('../dist/ast/UndoStack');
21
+ PreviewState = require('../dist/ast/PreviewState');
18
22
  } catch (err) {
19
23
  console.error(
20
- '[rn-studio] Unable to load dist/ast/AstEngine. Did you run `npm run build`?'
24
+ '[rn-studio] Unable to load dist modules. Did you run `npm run build`?',
21
25
  );
22
26
  console.error(err.message);
23
27
  process.exit(1);
@@ -29,8 +33,22 @@ const wss = new WebSocketServer({ port: PORT });
29
33
  console.log(`[rn-studio] Server running on ws://localhost:${PORT}`);
30
34
  console.log('[rn-studio] Waiting for React Native runtime to connect...');
31
35
 
36
+ function broadcastStackState(ws) {
37
+ const state = UndoStack.getStackState();
38
+ const payload = JSON.stringify({ type: 'STACK_STATE', payload: state });
39
+ if (ws) {
40
+ ws.send(payload);
41
+ } else {
42
+ wss.clients.forEach((c) => {
43
+ if (c.readyState === 1) c.send(payload);
44
+ });
45
+ }
46
+ }
47
+
32
48
  wss.on('connection', (ws) => {
33
49
  console.log('[rn-studio] Client connected');
50
+ // Sync new clients with the current stack depths.
51
+ broadcastStackState(ws);
34
52
 
35
53
  ws.on('message', async (raw) => {
36
54
  let msg;
@@ -41,7 +59,7 @@ wss.on('connection', (ws) => {
41
59
  JSON.stringify({
42
60
  type: 'ERROR',
43
61
  payload: { message: 'Invalid JSON payload' },
44
- })
62
+ }),
45
63
  );
46
64
  return;
47
65
  }
@@ -54,7 +72,7 @@ wss.on('connection', (ws) => {
54
72
 
55
73
  if (msg.type === 'STYLE_CHANGE') {
56
74
  const { source, key, value } = msg.payload;
57
- await rewriteStyle({
75
+ await AstEngine.rewriteStyle({
58
76
  file: source.file,
59
77
  line: source.line,
60
78
  column: source.column,
@@ -63,18 +81,119 @@ wss.on('connection', (ws) => {
63
81
  });
64
82
  ws.send(JSON.stringify({ type: 'ACK', payload: { success: true } }));
65
83
  console.log(
66
- `[rn-studio] ✓ ${source.componentName} → ${key}: ${value}`
84
+ `[rn-studio] ✓ ${source.componentName} → ${key}: ${value}`,
85
+ );
86
+ broadcastStackState();
87
+ return;
88
+ }
89
+
90
+ if (msg.type === 'UNDO') {
91
+ const entry = UndoStack.undo();
92
+ if (entry) {
93
+ console.log(`[rn-studio] ↶ undo: ${entry.label} (${entry.file})`);
94
+ ws.send(
95
+ JSON.stringify({
96
+ type: 'ACK',
97
+ payload: { success: true, message: 'undo' },
98
+ }),
99
+ );
100
+ } else {
101
+ ws.send(
102
+ JSON.stringify({
103
+ type: 'ACK',
104
+ payload: { success: false, message: 'Nothing to undo' },
105
+ }),
106
+ );
107
+ }
108
+ broadcastStackState();
109
+ return;
110
+ }
111
+
112
+ if (msg.type === 'REDO') {
113
+ const entry = UndoStack.redo();
114
+ if (entry) {
115
+ console.log(`[rn-studio] ↷ redo: ${entry.label} (${entry.file})`);
116
+ ws.send(
117
+ JSON.stringify({
118
+ type: 'ACK',
119
+ payload: { success: true, message: 'redo' },
120
+ }),
121
+ );
122
+ } else {
123
+ ws.send(
124
+ JSON.stringify({
125
+ type: 'ACK',
126
+ payload: { success: false, message: 'Nothing to redo' },
127
+ }),
128
+ );
129
+ }
130
+ broadcastStackState();
131
+ return;
132
+ }
133
+
134
+ if (msg.type === 'BEGIN_PREVIEW') {
135
+ const file = msg.payload && msg.payload.file;
136
+ if (file) {
137
+ PreviewState.begin(file);
138
+ console.log(`[rn-studio] ⋯ preview begin: ${file}`);
139
+ }
140
+ ws.send(JSON.stringify({ type: 'ACK', payload: { success: true } }));
141
+ return;
142
+ }
143
+
144
+ if (msg.type === 'COMMIT_PREVIEW') {
145
+ const result = PreviewState.commit();
146
+ if (result && result.editCount > 0) {
147
+ console.log(
148
+ `[rn-studio] ✓ preview commit: ${result.editCount} edit${
149
+ result.editCount === 1 ? '' : 's'
150
+ } (${result.file})`,
151
+ );
152
+ }
153
+ ws.send(
154
+ JSON.stringify({
155
+ type: 'ACK',
156
+ payload: {
157
+ success: true,
158
+ message: result ? `committed ${result.editCount}` : 'nothing to commit',
159
+ },
160
+ }),
161
+ );
162
+ broadcastStackState();
163
+ return;
164
+ }
165
+
166
+ if (msg.type === 'CANCEL_PREVIEW') {
167
+ const result = PreviewState.cancel();
168
+ if (result && result.editCount > 0) {
169
+ console.log(
170
+ `[rn-studio] ↺ preview cancel: reverted ${result.editCount} edit${
171
+ result.editCount === 1 ? '' : 's'
172
+ } (${result.file})`,
173
+ );
174
+ }
175
+ ws.send(
176
+ JSON.stringify({
177
+ type: 'ACK',
178
+ payload: {
179
+ success: true,
180
+ message: result ? `reverted ${result.editCount}` : 'nothing to cancel',
181
+ },
182
+ }),
67
183
  );
184
+ broadcastStackState();
68
185
  return;
69
186
  }
70
187
 
71
188
  if (msg.type === 'PROP_CHANGE') {
72
- // Reserved for future prop editing. Ack for now.
73
189
  ws.send(
74
190
  JSON.stringify({
75
191
  type: 'ACK',
76
- payload: { success: true, message: 'PROP_CHANGE not yet implemented' },
77
- })
192
+ payload: {
193
+ success: true,
194
+ message: 'PROP_CHANGE not yet implemented',
195
+ },
196
+ }),
78
197
  );
79
198
  return;
80
199
  }
@@ -84,12 +203,21 @@ wss.on('connection', (ws) => {
84
203
  JSON.stringify({
85
204
  type: 'ERROR',
86
205
  payload: { message: err && err.message ? err.message : String(err) },
87
- })
206
+ }),
88
207
  );
89
208
  }
90
209
  });
91
210
 
92
- ws.on('close', () => console.log('[rn-studio] Client disconnected'));
211
+ ws.on('close', () => {
212
+ console.log('[rn-studio] Client disconnected');
213
+ // Abandon any in-flight preview so a stale buffer can't bleed
214
+ // into a later session on a different file.
215
+ if (PreviewState.isActive()) {
216
+ PreviewState.cancel();
217
+ console.log('[rn-studio] (preview auto-cancelled on disconnect)');
218
+ broadcastStackState();
219
+ }
220
+ });
93
221
  });
94
222
 
95
223
  process.on('SIGINT', () => {
@@ -1,5 +1,5 @@
1
1
  import React, { MutableRefObject } from 'react';
2
- import type { BubblePosition, StudioConfig, StudioContextValue } from './types';
2
+ import type { BubblePosition, SourceLocation, StudioConfig, StudioContextValue } from './types';
3
3
  interface Props extends Partial<StudioConfig> {
4
4
  enabled?: boolean;
5
5
  serverPort?: number;
@@ -8,26 +8,13 @@ interface Props extends Partial<StudioConfig> {
8
8
  children: React.ReactNode;
9
9
  }
10
10
  /**
11
- * Shared mutable ref to the app root view. Populated by `<StudioProvider>`
12
- * and consumed by `<SelectionOverlay>` for hit-testing via the React
13
- * DevTools `getInspectorDataForViewAtPoint` API. Exposing it at module
14
- * scope avoids having to thread it through context (and keeps the
15
- * context value shape unchanged for consumers).
11
+ * Shared mutable ref to the app root view. Populated by
12
+ * `<StudioProvider>` and consumed by `<SelectionOverlay>` for
13
+ * hit-testing via `getInspectorDataForViewAtPoint`.
16
14
  */
17
15
  export declare const appRootRef: MutableRefObject<any>;
18
- /**
19
- * <StudioProvider>
20
- *
21
- * Wrap your App.tsx with this provider. When `enabled` is false (the
22
- * default, intended for production), it renders children verbatim and
23
- * introduces zero overhead — no context, no bridge, no overlay.
24
- *
25
- * When enabled, it manages the studio state machine, opens a WebSocket
26
- * connection to the CLI server, and renders the floating bubble,
27
- * selection overlay, and inspector panel above your app.
28
- */
29
16
  export declare function StudioProvider({ children, enabled, serverPort, bubblePosition, }: Props): React.JSX.Element;
17
+ export type { SourceLocation };
30
18
  /** Hook for any descendant of `<StudioProvider>` to read studio state. */
31
19
  export declare function useStudio(): StudioContextValue;
32
- export {};
33
20
  //# sourceMappingURL=StudioProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StudioProvider.d.ts","sourceRoot":"","sources":["../src/StudioProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EACZ,gBAAgB,EAKjB,MAAM,OAAO,CAAC;AAOf,OAAO,KAAK,EACV,cAAc,EAEd,YAAY,EACZ,kBAAkB,EAEnB,MAAM,SAAS,CAAC;AAEjB,UAAU,KAAM,SAAQ,OAAO,CAAC,YAAY,CAAC;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,EAAE,gBAAgB,CAAC,GAAG,CAAqB,CAAC;AAEnE;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,OAAe,EACf,UAAiB,EACjB,cAA+B,GAChC,EAAE,KAAK,qBAUP;AAyFD,0EAA0E;AAC1E,wBAAgB,SAAS,IAAI,kBAAkB,CAM9C"}
1
+ {"version":3,"file":"StudioProvider.d.ts","sourceRoot":"","sources":["../src/StudioProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EACZ,gBAAgB,EAMjB,MAAM,OAAO,CAAC;AAYf,OAAO,KAAK,EACV,cAAc,EAEd,cAAc,EACd,YAAY,EACZ,kBAAkB,EAEnB,MAAM,SAAS,CAAC;AAEjB,UAAU,KAAM,SAAQ,OAAO,CAAC,YAAY,CAAC;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACzB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;;GAIG;AAEH,eAAO,MAAM,UAAU,EAAE,gBAAgB,CAAC,GAAG,CAAqB,CAAC;AAEnE,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,OAAe,EACf,UAAiB,EACjB,cAA+B,GAChC,EAAE,KAAK,qBAYP;AA0QD,YAAY,EAAE,cAAc,EAAE,CAAC;AAE/B,0EAA0E;AAC1E,wBAAgB,SAAS,IAAI,kBAAkB,CAM9C"}