@emeryld/manager 1.3.0 → 1.4.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 +96 -0
- package/dist/create-package/cli-args.js +78 -0
- package/dist/create-package/prompts.js +138 -0
- package/dist/create-package/shared/configs.js +309 -0
- package/dist/create-package/shared/constants.js +5 -0
- package/dist/create-package/shared/fs-utils.js +69 -0
- package/dist/create-package/tasks.js +89 -0
- package/dist/create-package/types.js +1 -0
- package/dist/create-package/variant-info.js +67 -0
- package/dist/create-package/variants/client/expo-react-native/lib-files.js +168 -0
- package/dist/create-package/variants/client/expo-react-native/package-files.js +94 -0
- package/dist/create-package/variants/client/expo-react-native/scaffold.js +59 -0
- package/dist/create-package/variants/client/expo-react-native/ui-files.js +215 -0
- package/dist/create-package/variants/client/vite-react/health-page.js +251 -0
- package/dist/create-package/variants/client/vite-react/lib-files.js +176 -0
- package/dist/create-package/variants/client/vite-react/package-files.js +79 -0
- package/dist/create-package/variants/client/vite-react/scaffold.js +68 -0
- package/dist/create-package/variants/client/vite-react/ui-files.js +154 -0
- package/dist/create-package/variants/fullstack/files.js +129 -0
- package/dist/create-package/variants/fullstack/index.js +86 -0
- package/dist/create-package/variants/fullstack/utils.js +241 -0
- package/dist/llm-pack.js +2 -0
- package/dist/robot/cli/prompts.js +84 -27
- package/dist/robot/cli/settings.js +131 -56
- package/dist/robot/config.js +123 -50
- package/dist/robot/coordinator.js +10 -105
- package/dist/robot/extractors/classes.js +14 -13
- package/dist/robot/extractors/components.js +17 -10
- package/dist/robot/extractors/constants.js +9 -6
- package/dist/robot/extractors/functions.js +11 -8
- package/dist/robot/extractors/shared.js +6 -1
- package/dist/robot/extractors/types.js +5 -8
- package/dist/robot/llm-pack.js +1226 -0
- package/dist/robot/pack/builder.js +374 -0
- package/dist/robot/pack/cli.js +65 -0
- package/dist/robot/pack/exemplars.js +573 -0
- package/dist/robot/pack/globs.js +119 -0
- package/dist/robot/pack/selection.js +44 -0
- package/dist/robot/pack/symbols.js +309 -0
- package/dist/robot/pack/type-registry.js +285 -0
- package/dist/robot/pack/types.js +48 -0
- package/dist/robot/pack/utils.js +36 -0
- package/dist/robot/serializer.js +97 -0
- package/dist/robot/v2/cli.js +86 -0
- package/dist/robot/v2/globs.js +103 -0
- package/dist/robot/v2/parser/bundles.js +55 -0
- package/dist/robot/v2/parser/candidates.js +63 -0
- package/dist/robot/v2/parser/exemplars.js +114 -0
- package/dist/robot/v2/parser/exports.js +57 -0
- package/dist/robot/v2/parser/symbols.js +179 -0
- package/dist/robot/v2/parser.js +114 -0
- package/dist/robot/v2/types.js +42 -0
- package/dist/utils/export.js +39 -18
- package/package.json +2 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export function nativeAppTsx() {
|
|
2
|
+
return `import React from 'react'
|
|
3
|
+
import { SafeAreaView, ScrollView, StatusBar, StyleSheet, Text } from 'react-native'
|
|
4
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
+
import { queryClient, hasContract } from './src/queryClient'
|
|
6
|
+
import { AppSocketProvider } from './src/socket'
|
|
7
|
+
import { HealthScreen } from './src/screens/HealthScreen'
|
|
8
|
+
|
|
9
|
+
export default function App() {
|
|
10
|
+
return (
|
|
11
|
+
<QueryClientProvider client={queryClient}>
|
|
12
|
+
<AppSocketProvider>
|
|
13
|
+
<StatusBar barStyle="dark-content" />
|
|
14
|
+
<SafeAreaView style={styles.container}>
|
|
15
|
+
{!hasContract ? (
|
|
16
|
+
<Text style={styles.notice}>
|
|
17
|
+
Add your contract import in src/queryClient.ts and src/socket.tsx to enable
|
|
18
|
+
API + socket helpers.
|
|
19
|
+
</Text>
|
|
20
|
+
) : null}
|
|
21
|
+
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
|
22
|
+
<HealthScreen />
|
|
23
|
+
</ScrollView>
|
|
24
|
+
</SafeAreaView>
|
|
25
|
+
</AppSocketProvider>
|
|
26
|
+
</QueryClientProvider>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const styles = StyleSheet.create({
|
|
31
|
+
container: {
|
|
32
|
+
flex: 1,
|
|
33
|
+
padding: 16,
|
|
34
|
+
backgroundColor: '#f4f5fb',
|
|
35
|
+
},
|
|
36
|
+
notice: {
|
|
37
|
+
backgroundColor: '#fff7ed',
|
|
38
|
+
borderColor: '#fed7aa',
|
|
39
|
+
borderWidth: 1,
|
|
40
|
+
borderRadius: 8,
|
|
41
|
+
padding: 12,
|
|
42
|
+
marginBottom: 12,
|
|
43
|
+
color: '#9a3412',
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
export function nativeHealthScreen() {
|
|
49
|
+
return `import React from 'react'
|
|
50
|
+
import {
|
|
51
|
+
Button,
|
|
52
|
+
StyleSheet,
|
|
53
|
+
Text,
|
|
54
|
+
TextInput,
|
|
55
|
+
View,
|
|
56
|
+
} from 'react-native'
|
|
57
|
+
import { healthGet, healthPost, hasContract } from '../queryClient'
|
|
58
|
+
import { roomMeta, useSocketClient, useSocketConnection, socketReady } from '../socket'
|
|
59
|
+
|
|
60
|
+
const HEALTH_ROOMS = ['health']
|
|
61
|
+
|
|
62
|
+
function useHealthSocket(event: string, onMessage: (payload: Record<string, unknown>) => void) {
|
|
63
|
+
useSocketConnection({
|
|
64
|
+
event,
|
|
65
|
+
rooms: HEALTH_ROOMS,
|
|
66
|
+
joinMeta: roomMeta,
|
|
67
|
+
leaveMeta: roomMeta,
|
|
68
|
+
onMessage,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function useLogs() {
|
|
73
|
+
const [logs, setLogs] = React.useState<string[]>([])
|
|
74
|
+
return {
|
|
75
|
+
logs,
|
|
76
|
+
push: (msg: string) =>
|
|
77
|
+
setLogs((prev) => ['[' + new Date().toLocaleTimeString() + '] ' + msg, ...prev].slice(0, 60)),
|
|
78
|
+
clear: () => setLogs([]),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function HealthScreen() {
|
|
83
|
+
const [echo, setEcho] = React.useState('hello rrroute')
|
|
84
|
+
const httpGet = healthGet.useEndpoint()
|
|
85
|
+
const httpPost = healthPost.useEndpoint()
|
|
86
|
+
const socket = useSocketClient()
|
|
87
|
+
const { logs, push, clear } = useLogs()
|
|
88
|
+
|
|
89
|
+
useHealthSocket('health:connected', (payload) => {
|
|
90
|
+
push('connected ' + String(payload.socketId))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
useHealthSocket('health:pong', (payload) => {
|
|
94
|
+
const echo = payload.echo ? ' (echo: ' + String(payload.echo) + ')' : ''
|
|
95
|
+
push('pong at ' + String(payload.at) + echo)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<View style={styles.card}>
|
|
100
|
+
<Text style={styles.title}>RRRoutes health sandbox</Text>
|
|
101
|
+
<View style={styles.section}>
|
|
102
|
+
<Text style={styles.heading}>HTTP</Text>
|
|
103
|
+
{!hasContract ? (
|
|
104
|
+
<Text style={styles.muted}>
|
|
105
|
+
Wire up a contract to enable typed HTTP calls.
|
|
106
|
+
</Text>
|
|
107
|
+
) : (
|
|
108
|
+
<>
|
|
109
|
+
<Button title="GET /health" onPress={() => httpGet.refetch()} />
|
|
110
|
+
<View style={{ height: 12 }} />
|
|
111
|
+
<TextInput
|
|
112
|
+
style={styles.input}
|
|
113
|
+
value={echo}
|
|
114
|
+
onChangeText={setEcho}
|
|
115
|
+
placeholder="echo payload"
|
|
116
|
+
/>
|
|
117
|
+
<Button
|
|
118
|
+
title="POST /health"
|
|
119
|
+
onPress={() =>
|
|
120
|
+
httpPost
|
|
121
|
+
.mutateAsync({ echo })
|
|
122
|
+
.then(() => push('POST /health ok'))
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
125
|
+
<Text style={styles.label}>GET data</Text>
|
|
126
|
+
<Text style={styles.code}>
|
|
127
|
+
{JSON.stringify(httpGet.data ?? {}, null, 2)}
|
|
128
|
+
</Text>
|
|
129
|
+
<Text style={styles.label}>POST data</Text>
|
|
130
|
+
<Text style={styles.code}>
|
|
131
|
+
{JSON.stringify(httpPost.data ?? {}, null, 2)}
|
|
132
|
+
</Text>
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</View>
|
|
136
|
+
|
|
137
|
+
<View style={styles.section}>
|
|
138
|
+
<Text style={styles.heading}>Socket</Text>
|
|
139
|
+
{!socketReady ? (
|
|
140
|
+
<Text style={styles.muted}>
|
|
141
|
+
Wire up socket events via your contract to enable live updates.
|
|
142
|
+
</Text>
|
|
143
|
+
) : (
|
|
144
|
+
<>
|
|
145
|
+
<View style={styles.row}>
|
|
146
|
+
<Button title="Connect" onPress={() => socket.connect()} />
|
|
147
|
+
<Button title="Disconnect" onPress={() => socket.disconnect()} />
|
|
148
|
+
</View>
|
|
149
|
+
<View style={{ height: 8 }} />
|
|
150
|
+
<Button
|
|
151
|
+
title="Emit ping"
|
|
152
|
+
onPress={() =>
|
|
153
|
+
socket.emit('health:ping', { note: 'ping from app' })
|
|
154
|
+
}
|
|
155
|
+
/>
|
|
156
|
+
<View style={{ height: 8 }} />
|
|
157
|
+
<View style={styles.row}>
|
|
158
|
+
<Button
|
|
159
|
+
title="Join room"
|
|
160
|
+
onPress={() => socket.joinRooms(HEALTH_ROOMS, roomMeta)}
|
|
161
|
+
/>
|
|
162
|
+
<Button
|
|
163
|
+
title="Leave room"
|
|
164
|
+
onPress={() => socket.leaveRooms(HEALTH_ROOMS, roomMeta)}
|
|
165
|
+
/>
|
|
166
|
+
</View>
|
|
167
|
+
</>
|
|
168
|
+
)}
|
|
169
|
+
</View>
|
|
170
|
+
|
|
171
|
+
<View style={styles.section}>
|
|
172
|
+
<Text style={styles.heading}>Socket logs</Text>
|
|
173
|
+
<Button title="Clear logs" onPress={clear} />
|
|
174
|
+
<Text style={styles.code}>
|
|
175
|
+
{logs.length === 0 ? 'No messages yet' : logs.join('\\n')}
|
|
176
|
+
</Text>
|
|
177
|
+
</View>
|
|
178
|
+
</View>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const styles = StyleSheet.create({
|
|
183
|
+
card: {
|
|
184
|
+
backgroundColor: '#fff',
|
|
185
|
+
borderRadius: 12,
|
|
186
|
+
padding: 16,
|
|
187
|
+
gap: 12,
|
|
188
|
+
shadowColor: '#000',
|
|
189
|
+
shadowOpacity: 0.05,
|
|
190
|
+
shadowRadius: 8,
|
|
191
|
+
},
|
|
192
|
+
title: { fontSize: 20, fontWeight: '700' },
|
|
193
|
+
section: { gap: 8 },
|
|
194
|
+
heading: { fontWeight: '600', fontSize: 16 },
|
|
195
|
+
row: { flexDirection: 'row', gap: 8, justifyContent: 'space-between' },
|
|
196
|
+
input: {
|
|
197
|
+
borderWidth: 1,
|
|
198
|
+
borderColor: '#d0d4de',
|
|
199
|
+
borderRadius: 8,
|
|
200
|
+
padding: 8,
|
|
201
|
+
},
|
|
202
|
+
label: { marginTop: 6, fontWeight: '600' },
|
|
203
|
+
code: {
|
|
204
|
+
backgroundColor: '#0f172a',
|
|
205
|
+
color: '#e2e8f0',
|
|
206
|
+
padding: 8,
|
|
207
|
+
borderRadius: 8,
|
|
208
|
+
fontFamily: 'Courier',
|
|
209
|
+
},
|
|
210
|
+
muted: {
|
|
211
|
+
color: '#6b7280',
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
export function HealthPage() {
|
|
2
|
+
return `import React from 'react'
|
|
3
|
+
import {
|
|
4
|
+
Bar,
|
|
5
|
+
BarChart,
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
Tooltip,
|
|
8
|
+
XAxis,
|
|
9
|
+
YAxis,
|
|
10
|
+
} from 'recharts'
|
|
11
|
+
import { healthGet, healthPost, hasContract } from '../lib/queryClient'
|
|
12
|
+
import {
|
|
13
|
+
roomMeta,
|
|
14
|
+
socketReady,
|
|
15
|
+
useSocketClient,
|
|
16
|
+
useSocketConnection,
|
|
17
|
+
} from '../lib/socket'
|
|
18
|
+
|
|
19
|
+
const HEALTH_ROOMS = ['health']
|
|
20
|
+
|
|
21
|
+
function now() {
|
|
22
|
+
return new Date().toLocaleTimeString()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function useHealthSocket(event: string, onMessage: (payload: Record<string, unknown>) => void) {
|
|
26
|
+
useSocketConnection({
|
|
27
|
+
event,
|
|
28
|
+
rooms: HEALTH_ROOMS,
|
|
29
|
+
joinMeta: roomMeta,
|
|
30
|
+
leaveMeta: roomMeta,
|
|
31
|
+
onMessage,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function useLogs() {
|
|
36
|
+
const [logs, setLogs] = React.useState<string[]>([])
|
|
37
|
+
return {
|
|
38
|
+
logs,
|
|
39
|
+
push: (msg: string) =>
|
|
40
|
+
setLogs((prev) => ['[' + now() + '] ' + msg, ...prev].slice(0, 60)),
|
|
41
|
+
clear: () => setLogs([]),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type HttpSectionProps = {
|
|
46
|
+
echo: string
|
|
47
|
+
onEchoChange: (value: string) => void
|
|
48
|
+
onLog: (message: string) => void
|
|
49
|
+
httpGet: ReturnType<typeof healthGet.useEndpoint>
|
|
50
|
+
httpPost: ReturnType<typeof healthPost.useEndpoint>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function HttpSection({ echo, onEchoChange, onLog, httpGet, httpPost }: HttpSectionProps) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="card">
|
|
56
|
+
<h2>HTTP endpoints</h2>
|
|
57
|
+
{!hasContract ? (
|
|
58
|
+
<p className="muted">Wire up a contract to enable HTTP calls.</p>
|
|
59
|
+
) : (
|
|
60
|
+
<>
|
|
61
|
+
<p>GET and POST against the shared contract.</p>
|
|
62
|
+
<div>
|
|
63
|
+
<button onClick={() => httpGet.refetch()}>GET /health</button>
|
|
64
|
+
{httpGet.error ? (
|
|
65
|
+
<p style={{ color: 'crimson', marginTop: 6 }}>
|
|
66
|
+
GET error: {String(httpGet.error)}
|
|
67
|
+
</p>
|
|
68
|
+
) : null}
|
|
69
|
+
</div>
|
|
70
|
+
<div style={{ marginTop: 12 }}>
|
|
71
|
+
<input
|
|
72
|
+
value={echo}
|
|
73
|
+
onChange={(e) => onEchoChange(e.target.value)}
|
|
74
|
+
placeholder="echo payload"
|
|
75
|
+
style={{ width: '100%', padding: '8px' }}
|
|
76
|
+
/>
|
|
77
|
+
<div style={{ marginTop: 8 }}>
|
|
78
|
+
<button
|
|
79
|
+
onClick={() =>
|
|
80
|
+
httpPost.mutateAsync({ echo }).then(() => onLog('POST /health ok'))
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
POST /health
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
{httpPost.error ? (
|
|
87
|
+
<p style={{ color: 'crimson', marginTop: 6 }}>
|
|
88
|
+
POST error: {String(httpPost.error)}
|
|
89
|
+
</p>
|
|
90
|
+
) : null}
|
|
91
|
+
</div>
|
|
92
|
+
<div style={{ marginTop: 12 }}>
|
|
93
|
+
<strong>GET data</strong>
|
|
94
|
+
<pre>{JSON.stringify(httpGet.data, null, 2) ?? 'none'}</pre>
|
|
95
|
+
<strong>POST data</strong>
|
|
96
|
+
<pre>{JSON.stringify(httpPost.data, null, 2) ?? 'none'}</pre>
|
|
97
|
+
</div>
|
|
98
|
+
</>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type SocketSectionProps = {
|
|
105
|
+
socket: ReturnType<typeof useSocketClient>
|
|
106
|
+
onLog: (message: string) => void
|
|
107
|
+
hasError: boolean
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function SocketSection({ socket, onLog, hasError }: SocketSectionProps) {
|
|
111
|
+
return (
|
|
112
|
+
<div className="card">
|
|
113
|
+
<h2>Socket</h2>
|
|
114
|
+
{!socketReady ? (
|
|
115
|
+
<p className="muted">
|
|
116
|
+
Wire up socket events via your contract to enable live updates.
|
|
117
|
+
</p>
|
|
118
|
+
) : (
|
|
119
|
+
<>
|
|
120
|
+
<p>Connect, ping, and watch lifecycle events.</p>
|
|
121
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
122
|
+
<button onClick={() => socket.connect()}>Connect</button>
|
|
123
|
+
<button className="secondary" onClick={() => socket.disconnect()}>
|
|
124
|
+
Disconnect
|
|
125
|
+
</button>
|
|
126
|
+
<button onClick={() => socket.emit('health:ping', { note: 'ping from client' })}>
|
|
127
|
+
Emit ping
|
|
128
|
+
</button>
|
|
129
|
+
<button
|
|
130
|
+
className="secondary"
|
|
131
|
+
onClick={() => socket.joinRooms(HEALTH_ROOMS, roomMeta)}
|
|
132
|
+
>
|
|
133
|
+
Join room
|
|
134
|
+
</button>
|
|
135
|
+
<button
|
|
136
|
+
className="secondary"
|
|
137
|
+
onClick={() => socket.leaveRooms(HEALTH_ROOMS, roomMeta)}
|
|
138
|
+
>
|
|
139
|
+
Leave room
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
{hasError ? (
|
|
143
|
+
<p style={{ color: 'crimson', marginTop: 8 }}>
|
|
144
|
+
Check server logs; errors will also show above.
|
|
145
|
+
</p>
|
|
146
|
+
) : null}
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
type LogsSectionProps = {
|
|
154
|
+
logs: string[]
|
|
155
|
+
onClear: () => void
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function LogsSection({ logs, onClear }: LogsSectionProps) {
|
|
159
|
+
return (
|
|
160
|
+
<div className="card">
|
|
161
|
+
<h2>Socket logs</h2>
|
|
162
|
+
<button className="secondary" onClick={onClear}>
|
|
163
|
+
Clear
|
|
164
|
+
</button>
|
|
165
|
+
<div className="logs">
|
|
166
|
+
{logs.length === 0 ? 'No messages yet' : logs.join('\\n')}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type ChartSectionProps = {
|
|
173
|
+
chartData: { name: string; value: number }[]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ChartSection({ chartData }: ChartSectionProps) {
|
|
177
|
+
return (
|
|
178
|
+
<div className="card chart-card">
|
|
179
|
+
<h2>Live state</h2>
|
|
180
|
+
<div className="chart-container">
|
|
181
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
182
|
+
<BarChart data={chartData}>
|
|
183
|
+
<XAxis dataKey="name" />
|
|
184
|
+
<YAxis />
|
|
185
|
+
<Tooltip />
|
|
186
|
+
<Bar dataKey="value" fill="#2563eb" radius={[4, 4, 0, 0]} />
|
|
187
|
+
</BarChart>
|
|
188
|
+
</ResponsiveContainer>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function HealthPage() {
|
|
195
|
+
const [echo, setEcho] = React.useState('hello rrroute')
|
|
196
|
+
const httpGet = healthGet.useEndpoint()
|
|
197
|
+
const httpPost = healthPost.useEndpoint()
|
|
198
|
+
const socket = useSocketClient()
|
|
199
|
+
const { logs, push, clear } = useLogs()
|
|
200
|
+
|
|
201
|
+
const chartData = React.useMemo(
|
|
202
|
+
() => [
|
|
203
|
+
{ name: 'Logs', value: logs.length },
|
|
204
|
+
{ name: 'Socket ready', value: socketReady ? 1 : 0 },
|
|
205
|
+
{
|
|
206
|
+
name: 'HTTP errors',
|
|
207
|
+
value: Number(Boolean(httpGet.error || httpPost.error)),
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
[logs.length, socketReady, httpGet.error, httpPost.error],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
useHealthSocket('health:connected', (payload) => {
|
|
214
|
+
push('socket connected (' + String(payload.socketId) + ')')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
useHealthSocket('health:pong', (payload) => {
|
|
218
|
+
const echo = payload.echo ? ' (echo: ' + String(payload.echo) + ')' : ''
|
|
219
|
+
push('pong at ' + String(payload.at) + echo)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div className="health">
|
|
224
|
+
<h1>RRRoutes health sandbox</h1>
|
|
225
|
+
{!hasContract ? (
|
|
226
|
+
<p className="notice">
|
|
227
|
+
Add your contract import in <code>src/lib/queryClient.ts</code> and{' '}
|
|
228
|
+
<code>src/lib/socket.tsx</code> to call the real API and socket events.
|
|
229
|
+
</p>
|
|
230
|
+
) : null}
|
|
231
|
+
<div className="grid">
|
|
232
|
+
<HttpSection
|
|
233
|
+
echo={echo}
|
|
234
|
+
onEchoChange={setEcho}
|
|
235
|
+
onLog={push}
|
|
236
|
+
httpGet={httpGet}
|
|
237
|
+
httpPost={httpPost}
|
|
238
|
+
/>
|
|
239
|
+
<SocketSection
|
|
240
|
+
socket={socket}
|
|
241
|
+
onLog={push}
|
|
242
|
+
hasError={Boolean(httpGet.error || httpPost.error)}
|
|
243
|
+
/>
|
|
244
|
+
<LogsSection logs={logs} onClear={clear} />
|
|
245
|
+
<ChartSection chartData={chartData} />
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
export function queryClient(contractImport) {
|
|
2
|
+
if (contractImport) {
|
|
3
|
+
return `import { QueryClient } from '@tanstack/react-query'
|
|
4
|
+
import { createRouteClient } from '@emeryld/rrroutes-client'
|
|
5
|
+
import { registry } from '${contractImport}'
|
|
6
|
+
|
|
7
|
+
const baseUrl = import.meta.env.VITE_API_URL ?? 'http://localhost:4000'
|
|
8
|
+
|
|
9
|
+
export const queryClient = new QueryClient()
|
|
10
|
+
|
|
11
|
+
export const routeClient = createRouteClient({
|
|
12
|
+
baseUrl,
|
|
13
|
+
queryClient,
|
|
14
|
+
environment: import.meta.env.MODE === 'production' ? 'production' : 'development',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
|
|
18
|
+
export const healthPost = routeClient.build(registry.byKey['POST /api/health'])
|
|
19
|
+
export const hasContract = true as const
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
return `import { QueryClient } from '@tanstack/react-query'
|
|
23
|
+
import { createRouteClient } from '@emeryld/rrroutes-client'
|
|
24
|
+
|
|
25
|
+
const baseUrl = import.meta.env.VITE_API_URL ?? 'http://localhost:4000'
|
|
26
|
+
|
|
27
|
+
export const queryClient = new QueryClient()
|
|
28
|
+
|
|
29
|
+
export const routeClient = createRouteClient({
|
|
30
|
+
baseUrl,
|
|
31
|
+
queryClient,
|
|
32
|
+
environment: import.meta.env.MODE === 'production' ? 'production' : 'development',
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
type RouteEndpoint = ReturnType<typeof routeClient.build>
|
|
36
|
+
|
|
37
|
+
function placeholderUseEndpoint() {
|
|
38
|
+
const error = new Error('Add your contract import to src/lib/queryClient.ts')
|
|
39
|
+
return {
|
|
40
|
+
data: undefined,
|
|
41
|
+
error,
|
|
42
|
+
refetch: () => undefined,
|
|
43
|
+
mutateAsync: async () => Promise.reject(error),
|
|
44
|
+
isPending: false,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const placeholderEndpoint = {
|
|
49
|
+
useEndpoint: placeholderUseEndpoint,
|
|
50
|
+
} as unknown as RouteEndpoint
|
|
51
|
+
|
|
52
|
+
export const healthGet: RouteEndpoint = placeholderEndpoint
|
|
53
|
+
export const healthPost: RouteEndpoint = placeholderEndpoint
|
|
54
|
+
export const hasContract = false as const
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
export function socketProvider(contractImport) {
|
|
58
|
+
if (contractImport) {
|
|
59
|
+
return `import React from 'react'
|
|
60
|
+
import { io, type Socket } from 'socket.io-client'
|
|
61
|
+
import {
|
|
62
|
+
buildSocketProvider,
|
|
63
|
+
type SocketClientOptions,
|
|
64
|
+
} from '@emeryld/rrroutes-client'
|
|
65
|
+
import { socketConfig, socketEvents } from '${contractImport}'
|
|
66
|
+
|
|
67
|
+
const socketUrl = import.meta.env.VITE_SOCKET_URL ?? 'http://localhost:4000'
|
|
68
|
+
const socketPath = import.meta.env.VITE_SOCKET_PATH ?? '/socket.io'
|
|
69
|
+
|
|
70
|
+
const sysEvents: SocketClientOptions<
|
|
71
|
+
typeof socketEvents,
|
|
72
|
+
typeof socketConfig
|
|
73
|
+
>['sys'] = {
|
|
74
|
+
'sys:connect': async ({ socket }) => {
|
|
75
|
+
console.info('socket connected', socket.id)
|
|
76
|
+
},
|
|
77
|
+
'sys:disconnect': async ({ reason }) => {
|
|
78
|
+
console.info('socket disconnected', reason)
|
|
79
|
+
},
|
|
80
|
+
'sys:reconnect': async ({ attempt, socket }) => {
|
|
81
|
+
console.info('socket reconnect', attempt, socket?.id)
|
|
82
|
+
},
|
|
83
|
+
'sys:connect_error': async ({ error }) => {
|
|
84
|
+
console.warn('socket connect error', error)
|
|
85
|
+
},
|
|
86
|
+
'sys:ping': () => ({
|
|
87
|
+
note: 'client-heartbeat',
|
|
88
|
+
sentAt: new Date().toISOString(),
|
|
89
|
+
}),
|
|
90
|
+
'sys:pong': async ({ payload }) => {
|
|
91
|
+
console.info('socket pong', payload)
|
|
92
|
+
},
|
|
93
|
+
'sys:room_join': async ({ rooms }) => {
|
|
94
|
+
console.info('joining rooms', rooms)
|
|
95
|
+
return true
|
|
96
|
+
},
|
|
97
|
+
'sys:room_leave': async ({ rooms }) => {
|
|
98
|
+
console.info('leaving rooms', rooms)
|
|
99
|
+
return true
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const baseOptions: Omit<
|
|
104
|
+
SocketClientOptions<typeof socketEvents, typeof socketConfig>,
|
|
105
|
+
'socket'
|
|
106
|
+
> = {
|
|
107
|
+
config: socketConfig,
|
|
108
|
+
sys: sysEvents,
|
|
109
|
+
environment: import.meta.env.MODE === 'production' ? 'production' : 'development',
|
|
110
|
+
heartbeat: { intervalMs: 15_000, timeoutMs: 7_500 },
|
|
111
|
+
debug: {
|
|
112
|
+
connection: true,
|
|
113
|
+
heartbeat: true,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { SocketProvider, useSocketClient, useSocketConnection } =
|
|
118
|
+
buildSocketProvider({
|
|
119
|
+
events: socketEvents,
|
|
120
|
+
options: baseOptions,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
function getSocket(): Promise<Socket> {
|
|
124
|
+
return Promise.resolve(
|
|
125
|
+
io(socketUrl, {
|
|
126
|
+
path: socketPath,
|
|
127
|
+
transports: ['websocket'],
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const roomMeta = { room: 'health' }
|
|
133
|
+
export const socketReady = true as const
|
|
134
|
+
|
|
135
|
+
export function AppSocketProvider(props: React.PropsWithChildren) {
|
|
136
|
+
return (
|
|
137
|
+
<SocketProvider
|
|
138
|
+
getSocket={getSocket}
|
|
139
|
+
destroyLeaveMeta={roomMeta}
|
|
140
|
+
fallback={<p>Connecting socket…</p>}
|
|
141
|
+
>
|
|
142
|
+
{props.children}
|
|
143
|
+
</SocketProvider>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { useSocketClient, useSocketConnection }
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
return `import React from 'react'
|
|
151
|
+
|
|
152
|
+
export const roomMeta = { room: 'health' }
|
|
153
|
+
export const socketReady = false as const
|
|
154
|
+
|
|
155
|
+
const stubSocket = {
|
|
156
|
+
connect: () =>
|
|
157
|
+
console.info('Socket disabled; add your contract import in src/lib/socket.tsx.'),
|
|
158
|
+
disconnect: () => undefined,
|
|
159
|
+
emit: () => undefined,
|
|
160
|
+
joinRooms: () => undefined,
|
|
161
|
+
leaveRooms: () => undefined,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function AppSocketProvider(props: React.PropsWithChildren) {
|
|
165
|
+
return <>{props.children}</>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function useSocketClient() {
|
|
169
|
+
return stubSocket
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function useSocketConnection() {
|
|
173
|
+
// no-op when sockets are not configured
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
}
|