@emeryld/manager 0.5.1 → 0.6.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 +55 -19
- package/dist/create-package/index.js +30 -1
- package/dist/create-package/variants/client/client_expo_rn.js +530 -0
- package/dist/create-package/variants/client/client_vite_r.js +583 -0
- package/dist/create-package/variants/client/shared.js +56 -0
- package/dist/create-package/variants/client.js +1 -122
- package/dist/create-package/variants/fullstack.js +262 -10
- package/dist/menu.js +17 -10
- package/package.json +1 -1
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { BASE_LINT_DEV_DEPENDENCIES, basePackageFiles, basePackageJson, baseScripts, buildReadme, isWorkspaceRoot, packageTsConfig, writeFileIfMissing, } from '../../shared.js';
|
|
2
|
+
const EXPO_CLIENT_SCRIPTS = [
|
|
3
|
+
'dev',
|
|
4
|
+
'build',
|
|
5
|
+
'typecheck',
|
|
6
|
+
'lint',
|
|
7
|
+
'lint:fix',
|
|
8
|
+
'format',
|
|
9
|
+
'format:check',
|
|
10
|
+
'clean',
|
|
11
|
+
'start',
|
|
12
|
+
'android',
|
|
13
|
+
'ios',
|
|
14
|
+
'web',
|
|
15
|
+
'test',
|
|
16
|
+
];
|
|
17
|
+
function slugify(name) {
|
|
18
|
+
const cleaned = name.replace(/^@/, '').replace(/\//g, '-');
|
|
19
|
+
const slug = cleaned
|
|
20
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join('-')
|
|
23
|
+
.toLowerCase();
|
|
24
|
+
return slug || 'rrr-client';
|
|
25
|
+
}
|
|
26
|
+
function nativePackageJson(name, contractName, includePrepare) {
|
|
27
|
+
const dependencies = {
|
|
28
|
+
'@emeryld/rrroutes-client': '^2.5.3',
|
|
29
|
+
'@tanstack/react-query': '^5.90.12',
|
|
30
|
+
expo: '~52.0.8',
|
|
31
|
+
'expo-constants': '~16.0.2',
|
|
32
|
+
'expo-status-bar': '~2.0.1',
|
|
33
|
+
react: '18.3.1',
|
|
34
|
+
'react-native': '0.76.6',
|
|
35
|
+
'react-native-safe-area-context': '4.12.0',
|
|
36
|
+
'react-native-screens': '4.4.0',
|
|
37
|
+
'socket.io-client': '^4.8.3',
|
|
38
|
+
zod: '^4.2.1',
|
|
39
|
+
};
|
|
40
|
+
if (contractName)
|
|
41
|
+
dependencies[contractName] = 'workspace:*';
|
|
42
|
+
const devDependencies = {
|
|
43
|
+
...BASE_LINT_DEV_DEPENDENCIES,
|
|
44
|
+
'@babel/core': '^7.25.2',
|
|
45
|
+
'@types/react': '~18.2.79',
|
|
46
|
+
'@types/react-native': '~0.73.0',
|
|
47
|
+
'babel-preset-expo': '^11.0.0',
|
|
48
|
+
typescript: '^5.9.3',
|
|
49
|
+
};
|
|
50
|
+
return basePackageJson({
|
|
51
|
+
name,
|
|
52
|
+
main: 'expo/AppEntry',
|
|
53
|
+
useDefaults: false,
|
|
54
|
+
scripts: baseScripts('expo start', {
|
|
55
|
+
start: 'expo start',
|
|
56
|
+
android: 'expo start --android',
|
|
57
|
+
ios: 'expo start --ios',
|
|
58
|
+
web: 'expo start --web',
|
|
59
|
+
build: 'tsc -p tsconfig.json --noEmit',
|
|
60
|
+
test: 'echo "add tests"',
|
|
61
|
+
}, { includePrepare }),
|
|
62
|
+
dependencies,
|
|
63
|
+
devDependencies,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function nativeAppJson(pkgName) {
|
|
67
|
+
const baseSlug = slugify(pkgName);
|
|
68
|
+
const slug = `${baseSlug}-client`;
|
|
69
|
+
return `${JSON.stringify({
|
|
70
|
+
expo: {
|
|
71
|
+
name: slug,
|
|
72
|
+
slug,
|
|
73
|
+
version: '0.1.0',
|
|
74
|
+
orientation: 'portrait',
|
|
75
|
+
scheme: baseSlug,
|
|
76
|
+
platforms: ['ios', 'android', 'web'],
|
|
77
|
+
userInterfaceStyle: 'light',
|
|
78
|
+
extra: {
|
|
79
|
+
apiUrl: 'http://localhost:4000',
|
|
80
|
+
socketUrl: 'http://localhost:4000',
|
|
81
|
+
socketPath: '/socket.io',
|
|
82
|
+
},
|
|
83
|
+
experiments: {
|
|
84
|
+
typedRoutes: false,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
}, null, 2)}\n`;
|
|
88
|
+
}
|
|
89
|
+
const NATIVE_BABEL = `module.exports = function (api) {
|
|
90
|
+
api.cache(true)
|
|
91
|
+
return {
|
|
92
|
+
presets: ['babel-preset-expo'],
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
function nativeQueryClient(contractImport) {
|
|
97
|
+
if (contractImport) {
|
|
98
|
+
return `import Constants from 'expo-constants'
|
|
99
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
100
|
+
import { createRouteClient } from '@emeryld/rrroutes-client'
|
|
101
|
+
import { registry } from '${contractImport}'
|
|
102
|
+
|
|
103
|
+
const { apiUrl } = (Constants.expoConfig?.extra ?? {}) as Record<string, string>
|
|
104
|
+
|
|
105
|
+
export const queryClient = new QueryClient()
|
|
106
|
+
|
|
107
|
+
export const routeClient = createRouteClient({
|
|
108
|
+
baseUrl: apiUrl ?? 'http://localhost:4000',
|
|
109
|
+
queryClient,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
export const healthGet = routeClient.build(registry.byKey['GET /api/health'])
|
|
113
|
+
export const healthPost = routeClient.build(registry.byKey['POST /api/health'])
|
|
114
|
+
export const hasContract = true as const
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
return `import Constants from 'expo-constants'
|
|
118
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
119
|
+
import { createRouteClient } from '@emeryld/rrroutes-client'
|
|
120
|
+
|
|
121
|
+
const { apiUrl } = (Constants.expoConfig?.extra ?? {}) as Record<string, string>
|
|
122
|
+
|
|
123
|
+
export const queryClient = new QueryClient()
|
|
124
|
+
|
|
125
|
+
export const routeClient = createRouteClient({
|
|
126
|
+
baseUrl: apiUrl ?? 'http://localhost:4000',
|
|
127
|
+
queryClient,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
type RouteEndpoint = ReturnType<typeof routeClient.build>
|
|
131
|
+
|
|
132
|
+
function placeholderUseEndpoint() {
|
|
133
|
+
const error = new Error('Add your contract import to src/queryClient.ts')
|
|
134
|
+
return {
|
|
135
|
+
data: undefined,
|
|
136
|
+
error,
|
|
137
|
+
refetch: () => undefined,
|
|
138
|
+
mutateAsync: async () => Promise.reject(error),
|
|
139
|
+
isPending: false,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const placeholderEndpoint = {
|
|
144
|
+
useEndpoint: placeholderUseEndpoint,
|
|
145
|
+
} as unknown as RouteEndpoint
|
|
146
|
+
|
|
147
|
+
export const healthGet: RouteEndpoint = placeholderEndpoint
|
|
148
|
+
export const healthPost: RouteEndpoint = placeholderEndpoint
|
|
149
|
+
export const hasContract = false as const
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
function nativeSocket(contractImport) {
|
|
153
|
+
if (contractImport) {
|
|
154
|
+
return `import React from 'react'
|
|
155
|
+
import Constants from 'expo-constants'
|
|
156
|
+
import { io, type Socket } from 'socket.io-client'
|
|
157
|
+
import {
|
|
158
|
+
buildSocketProvider,
|
|
159
|
+
type SocketClientOptions,
|
|
160
|
+
} from '@emeryld/rrroutes-client'
|
|
161
|
+
import { socketConfig, socketEvents } from '${contractImport}'
|
|
162
|
+
|
|
163
|
+
const extras = (Constants.expoConfig?.extra ?? {}) as Record<string, string>
|
|
164
|
+
const socketUrl = extras.socketUrl ?? 'http://localhost:4000'
|
|
165
|
+
const socketPath = extras.socketPath ?? '/socket.io'
|
|
166
|
+
|
|
167
|
+
const sysEvents: SocketClientOptions<
|
|
168
|
+
typeof socketEvents,
|
|
169
|
+
typeof socketConfig
|
|
170
|
+
>['sys'] = {
|
|
171
|
+
'sys:connect': async ({ socket }) => {
|
|
172
|
+
console.log('socket connected', socket.id)
|
|
173
|
+
},
|
|
174
|
+
'sys:disconnect': async ({ reason }) => console.log('disconnected', reason),
|
|
175
|
+
'sys:reconnect': async ({ attempt, socket }) =>
|
|
176
|
+
console.log('reconnect', attempt, socket?.id),
|
|
177
|
+
'sys:connect_error': async ({ error }) =>
|
|
178
|
+
console.warn('socket connect error', error),
|
|
179
|
+
'sys:ping': () => ({
|
|
180
|
+
note: 'client-heartbeat',
|
|
181
|
+
sentAt: new Date().toISOString(),
|
|
182
|
+
}),
|
|
183
|
+
'sys:pong': async ({ payload }) => console.log('pong', payload),
|
|
184
|
+
'sys:room_join': async ({ rooms }) => {
|
|
185
|
+
console.log('join rooms', rooms)
|
|
186
|
+
return true
|
|
187
|
+
},
|
|
188
|
+
'sys:room_leave': async ({ rooms }) => {
|
|
189
|
+
console.log('leave rooms', rooms)
|
|
190
|
+
return true
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const baseOptions: Omit<
|
|
195
|
+
SocketClientOptions<typeof socketEvents, typeof socketConfig>,
|
|
196
|
+
'socket'
|
|
197
|
+
> = {
|
|
198
|
+
config: socketConfig,
|
|
199
|
+
sys: sysEvents,
|
|
200
|
+
heartbeat: { intervalMs: 15_000, timeoutMs: 7_500 },
|
|
201
|
+
debug: { connection: true },
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const { SocketProvider, useSocketClient, useSocketConnection } =
|
|
205
|
+
buildSocketProvider({
|
|
206
|
+
events: socketEvents,
|
|
207
|
+
options: baseOptions,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
function getSocket(): Promise<Socket> {
|
|
211
|
+
return Promise.resolve(
|
|
212
|
+
io(socketUrl, {
|
|
213
|
+
path: socketPath,
|
|
214
|
+
transports: ['websocket'],
|
|
215
|
+
}),
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const roomMeta = { room: 'health' }
|
|
220
|
+
export const socketReady = true as const
|
|
221
|
+
|
|
222
|
+
export function AppSocketProvider(props: React.PropsWithChildren) {
|
|
223
|
+
return (
|
|
224
|
+
<SocketProvider
|
|
225
|
+
getSocket={getSocket}
|
|
226
|
+
destroyLeaveMeta={roomMeta}
|
|
227
|
+
fallback={<></>}
|
|
228
|
+
>
|
|
229
|
+
{props.children}
|
|
230
|
+
</SocketProvider>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export { useSocketClient, useSocketConnection }
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
return `import React from 'react'
|
|
238
|
+
|
|
239
|
+
export const roomMeta = { room: 'health' }
|
|
240
|
+
export const socketReady = false as const
|
|
241
|
+
|
|
242
|
+
const stubSocket = {
|
|
243
|
+
connect: () =>
|
|
244
|
+
console.info('Socket disabled; add your contract import in src/socket.tsx.'),
|
|
245
|
+
disconnect: () => undefined,
|
|
246
|
+
emit: () => undefined,
|
|
247
|
+
joinRooms: () => undefined,
|
|
248
|
+
leaveRooms: () => undefined,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function AppSocketProvider(props: React.PropsWithChildren) {
|
|
252
|
+
return <>{props.children}</>
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function useSocketClient() {
|
|
256
|
+
return stubSocket
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function useSocketConnection() {
|
|
260
|
+
// no-op when sockets are not configured
|
|
261
|
+
}
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
function nativeAppTsx() {
|
|
265
|
+
return `import React from 'react'
|
|
266
|
+
import { SafeAreaView, ScrollView, StatusBar, StyleSheet, Text } from 'react-native'
|
|
267
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
268
|
+
import { queryClient, hasContract } from './src/queryClient'
|
|
269
|
+
import { AppSocketProvider } from './src/socket'
|
|
270
|
+
import { HealthScreen } from './src/screens/HealthScreen'
|
|
271
|
+
|
|
272
|
+
export default function App() {
|
|
273
|
+
return (
|
|
274
|
+
<QueryClientProvider client={queryClient}>
|
|
275
|
+
<AppSocketProvider>
|
|
276
|
+
<StatusBar barStyle="dark-content" />
|
|
277
|
+
<SafeAreaView style={styles.container}>
|
|
278
|
+
{!hasContract ? (
|
|
279
|
+
<Text style={styles.notice}>
|
|
280
|
+
Add your contract import in src/queryClient.ts and src/socket.tsx to enable
|
|
281
|
+
API + socket helpers.
|
|
282
|
+
</Text>
|
|
283
|
+
) : null}
|
|
284
|
+
<ScrollView contentInsetAdjustmentBehavior="automatic">
|
|
285
|
+
<HealthScreen />
|
|
286
|
+
</ScrollView>
|
|
287
|
+
</SafeAreaView>
|
|
288
|
+
</AppSocketProvider>
|
|
289
|
+
</QueryClientProvider>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const styles = StyleSheet.create({
|
|
294
|
+
container: {
|
|
295
|
+
flex: 1,
|
|
296
|
+
padding: 16,
|
|
297
|
+
backgroundColor: '#f4f5fb',
|
|
298
|
+
},
|
|
299
|
+
notice: {
|
|
300
|
+
backgroundColor: '#fff7ed',
|
|
301
|
+
borderColor: '#fed7aa',
|
|
302
|
+
borderWidth: 1,
|
|
303
|
+
borderRadius: 8,
|
|
304
|
+
padding: 12,
|
|
305
|
+
marginBottom: 12,
|
|
306
|
+
color: '#9a3412',
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
function nativeHealthScreen() {
|
|
312
|
+
return `import React from 'react'
|
|
313
|
+
import {
|
|
314
|
+
Button,
|
|
315
|
+
StyleSheet,
|
|
316
|
+
Text,
|
|
317
|
+
TextInput,
|
|
318
|
+
View,
|
|
319
|
+
} from 'react-native'
|
|
320
|
+
import { healthGet, healthPost, hasContract } from '../queryClient'
|
|
321
|
+
import { roomMeta, useSocketClient, useSocketConnection, socketReady } from '../socket'
|
|
322
|
+
|
|
323
|
+
const now = () => new Date().toLocaleTimeString()
|
|
324
|
+
|
|
325
|
+
function useLogs() {
|
|
326
|
+
const [logs, setLogs] = React.useState<string[]>([])
|
|
327
|
+
return {
|
|
328
|
+
logs,
|
|
329
|
+
push: (msg: string) =>
|
|
330
|
+
setLogs((prev) => [\`[\${now()}] \${msg}\`, ...prev].slice(0, 60)),
|
|
331
|
+
clear: () => setLogs([]),
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function HealthScreen() {
|
|
336
|
+
const [echo, setEcho] = React.useState('hello rrroute')
|
|
337
|
+
const httpGet = healthGet.useEndpoint()
|
|
338
|
+
const httpPost = healthPost.useEndpoint()
|
|
339
|
+
const socket = useSocketClient()
|
|
340
|
+
const { logs, push, clear } = useLogs()
|
|
341
|
+
|
|
342
|
+
useSocketConnection({
|
|
343
|
+
event: 'health:connected',
|
|
344
|
+
rooms: ['health'],
|
|
345
|
+
joinMeta: roomMeta,
|
|
346
|
+
leaveMeta: roomMeta,
|
|
347
|
+
onMessage: (payload) => push(\`connected \${payload.socketId}\`),
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
useSocketConnection({
|
|
351
|
+
event: 'health:pong',
|
|
352
|
+
rooms: ['health'],
|
|
353
|
+
joinMeta: roomMeta,
|
|
354
|
+
leaveMeta: roomMeta,
|
|
355
|
+
onMessage: (payload) =>
|
|
356
|
+
push(\`pong at \${payload.at}\${payload.echo ? \` (echo: \${payload.echo})\` : ''}\`),
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<View style={styles.card}>
|
|
361
|
+
<Text style={styles.title}>RRRoutes health sandbox</Text>
|
|
362
|
+
<View style={styles.section}>
|
|
363
|
+
<Text style={styles.heading}>HTTP</Text>
|
|
364
|
+
{!hasContract ? (
|
|
365
|
+
<Text style={styles.muted}>
|
|
366
|
+
Wire up a contract to enable typed HTTP calls.
|
|
367
|
+
</Text>
|
|
368
|
+
) : (
|
|
369
|
+
<>
|
|
370
|
+
<Button title="GET /health" onPress={() => httpGet.refetch()} />
|
|
371
|
+
<View style={{ height: 12 }} />
|
|
372
|
+
<TextInput
|
|
373
|
+
style={styles.input}
|
|
374
|
+
value={echo}
|
|
375
|
+
onChangeText={setEcho}
|
|
376
|
+
placeholder="echo payload"
|
|
377
|
+
/>
|
|
378
|
+
<Button
|
|
379
|
+
title="POST /health"
|
|
380
|
+
onPress={() =>
|
|
381
|
+
httpPost
|
|
382
|
+
.mutateAsync({ echo })
|
|
383
|
+
.then(() => push('POST /health ok'))
|
|
384
|
+
}
|
|
385
|
+
/>
|
|
386
|
+
<Text style={styles.label}>GET data</Text>
|
|
387
|
+
<Text style={styles.code}>
|
|
388
|
+
{JSON.stringify(httpGet.data ?? {}, null, 2)}
|
|
389
|
+
</Text>
|
|
390
|
+
<Text style={styles.label}>POST data</Text>
|
|
391
|
+
<Text style={styles.code}>
|
|
392
|
+
{JSON.stringify(httpPost.data ?? {}, null, 2)}
|
|
393
|
+
</Text>
|
|
394
|
+
</>
|
|
395
|
+
)}
|
|
396
|
+
</View>
|
|
397
|
+
|
|
398
|
+
<View style={styles.section}>
|
|
399
|
+
<Text style={styles.heading}>Socket</Text>
|
|
400
|
+
{!socketReady ? (
|
|
401
|
+
<Text style={styles.muted}>
|
|
402
|
+
Wire up socket events via your contract to enable live updates.
|
|
403
|
+
</Text>
|
|
404
|
+
) : (
|
|
405
|
+
<>
|
|
406
|
+
<View style={styles.row}>
|
|
407
|
+
<Button title="Connect" onPress={() => socket.connect()} />
|
|
408
|
+
<Button title="Disconnect" onPress={() => socket.disconnect()} />
|
|
409
|
+
</View>
|
|
410
|
+
<View style={{ height: 8 }} />
|
|
411
|
+
<Button
|
|
412
|
+
title="Emit ping"
|
|
413
|
+
onPress={() =>
|
|
414
|
+
socket.emit('health:ping', { note: 'ping from app' })
|
|
415
|
+
}
|
|
416
|
+
/>
|
|
417
|
+
<View style={{ height: 8 }} />
|
|
418
|
+
<View style={styles.row}>
|
|
419
|
+
<Button
|
|
420
|
+
title="Join room"
|
|
421
|
+
onPress={() => socket.joinRooms(['health'], roomMeta)}
|
|
422
|
+
/>
|
|
423
|
+
<Button
|
|
424
|
+
title="Leave room"
|
|
425
|
+
onPress={() => socket.leaveRooms(['health'], roomMeta)}
|
|
426
|
+
/>
|
|
427
|
+
</View>
|
|
428
|
+
</>
|
|
429
|
+
)}
|
|
430
|
+
</View>
|
|
431
|
+
|
|
432
|
+
<View style={styles.section}>
|
|
433
|
+
<Text style={styles.heading}>Socket logs</Text>
|
|
434
|
+
<Button title="Clear logs" onPress={clear} />
|
|
435
|
+
<Text style={styles.code}>
|
|
436
|
+
{logs.length === 0 ? 'No messages yet' : logs.join('\\n')}
|
|
437
|
+
</Text>
|
|
438
|
+
</View>
|
|
439
|
+
</View>
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const styles = StyleSheet.create({
|
|
444
|
+
card: {
|
|
445
|
+
backgroundColor: '#fff',
|
|
446
|
+
borderRadius: 12,
|
|
447
|
+
padding: 16,
|
|
448
|
+
gap: 12,
|
|
449
|
+
shadowColor: '#000',
|
|
450
|
+
shadowOpacity: 0.05,
|
|
451
|
+
shadowRadius: 8,
|
|
452
|
+
},
|
|
453
|
+
title: { fontSize: 20, fontWeight: '700' },
|
|
454
|
+
section: { gap: 8 },
|
|
455
|
+
heading: { fontWeight: '600', fontSize: 16 },
|
|
456
|
+
row: { flexDirection: 'row', gap: 8, justifyContent: 'space-between' },
|
|
457
|
+
input: {
|
|
458
|
+
borderWidth: 1,
|
|
459
|
+
borderColor: '#d0d4de',
|
|
460
|
+
borderRadius: 8,
|
|
461
|
+
padding: 8,
|
|
462
|
+
},
|
|
463
|
+
label: { marginTop: 6, fontWeight: '600' },
|
|
464
|
+
code: {
|
|
465
|
+
backgroundColor: '#0f172a',
|
|
466
|
+
color: '#e2e8f0',
|
|
467
|
+
padding: 8,
|
|
468
|
+
borderRadius: 8,
|
|
469
|
+
fontFamily: 'Courier',
|
|
470
|
+
},
|
|
471
|
+
muted: {
|
|
472
|
+
color: '#6b7280',
|
|
473
|
+
},
|
|
474
|
+
})
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
export async function scaffoldExpoReactNativeClient(ctx) {
|
|
478
|
+
const includePrepare = isWorkspaceRoot(ctx.targetDir);
|
|
479
|
+
const tsconfig = await packageTsConfig(ctx.targetDir, {
|
|
480
|
+
include: ['App.tsx', 'src/**/*.ts', 'src/**/*.tsx'],
|
|
481
|
+
jsx: 'react-native',
|
|
482
|
+
types: ['react', 'react-native'],
|
|
483
|
+
rootDir: '.',
|
|
484
|
+
});
|
|
485
|
+
const files = {
|
|
486
|
+
'package.json': nativePackageJson(ctx.pkgName, ctx.contractName, includePrepare),
|
|
487
|
+
'tsconfig.json': tsconfig,
|
|
488
|
+
'app.json': nativeAppJson(ctx.pkgName),
|
|
489
|
+
'babel.config.js': NATIVE_BABEL,
|
|
490
|
+
'App.tsx': nativeAppTsx(),
|
|
491
|
+
'src/queryClient.ts': nativeQueryClient(ctx.contractName),
|
|
492
|
+
'src/socket.tsx': nativeSocket(ctx.contractName),
|
|
493
|
+
'src/screens/HealthScreen.tsx': nativeHealthScreen(),
|
|
494
|
+
'README.md': buildReadme({
|
|
495
|
+
name: ctx.pkgName,
|
|
496
|
+
description: 'Expo + React Native RRRoutes client scaffold.',
|
|
497
|
+
scripts: [...EXPO_CLIENT_SCRIPTS],
|
|
498
|
+
sections: [
|
|
499
|
+
{
|
|
500
|
+
title: 'Getting Started',
|
|
501
|
+
lines: [
|
|
502
|
+
'- Install deps: `npm install` (or `pnpm install`).',
|
|
503
|
+
'- Start Expo dev server: `npm run dev` (or `npm start`).',
|
|
504
|
+
'- Launch on device/simulator: `npm run android`, `npm run ios`, or `npm run web`.',
|
|
505
|
+
],
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
title: 'Contract wiring',
|
|
509
|
+
lines: [
|
|
510
|
+
ctx.contractName
|
|
511
|
+
? `- Contract wired to ${ctx.contractName}; adjust the import in \`src/queryClient.ts\` if needed.`
|
|
512
|
+
: '- Update `src/queryClient.ts` and `src/socket.tsx` with your contract import to enable HTTP + socket helpers.',
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
title: 'Environment',
|
|
517
|
+
lines: [
|
|
518
|
+
'- `app.json` extra values (`apiUrl`, `socketUrl`, `socketPath`) configure runtime endpoints.',
|
|
519
|
+
'- Update those values or read from native env as needed.',
|
|
520
|
+
],
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
}),
|
|
524
|
+
...basePackageFiles(),
|
|
525
|
+
};
|
|
526
|
+
for (const [relative, contents] of Object.entries(files)) {
|
|
527
|
+
// eslint-disable-next-line no-await-in-loop
|
|
528
|
+
await writeFileIfMissing(ctx.targetDir, relative, contents);
|
|
529
|
+
}
|
|
530
|
+
}
|