rn-studio 0.2.1 → 0.3.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/bin/rn-studio-init.js +173 -0
- package/bin/rn-studio-server.js +77 -14
- package/dist/StudioProvider.d.ts +5 -18
- package/dist/StudioProvider.d.ts.map +1 -1
- package/dist/StudioProvider.js +118 -41
- package/dist/StudioProvider.js.map +1 -1
- package/dist/ast/AstEngine.d.ts.map +1 -1
- package/dist/ast/AstEngine.js +10 -0
- package/dist/ast/AstEngine.js.map +1 -1
- package/dist/ast/UndoStack.d.ts +18 -0
- package/dist/ast/UndoStack.d.ts.map +1 -0
- package/dist/ast/UndoStack.js +105 -0
- package/dist/ast/UndoStack.js.map +1 -0
- package/dist/components/AddPropertyModal.d.ts +19 -0
- package/dist/components/AddPropertyModal.d.ts.map +1 -0
- package/dist/components/AddPropertyModal.js +174 -0
- package/dist/components/AddPropertyModal.js.map +1 -0
- package/dist/components/InspectorPanel.js +35 -5
- package/dist/components/InspectorPanel.js.map +1 -1
- package/dist/components/SelectionOverlay.d.ts.map +1 -1
- package/dist/components/SelectionOverlay.js +5 -0
- package/dist/components/SelectionOverlay.js.map +1 -1
- package/dist/components/StyleEditor.d.ts +5 -3
- package/dist/components/StyleEditor.d.ts.map +1 -1
- package/dist/components/StyleEditor.js +45 -11
- package/dist/components/StyleEditor.js.map +1 -1
- package/dist/data/styleProperties.d.ts +26 -0
- package/dist/data/styleProperties.d.ts.map +1 -0
- package/dist/data/styleProperties.js +142 -0
- package/dist/data/styleProperties.js.map +1 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/autoScroll.d.ts +15 -0
- package/dist/utils/autoScroll.d.ts.map +1 -0
- package/dist/utils/autoScroll.js +132 -0
- package/dist/utils/autoScroll.js.map +1 -0
- package/dist/utils/findFiberBySource.d.ts +17 -0
- package/dist/utils/findFiberBySource.d.ts.map +1 -0
- package/dist/utils/findFiberBySource.js +76 -0
- package/dist/utils/findFiberBySource.js.map +1 -0
- package/dist/utils/persistence.d.ts +12 -0
- package/dist/utils/persistence.d.ts.map +1 -0
- package/dist/utils/persistence.js +44 -0
- package/dist/utils/persistence.js.map +1 -0
- 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();
|
package/bin/rn-studio-server.js
CHANGED
|
@@ -5,19 +5,21 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Run alongside Metro: npm run studio
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
15
|
+
let AstEngine;
|
|
16
|
+
let UndoStack;
|
|
16
17
|
try {
|
|
17
|
-
|
|
18
|
+
AstEngine = require('../dist/ast/AstEngine');
|
|
19
|
+
UndoStack = require('../dist/ast/UndoStack');
|
|
18
20
|
} catch (err) {
|
|
19
21
|
console.error(
|
|
20
|
-
'[rn-studio] Unable to load dist
|
|
22
|
+
'[rn-studio] Unable to load dist modules. Did you run `npm run build`?',
|
|
21
23
|
);
|
|
22
24
|
console.error(err.message);
|
|
23
25
|
process.exit(1);
|
|
@@ -29,8 +31,22 @@ const wss = new WebSocketServer({ port: PORT });
|
|
|
29
31
|
console.log(`[rn-studio] Server running on ws://localhost:${PORT}`);
|
|
30
32
|
console.log('[rn-studio] Waiting for React Native runtime to connect...');
|
|
31
33
|
|
|
34
|
+
function broadcastStackState(ws) {
|
|
35
|
+
const state = UndoStack.getStackState();
|
|
36
|
+
const payload = JSON.stringify({ type: 'STACK_STATE', payload: state });
|
|
37
|
+
if (ws) {
|
|
38
|
+
ws.send(payload);
|
|
39
|
+
} else {
|
|
40
|
+
wss.clients.forEach((c) => {
|
|
41
|
+
if (c.readyState === 1) c.send(payload);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
wss.on('connection', (ws) => {
|
|
33
47
|
console.log('[rn-studio] Client connected');
|
|
48
|
+
// Sync new clients with the current stack depths.
|
|
49
|
+
broadcastStackState(ws);
|
|
34
50
|
|
|
35
51
|
ws.on('message', async (raw) => {
|
|
36
52
|
let msg;
|
|
@@ -41,7 +57,7 @@ wss.on('connection', (ws) => {
|
|
|
41
57
|
JSON.stringify({
|
|
42
58
|
type: 'ERROR',
|
|
43
59
|
payload: { message: 'Invalid JSON payload' },
|
|
44
|
-
})
|
|
60
|
+
}),
|
|
45
61
|
);
|
|
46
62
|
return;
|
|
47
63
|
}
|
|
@@ -54,7 +70,7 @@ wss.on('connection', (ws) => {
|
|
|
54
70
|
|
|
55
71
|
if (msg.type === 'STYLE_CHANGE') {
|
|
56
72
|
const { source, key, value } = msg.payload;
|
|
57
|
-
await rewriteStyle({
|
|
73
|
+
await AstEngine.rewriteStyle({
|
|
58
74
|
file: source.file,
|
|
59
75
|
line: source.line,
|
|
60
76
|
column: source.column,
|
|
@@ -63,18 +79,65 @@ wss.on('connection', (ws) => {
|
|
|
63
79
|
});
|
|
64
80
|
ws.send(JSON.stringify({ type: 'ACK', payload: { success: true } }));
|
|
65
81
|
console.log(
|
|
66
|
-
`[rn-studio] ✓ ${source.componentName} → ${key}: ${value}
|
|
82
|
+
`[rn-studio] ✓ ${source.componentName} → ${key}: ${value}`,
|
|
67
83
|
);
|
|
84
|
+
broadcastStackState();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (msg.type === 'UNDO') {
|
|
89
|
+
const entry = UndoStack.undo();
|
|
90
|
+
if (entry) {
|
|
91
|
+
console.log(`[rn-studio] ↶ undo: ${entry.label} (${entry.file})`);
|
|
92
|
+
ws.send(
|
|
93
|
+
JSON.stringify({
|
|
94
|
+
type: 'ACK',
|
|
95
|
+
payload: { success: true, message: 'undo' },
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
ws.send(
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
type: 'ACK',
|
|
102
|
+
payload: { success: false, message: 'Nothing to undo' },
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
broadcastStackState();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (msg.type === 'REDO') {
|
|
111
|
+
const entry = UndoStack.redo();
|
|
112
|
+
if (entry) {
|
|
113
|
+
console.log(`[rn-studio] ↷ redo: ${entry.label} (${entry.file})`);
|
|
114
|
+
ws.send(
|
|
115
|
+
JSON.stringify({
|
|
116
|
+
type: 'ACK',
|
|
117
|
+
payload: { success: true, message: 'redo' },
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
ws.send(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
type: 'ACK',
|
|
124
|
+
payload: { success: false, message: 'Nothing to redo' },
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
broadcastStackState();
|
|
68
129
|
return;
|
|
69
130
|
}
|
|
70
131
|
|
|
71
132
|
if (msg.type === 'PROP_CHANGE') {
|
|
72
|
-
// Reserved for future prop editing. Ack for now.
|
|
73
133
|
ws.send(
|
|
74
134
|
JSON.stringify({
|
|
75
135
|
type: 'ACK',
|
|
76
|
-
payload: {
|
|
77
|
-
|
|
136
|
+
payload: {
|
|
137
|
+
success: true,
|
|
138
|
+
message: 'PROP_CHANGE not yet implemented',
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
78
141
|
);
|
|
79
142
|
return;
|
|
80
143
|
}
|
|
@@ -84,7 +147,7 @@ wss.on('connection', (ws) => {
|
|
|
84
147
|
JSON.stringify({
|
|
85
148
|
type: 'ERROR',
|
|
86
149
|
payload: { message: err && err.message ? err.message : String(err) },
|
|
87
|
-
})
|
|
150
|
+
}),
|
|
88
151
|
);
|
|
89
152
|
}
|
|
90
153
|
});
|
package/dist/StudioProvider.d.ts
CHANGED
|
@@ -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
|
|
12
|
-
* and consumed by `<SelectionOverlay>` for
|
|
13
|
-
*
|
|
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,
|
|
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;AA4LD,YAAY,EAAE,cAAc,EAAE,CAAC;AAE/B,0EAA0E;AAC1E,wBAAgB,SAAS,IAAI,kBAAkB,CAM9C"}
|
package/dist/StudioProvider.js
CHANGED
|
@@ -43,25 +43,15 @@ const WebSocketBridge_1 = require("./bridge/WebSocketBridge");
|
|
|
43
43
|
const FloatingBubble_1 = require("./components/FloatingBubble");
|
|
44
44
|
const SelectionOverlay_1 = require("./components/SelectionOverlay");
|
|
45
45
|
const InspectorPanel_1 = require("./components/InspectorPanel");
|
|
46
|
+
const persistence_1 = require("./utils/persistence");
|
|
47
|
+
const findFiberBySource_1 = require("./utils/findFiberBySource");
|
|
46
48
|
/**
|
|
47
|
-
* Shared mutable ref to the app root view. Populated by
|
|
48
|
-
* and consumed by `<SelectionOverlay>` for
|
|
49
|
-
*
|
|
50
|
-
* scope avoids having to thread it through context (and keeps the
|
|
51
|
-
* context value shape unchanged for consumers).
|
|
49
|
+
* Shared mutable ref to the app root view. Populated by
|
|
50
|
+
* `<StudioProvider>` and consumed by `<SelectionOverlay>` for
|
|
51
|
+
* hit-testing via `getInspectorDataForViewAtPoint`.
|
|
52
52
|
*/
|
|
53
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
53
54
|
exports.appRootRef = { current: null };
|
|
54
|
-
/**
|
|
55
|
-
* <StudioProvider>
|
|
56
|
-
*
|
|
57
|
-
* Wrap your App.tsx with this provider. When `enabled` is false (the
|
|
58
|
-
* default, intended for production), it renders children verbatim and
|
|
59
|
-
* introduces zero overhead — no context, no bridge, no overlay.
|
|
60
|
-
*
|
|
61
|
-
* When enabled, it manages the studio state machine, opens a WebSocket
|
|
62
|
-
* connection to the CLI server, and renders the floating bubble,
|
|
63
|
-
* selection overlay, and inspector panel above your app.
|
|
64
|
-
*/
|
|
65
55
|
function StudioProvider({ children, enabled = false, serverPort = 7878, bubblePosition = 'bottom-right', }) {
|
|
66
56
|
if (!enabled) {
|
|
67
57
|
return react_1.default.createElement(react_1.default.Fragment, null, children);
|
|
@@ -71,50 +61,124 @@ function StudioProvider({ children, enabled = false, serverPort = 7878, bubblePo
|
|
|
71
61
|
const StudioProviderInner = ({ serverPort, bubblePosition, children }) => {
|
|
72
62
|
const [state, setState] = (0, react_1.useState)('IDLE');
|
|
73
63
|
const [selectedComponent, setSelectedComponent] = (0, react_1.useState)(null);
|
|
64
|
+
const [canUndo, setCanUndo] = (0, react_1.useState)(false);
|
|
65
|
+
const [canRedo, setCanRedo] = (0, react_1.useState)(false);
|
|
74
66
|
const bridgeRef = (0, react_1.useRef)(null);
|
|
75
67
|
if (!bridgeRef.current) {
|
|
76
68
|
bridgeRef.current = new WebSocketBridge_1.WebSocketBridge(serverPort);
|
|
77
69
|
}
|
|
70
|
+
// Connect + subscribe to server messages.
|
|
78
71
|
(0, react_1.useEffect)(() => {
|
|
79
72
|
const bridge = bridgeRef.current;
|
|
80
73
|
bridge.connect();
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
const offStack = bridge.on('STACK_STATE', (msg) => {
|
|
75
|
+
if (msg.payload) {
|
|
76
|
+
setCanUndo(msg.payload.undo > 0);
|
|
77
|
+
setCanRedo(msg.payload.redo > 0);
|
|
78
|
+
}
|
|
84
79
|
});
|
|
85
80
|
return () => {
|
|
86
|
-
|
|
81
|
+
offStack();
|
|
87
82
|
bridge.disconnect();
|
|
88
83
|
};
|
|
89
84
|
}, []);
|
|
90
|
-
|
|
85
|
+
// Persist the last-selected source whenever it changes.
|
|
86
|
+
(0, react_1.useEffect)(() => {
|
|
87
|
+
if (selectedComponent) {
|
|
88
|
+
(0, persistence_1.saveLastSelection)(selectedComponent.source).catch(() => { });
|
|
89
|
+
}
|
|
90
|
+
}, [selectedComponent]);
|
|
91
|
+
// On initial mount, attempt to re-select the previously selected
|
|
92
|
+
// component (survives Cmd+R and Fast Refresh). Best-effort only.
|
|
93
|
+
(0, react_1.useEffect)(() => {
|
|
94
|
+
let cancelled = false;
|
|
95
|
+
(async () => {
|
|
96
|
+
const saved = await (0, persistence_1.loadLastSelection)();
|
|
97
|
+
if (!saved || cancelled)
|
|
98
|
+
return;
|
|
99
|
+
// Wait a frame for the fiber tree to commit.
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
if (cancelled)
|
|
102
|
+
return;
|
|
103
|
+
const fiber = (0, findFiberBySource_1.findFiberBySource)(saved);
|
|
104
|
+
if (!fiber)
|
|
105
|
+
return;
|
|
106
|
+
const props = (fiber.memoizedProps || {});
|
|
107
|
+
const node = {
|
|
108
|
+
id: `${saved.file}:${saved.line}:${saved.column}`,
|
|
109
|
+
componentName: saved.componentName,
|
|
110
|
+
source: saved,
|
|
111
|
+
props,
|
|
112
|
+
styles: [],
|
|
113
|
+
children: [],
|
|
114
|
+
};
|
|
115
|
+
// Silently restore in ACTIVE state so the user can tap the
|
|
116
|
+
// bubble to resume editing, without jarring re-opening the
|
|
117
|
+
// inspector automatically.
|
|
118
|
+
setSelectedComponent(node);
|
|
119
|
+
setState('ACTIVE');
|
|
120
|
+
}, 500);
|
|
121
|
+
})();
|
|
122
|
+
return () => {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
};
|
|
125
|
+
}, []);
|
|
126
|
+
const toggleActive = (0, react_1.useCallback)(() => {
|
|
91
127
|
setState((s) => {
|
|
92
128
|
if (s === 'IDLE')
|
|
93
129
|
return 'ACTIVE';
|
|
94
130
|
setSelectedComponent(null);
|
|
95
131
|
return 'IDLE';
|
|
96
132
|
});
|
|
97
|
-
};
|
|
98
|
-
const selectComponent = (node) => {
|
|
133
|
+
}, []);
|
|
134
|
+
const selectComponent = (0, react_1.useCallback)((node) => {
|
|
99
135
|
setSelectedComponent(node);
|
|
100
136
|
setState('SELECTED');
|
|
101
|
-
};
|
|
102
|
-
const clearSelection = () => {
|
|
137
|
+
}, []);
|
|
138
|
+
const clearSelection = (0, react_1.useCallback)(() => {
|
|
103
139
|
setSelectedComponent(null);
|
|
104
140
|
setState('ACTIVE');
|
|
105
|
-
};
|
|
106
|
-
const updateStyle = (key, value) => {
|
|
107
|
-
|
|
141
|
+
}, []);
|
|
142
|
+
const updateStyle = (0, react_1.useCallback)((key, value) => {
|
|
143
|
+
const current = selectedComponent;
|
|
144
|
+
if (!current)
|
|
108
145
|
return;
|
|
109
146
|
bridgeRef.current?.send({
|
|
110
147
|
type: 'STYLE_CHANGE',
|
|
111
|
-
payload: {
|
|
112
|
-
source: selectedComponent.source,
|
|
113
|
-
key,
|
|
114
|
-
value,
|
|
115
|
-
},
|
|
148
|
+
payload: { source: current.source, key, value },
|
|
116
149
|
});
|
|
117
|
-
|
|
150
|
+
// Optimistically update the local styles list so the editor
|
|
151
|
+
// reflects the new value without a round-trip.
|
|
152
|
+
setSelectedComponent({
|
|
153
|
+
...current,
|
|
154
|
+
styles: upsertLocalStyle(current.styles, key, value),
|
|
155
|
+
});
|
|
156
|
+
}, [selectedComponent]);
|
|
157
|
+
const addStyleProperty = (0, react_1.useCallback)((key, value) => {
|
|
158
|
+
const current = selectedComponent;
|
|
159
|
+
if (!current)
|
|
160
|
+
return;
|
|
161
|
+
// Booleans aren't writable via AST here yet; skip.
|
|
162
|
+
if (typeof value === 'boolean')
|
|
163
|
+
return;
|
|
164
|
+
bridgeRef.current?.send({
|
|
165
|
+
type: 'STYLE_CHANGE',
|
|
166
|
+
payload: { source: current.source, key, value },
|
|
167
|
+
});
|
|
168
|
+
setSelectedComponent({
|
|
169
|
+
...current,
|
|
170
|
+
styles: upsertLocalStyle(current.styles, key, value),
|
|
171
|
+
});
|
|
172
|
+
}, [selectedComponent]);
|
|
173
|
+
const undo = (0, react_1.useCallback)(() => {
|
|
174
|
+
bridgeRef.current?.send({ type: 'UNDO' });
|
|
175
|
+
}, []);
|
|
176
|
+
const redo = (0, react_1.useCallback)(() => {
|
|
177
|
+
bridgeRef.current?.send({ type: 'REDO' });
|
|
178
|
+
}, []);
|
|
179
|
+
const setAppRootRef = (0, react_1.useCallback)((r) => {
|
|
180
|
+
exports.appRootRef.current = r;
|
|
181
|
+
}, []);
|
|
118
182
|
const ctx = {
|
|
119
183
|
isActive: state !== 'IDLE',
|
|
120
184
|
isSelecting: state === 'SELECTING' || state === 'ACTIVE',
|
|
@@ -123,13 +187,11 @@ const StudioProviderInner = ({ serverPort, bubblePosition, children }) => {
|
|
|
123
187
|
selectComponent,
|
|
124
188
|
clearSelection,
|
|
125
189
|
updateStyle,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const setAppRootRef = (r) => {
|
|
132
|
-
exports.appRootRef.current = r;
|
|
190
|
+
addStyleProperty,
|
|
191
|
+
undo,
|
|
192
|
+
redo,
|
|
193
|
+
canUndo,
|
|
194
|
+
canRedo,
|
|
133
195
|
};
|
|
134
196
|
return (react_1.default.createElement(StudioContext_1.StudioContext.Provider, { value: ctx },
|
|
135
197
|
react_1.default.createElement(react_native_1.View, { ref: setAppRootRef, collapsable: false, style: { flex: 1 } }, children),
|
|
@@ -137,6 +199,21 @@ const StudioProviderInner = ({ serverPort, bubblePosition, children }) => {
|
|
|
137
199
|
react_1.default.createElement(InspectorPanel_1.InspectorPanel, null),
|
|
138
200
|
react_1.default.createElement(FloatingBubble_1.FloatingBubble, { position: bubblePosition })));
|
|
139
201
|
};
|
|
202
|
+
function upsertLocalStyle(list, key, value) {
|
|
203
|
+
const existing = list.findIndex((s) => s.key === key);
|
|
204
|
+
const type = typeof value === 'number'
|
|
205
|
+
? 'number'
|
|
206
|
+
: /color/i.test(key) || /^#[0-9a-f]{3,8}$/i.test(String(value))
|
|
207
|
+
? 'color'
|
|
208
|
+
: 'string';
|
|
209
|
+
const entry = { key, value, type };
|
|
210
|
+
if (existing >= 0) {
|
|
211
|
+
const next = list.slice();
|
|
212
|
+
next[existing] = entry;
|
|
213
|
+
return next;
|
|
214
|
+
}
|
|
215
|
+
return [...list, entry];
|
|
216
|
+
}
|
|
140
217
|
/** Hook for any descendant of `<StudioProvider>` to read studio state. */
|
|
141
218
|
function useStudio() {
|
|
142
219
|
const ctx = (0, react_1.useContext)(StudioContext_1.StudioContext);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StudioProvider.js","sourceRoot":"","sources":["../src/StudioProvider.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"StudioProvider.js","sourceRoot":"","sources":["../src/StudioProvider.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AstEngine.d.ts","sourceRoot":"","sources":["../../src/ast/AstEngine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AstEngine.d.ts","sourceRoot":"","sources":["../../src/ast/AstEngine.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CACxB;AAgHD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA6JtE"}
|
package/dist/ast/AstEngine.js
CHANGED
|
@@ -65,6 +65,7 @@ exports.rewriteStyle = rewriteStyle;
|
|
|
65
65
|
const parser = __importStar(require("@babel/parser"));
|
|
66
66
|
const recast = __importStar(require("recast"));
|
|
67
67
|
const fs = __importStar(require("fs"));
|
|
68
|
+
const UndoStack_1 = require("./UndoStack");
|
|
68
69
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
69
70
|
const b = recast.types.builders;
|
|
70
71
|
function buildLiteral(value) {
|
|
@@ -276,6 +277,15 @@ async function rewriteStyle(opts) {
|
|
|
276
277
|
throw new Error(`Matched JSX at ${file}:${line}:${column} but could not write style "${key}" — unsupported style expression form.`);
|
|
277
278
|
}
|
|
278
279
|
const output = recast.print(ast).code;
|
|
280
|
+
// No-op guard: don't touch the file if nothing actually changed.
|
|
281
|
+
if (output === source)
|
|
282
|
+
return;
|
|
279
283
|
fs.writeFileSync(file, output, 'utf-8');
|
|
284
|
+
(0, UndoStack_1.pushEdit)({
|
|
285
|
+
file,
|
|
286
|
+
before: source,
|
|
287
|
+
after: output,
|
|
288
|
+
label: `${key} = ${value}`,
|
|
289
|
+
});
|
|
280
290
|
}
|
|
281
291
|
//# sourceMappingURL=AstEngine.js.map
|