@hiyve/cli 1.0.18 → 1.0.19
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/package.json +1 -1
- package/src/commands/init.js +200 -176
- package/src/commands/init.test.js +151 -0
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -14,7 +14,10 @@ const TEMPLATES = {
|
|
|
14
14
|
basic: {
|
|
15
15
|
name: 'Basic',
|
|
16
16
|
description: 'Video conferencing with controls and participant list',
|
|
17
|
-
packages: [
|
|
17
|
+
packages: [
|
|
18
|
+
'@hiyve/react', '@hiyve/react-ui', '@hiyve/core', '@hiyve/rtc-client', '@hiyve/utilities',
|
|
19
|
+
'@hiyve/admin',
|
|
20
|
+
],
|
|
18
21
|
features: { intelligence: false, collaboration: false, capture: false },
|
|
19
22
|
},
|
|
20
23
|
ai: {
|
|
@@ -23,21 +26,65 @@ const TEMPLATES = {
|
|
|
23
26
|
packages: [
|
|
24
27
|
'@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/react-capture',
|
|
25
28
|
'@hiyve/core', '@hiyve/cloud', '@hiyve/rtc-client', '@hiyve/utilities',
|
|
29
|
+
'@hiyve/admin',
|
|
26
30
|
],
|
|
27
31
|
features: { intelligence: true, collaboration: false, capture: true },
|
|
28
32
|
},
|
|
29
33
|
full: {
|
|
30
34
|
name: 'Full Suite',
|
|
31
35
|
description: 'Video + AI + chat, polls, Q&A, file sharing, notes',
|
|
36
|
+
// react-room's REQUIRED peers are all here: react-intelligence and
|
|
37
|
+
// react-semantic-relay are imported statically, not optionally.
|
|
32
38
|
packages: [
|
|
33
|
-
'@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/react-
|
|
34
|
-
'@hiyve/react-collaboration', '@hiyve/react-notes', '@hiyve/react-room',
|
|
39
|
+
'@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/react-semantic-relay',
|
|
40
|
+
'@hiyve/react-capture', '@hiyve/react-collaboration', '@hiyve/react-notes', '@hiyve/react-room',
|
|
35
41
|
'@hiyve/core', '@hiyve/cloud', '@hiyve/rtc-client', '@hiyve/utilities',
|
|
42
|
+
'@hiyve/admin',
|
|
36
43
|
],
|
|
37
44
|
features: { intelligence: true, collaboration: true, capture: true },
|
|
38
45
|
},
|
|
39
46
|
};
|
|
40
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The signaling region every generated project pins. It appears in BOTH
|
|
50
|
+
* `.env.example` (SERVER_REGION — what the token server mints for) and the
|
|
51
|
+
* client (`region` on HiyveProvider / HiyveRoom). They must agree: a token
|
|
52
|
+
* presented to a stack that did not sign it is rejected with a generic
|
|
53
|
+
* "Internal server error".
|
|
54
|
+
*/
|
|
55
|
+
export const DEFAULT_REGION = 'us-east-2';
|
|
56
|
+
|
|
57
|
+
/** Port the generated token server listens on; vite proxies /api to it. */
|
|
58
|
+
const SERVER_PORT = 3001;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pinned versions. Taken from what `npm create vite@latest -- --template
|
|
62
|
+
* react-ts` emits (2026-09-01) plus the peer ranges the published @hiyve
|
|
63
|
+
* packages declare: React 18 or 19, MUI ^9. Bumping MUI below 9 or React
|
|
64
|
+
* below 18 makes `npm install` fail with ERESOLVE against the registry.
|
|
65
|
+
*/
|
|
66
|
+
export const PINS = {
|
|
67
|
+
react: '^19.2.8',
|
|
68
|
+
'react-dom': '^19.2.8',
|
|
69
|
+
'@mui/material': '^9.4.0',
|
|
70
|
+
'@mui/icons-material': '^9.4.0',
|
|
71
|
+
'@emotion/react': '^11.14.0',
|
|
72
|
+
'@emotion/styled': '^11.14.1',
|
|
73
|
+
express: '^5.2.1',
|
|
74
|
+
cors: '^2.8.6',
|
|
75
|
+
dotenv: '^17.4.2',
|
|
76
|
+
'@types/react': '^19.2.18',
|
|
77
|
+
'@types/react-dom': '^19.2.4',
|
|
78
|
+
'@types/express': '^5.0.6',
|
|
79
|
+
'@types/cors': '^2.8.19',
|
|
80
|
+
'@types/node': '^24.13.3',
|
|
81
|
+
'@vitejs/plugin-react': '^6.1.0',
|
|
82
|
+
concurrently: '^10.0.5',
|
|
83
|
+
tsx: '^4.23.13',
|
|
84
|
+
typescript: '~6.0.2',
|
|
85
|
+
vite: '^8.2.2',
|
|
86
|
+
};
|
|
87
|
+
|
|
41
88
|
export async function init(projectName, options) {
|
|
42
89
|
console.log('');
|
|
43
90
|
console.log(chalk.cyan(' Hiyve Project Scaffolding'));
|
|
@@ -102,8 +149,11 @@ export async function init(projectName, options) {
|
|
|
102
149
|
writeFile(projectDir, 'vite.config.ts', generateViteConfig());
|
|
103
150
|
writeFile(projectDir, '.env.example', generateEnvExample(template));
|
|
104
151
|
writeFile(projectDir, '.gitignore', generateGitignore());
|
|
105
|
-
writeFile(projectDir, 'src/main.tsx', generateMain());
|
|
152
|
+
writeFile(projectDir, 'src/main.tsx', generateMain(template));
|
|
106
153
|
writeFile(projectDir, 'src/App.tsx', generateApp(template));
|
|
154
|
+
if (!usesPrebuiltRoom(template)) {
|
|
155
|
+
writeFile(projectDir, 'src/VideoRoom.tsx', generateVideoRoom());
|
|
156
|
+
}
|
|
107
157
|
writeFile(projectDir, 'index.html', generateIndexHtml(projectName));
|
|
108
158
|
writeFile(projectDir, 'server/index.ts', generateServer(template));
|
|
109
159
|
|
|
@@ -113,7 +163,7 @@ export async function init(projectName, options) {
|
|
|
113
163
|
console.log(chalk.gray(' Next steps:'));
|
|
114
164
|
console.log('');
|
|
115
165
|
console.log(` ${chalk.cyan('cd')} ${projectName}`);
|
|
116
|
-
console.log(` ${chalk.cyan('cp')} .env.example .env ${chalk.gray('# Add your API key')}`);
|
|
166
|
+
console.log(` ${chalk.cyan('cp')} .env.example .env ${chalk.gray('# Add your API key + client secret (console.hiyve.dev)')}`);
|
|
117
167
|
console.log(` ${chalk.cyan('npm install')}`);
|
|
118
168
|
console.log(` ${chalk.cyan('npm run dev')}`);
|
|
119
169
|
console.log('');
|
|
@@ -136,17 +186,23 @@ function writeFile(dir, filePath, content) {
|
|
|
136
186
|
writeFileSync(fullPath, content, 'utf-8');
|
|
137
187
|
}
|
|
138
188
|
|
|
139
|
-
function
|
|
189
|
+
function usesPrebuiltRoom(template) {
|
|
190
|
+
return template.features.intelligence && template.features.collaboration;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function generatePackageJson(name, template) {
|
|
140
194
|
const deps = {};
|
|
141
195
|
for (const pkg of template.packages) {
|
|
142
196
|
deps[pkg] = 'latest';
|
|
143
197
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
198
|
+
for (const k of ['react', 'react-dom', '@mui/material', '@mui/icons-material', '@emotion/react', '@emotion/styled', 'express', 'cors', 'dotenv']) {
|
|
199
|
+
deps[k] = PINS[k];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const devDependencies = {};
|
|
203
|
+
for (const k of ['@types/react', '@types/react-dom', '@types/express', '@types/cors', '@types/node', '@vitejs/plugin-react', 'concurrently', 'tsx', 'typescript', 'vite']) {
|
|
204
|
+
devDependencies[k] = PINS[k];
|
|
205
|
+
}
|
|
150
206
|
|
|
151
207
|
const pkg = {
|
|
152
208
|
name,
|
|
@@ -154,39 +210,32 @@ function generatePackageJson(name, template) {
|
|
|
154
210
|
private: true,
|
|
155
211
|
type: 'module',
|
|
156
212
|
scripts: {
|
|
157
|
-
dev: 'concurrently "
|
|
213
|
+
dev: 'concurrently "tsx watch server/index.ts" "vite"',
|
|
158
214
|
build: 'vite build',
|
|
159
215
|
preview: 'vite preview',
|
|
160
|
-
|
|
216
|
+
server: 'tsx server/index.ts',
|
|
161
217
|
},
|
|
162
218
|
dependencies: deps,
|
|
163
|
-
devDependencies
|
|
164
|
-
'@types/react': '^18.2.0',
|
|
165
|
-
'@types/react-dom': '^18.2.0',
|
|
166
|
-
'@vitejs/plugin-react': '^4.2.0',
|
|
167
|
-
'concurrently': '^8.2.0',
|
|
168
|
-
'dotenv': '^16.3.0',
|
|
169
|
-
'express': '^4.18.0',
|
|
170
|
-
'tsx': '^4.7.0',
|
|
171
|
-
'typescript': '^5.3.0',
|
|
172
|
-
'vite': '^5.0.0',
|
|
173
|
-
},
|
|
219
|
+
devDependencies,
|
|
174
220
|
};
|
|
175
221
|
|
|
176
222
|
return JSON.stringify(pkg, null, 2) + '\n';
|
|
177
223
|
}
|
|
178
224
|
|
|
179
|
-
function generateTsConfig() {
|
|
225
|
+
export function generateTsConfig() {
|
|
226
|
+
// Mirrors the react-ts template `npm create vite@latest` emits, with one
|
|
227
|
+
// tsconfig covering both src/ (browser) and server/ (tsx).
|
|
180
228
|
const config = {
|
|
181
229
|
compilerOptions: {
|
|
182
|
-
target: '
|
|
183
|
-
|
|
184
|
-
lib: ['ES2020', 'DOM', 'DOM.Iterable'],
|
|
230
|
+
target: 'ES2023',
|
|
231
|
+
lib: ['ES2023', 'DOM', 'DOM.Iterable'],
|
|
185
232
|
module: 'ESNext',
|
|
186
|
-
skipLibCheck: true,
|
|
187
233
|
moduleResolution: 'bundler',
|
|
234
|
+
types: ['vite/client', 'node'],
|
|
235
|
+
skipLibCheck: true,
|
|
188
236
|
allowImportingTsExtensions: true,
|
|
189
|
-
|
|
237
|
+
verbatimModuleSyntax: true,
|
|
238
|
+
moduleDetection: 'force',
|
|
190
239
|
isolatedModules: true,
|
|
191
240
|
noEmit: true,
|
|
192
241
|
jsx: 'react-jsx',
|
|
@@ -195,46 +244,55 @@ function generateTsConfig() {
|
|
|
195
244
|
noUnusedParameters: true,
|
|
196
245
|
noFallthroughCasesInSwitch: true,
|
|
197
246
|
},
|
|
198
|
-
include: ['src'],
|
|
247
|
+
include: ['src', 'server', 'vite.config.ts'],
|
|
199
248
|
};
|
|
200
249
|
return JSON.stringify(config, null, 2) + '\n';
|
|
201
250
|
}
|
|
202
251
|
|
|
203
|
-
function generateViteConfig() {
|
|
252
|
+
export function generateViteConfig() {
|
|
204
253
|
return `import { defineConfig } from 'vite';
|
|
205
254
|
import react from '@vitejs/plugin-react';
|
|
206
255
|
|
|
207
256
|
export default defineConfig({
|
|
208
257
|
plugins: [react()],
|
|
209
258
|
server: {
|
|
210
|
-
port: 3000,
|
|
211
259
|
proxy: {
|
|
212
|
-
|
|
260
|
+
// The token server (server/index.ts). @hiyve/react fetches
|
|
261
|
+
// /api/generate-room-token and /api/generate-cloud-token from here.
|
|
262
|
+
'/api': 'http://localhost:${SERVER_PORT}',
|
|
213
263
|
},
|
|
214
264
|
},
|
|
215
265
|
});
|
|
216
266
|
`;
|
|
217
267
|
}
|
|
218
268
|
|
|
219
|
-
function generateEnvExample(template) {
|
|
220
|
-
let env = `#
|
|
221
|
-
|
|
269
|
+
export function generateEnvExample(template) {
|
|
270
|
+
let env = `# Both keys come from https://console.hiyve.dev (API Keys)
|
|
271
|
+
|
|
272
|
+
# API Key (required — identifies your app)
|
|
273
|
+
APIKEY=pk_live_your_api_key_here
|
|
274
|
+
|
|
275
|
+
# Client Secret (required for video conferencing — mints room tokens; server-side only)
|
|
276
|
+
CLIENT_SECRET=sk_live_your_client_secret_here
|
|
222
277
|
|
|
223
|
-
#
|
|
224
|
-
|
|
278
|
+
# Signaling region the token server mints for. MUST match the \`region\`
|
|
279
|
+
# passed to HiyveProvider / HiyveRoom in src/ (${DEFAULT_REGION} in both).
|
|
280
|
+
SERVER_REGION=${DEFAULT_REGION}
|
|
281
|
+
SERVER_REGION_URL=.rtc.muziemedia.com
|
|
282
|
+
ENVIRONMENT=development
|
|
225
283
|
`;
|
|
226
284
|
|
|
227
285
|
if (template.features.intelligence) {
|
|
228
286
|
env += `
|
|
229
|
-
# Cloud API
|
|
230
|
-
|
|
287
|
+
# Cloud API for AI features. Defaults to https://cloud.hiyve.io — leave unset.
|
|
288
|
+
# HIYVE_CLOUD_API_URL=https://cloud.hiyve.io
|
|
231
289
|
`;
|
|
232
290
|
}
|
|
233
291
|
|
|
234
292
|
return env;
|
|
235
293
|
}
|
|
236
294
|
|
|
237
|
-
function generateGitignore() {
|
|
295
|
+
export function generateGitignore() {
|
|
238
296
|
return `node_modules
|
|
239
297
|
dist
|
|
240
298
|
.env
|
|
@@ -242,53 +300,70 @@ dist
|
|
|
242
300
|
`;
|
|
243
301
|
}
|
|
244
302
|
|
|
245
|
-
function generateMain() {
|
|
246
|
-
|
|
247
|
-
|
|
303
|
+
export function generateMain(template) {
|
|
304
|
+
if (usesPrebuiltRoom(template)) {
|
|
305
|
+
// HiyveRoom (in App.tsx) provides the HiyveProvider, theme and region.
|
|
306
|
+
return `import ReactDOM from 'react-dom/client';
|
|
248
307
|
import App from './App';
|
|
249
308
|
|
|
309
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return `import ReactDOM from 'react-dom/client';
|
|
314
|
+
import { HiyveProvider } from '@hiyve/react';
|
|
315
|
+
import App from './App';
|
|
316
|
+
|
|
317
|
+
// region MUST match SERVER_REGION in .env — see .env.example.
|
|
250
318
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
251
|
-
<
|
|
319
|
+
<HiyveProvider region="${DEFAULT_REGION}">
|
|
252
320
|
<App />
|
|
253
|
-
</
|
|
321
|
+
</HiyveProvider>,
|
|
254
322
|
);
|
|
255
323
|
`;
|
|
256
324
|
}
|
|
257
325
|
|
|
258
|
-
function generateApp(template) {
|
|
259
|
-
if (template
|
|
260
|
-
|
|
261
|
-
return `import React, { useState, useCallback } from 'react';
|
|
326
|
+
export function generateApp(template) {
|
|
327
|
+
if (usesPrebuiltRoom(template)) {
|
|
328
|
+
return `import { useState, useCallback } from 'react';
|
|
262
329
|
import { HiyveRoom, PrebuiltRoom, PrebuiltLobby } from '@hiyve/react-room';
|
|
263
330
|
|
|
331
|
+
// Must match SERVER_REGION in .env — see .env.example.
|
|
332
|
+
const REGION = '${DEFAULT_REGION}';
|
|
333
|
+
|
|
334
|
+
/** Fetch a room token from server/index.ts (mountHiyveRoutes). */
|
|
335
|
+
async function fetchRoomToken(): Promise<string> {
|
|
336
|
+
const res = await fetch('/api/generate-room-token', { method: 'POST' });
|
|
337
|
+
const data = await res.json().catch(() => ({}));
|
|
338
|
+
if (!res.ok) throw new Error(data.message || 'Failed to generate room token');
|
|
339
|
+
return data.roomToken;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Fetch a cloud token for AI features from the same server. */
|
|
343
|
+
async function fetchCloudToken(): Promise<string> {
|
|
344
|
+
const res = await fetch('/api/generate-cloud-token', { method: 'POST' });
|
|
345
|
+
const data = await res.json().catch(() => ({}));
|
|
346
|
+
if (!res.ok) throw new Error(data.message || 'Failed to generate cloud token');
|
|
347
|
+
return data.cloudToken;
|
|
348
|
+
}
|
|
349
|
+
|
|
264
350
|
export default function App() {
|
|
265
351
|
const [roomToken, setRoomToken] = useState<string | null>(null);
|
|
266
|
-
const [cloudToken, setCloudToken] = useState<string | null>(null);
|
|
267
352
|
const [displayName, setDisplayName] = useState('');
|
|
268
353
|
|
|
269
354
|
const handleJoin = useCallback(async (name: string) => {
|
|
270
355
|
setDisplayName(name);
|
|
271
|
-
|
|
272
|
-
method: 'POST',
|
|
273
|
-
headers: { 'Content-Type': 'application/json' },
|
|
274
|
-
body: JSON.stringify({ roomName: 'my-room', userId: name, displayName: name }),
|
|
275
|
-
});
|
|
276
|
-
const data = await res.json();
|
|
277
|
-
setRoomToken(data.roomToken);
|
|
278
|
-
if (data.cloudToken) setCloudToken(data.cloudToken);
|
|
356
|
+
setRoomToken(await fetchRoomToken());
|
|
279
357
|
}, []);
|
|
280
358
|
|
|
281
|
-
const handleLeave = useCallback(() =>
|
|
282
|
-
setRoomToken(null);
|
|
283
|
-
setCloudToken(null);
|
|
284
|
-
}, []);
|
|
359
|
+
const handleLeave = useCallback(() => setRoomToken(null), []);
|
|
285
360
|
|
|
286
361
|
if (!roomToken) {
|
|
287
362
|
return <PrebuiltLobby onJoin={handleJoin} />;
|
|
288
363
|
}
|
|
289
364
|
|
|
290
365
|
return (
|
|
291
|
-
<HiyveRoom roomToken={roomToken}
|
|
366
|
+
<HiyveRoom roomToken={roomToken} region={REGION} generateToken={fetchCloudToken} userId={displayName} intelligence>
|
|
292
367
|
<PrebuiltRoom userId={displayName} onLeave={handleLeave} />
|
|
293
368
|
</HiyveRoom>
|
|
294
369
|
);
|
|
@@ -296,67 +371,62 @@ export default function App() {
|
|
|
296
371
|
`;
|
|
297
372
|
}
|
|
298
373
|
|
|
299
|
-
// Basic / AI
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
374
|
+
// Basic / AI — the getting-started guide's App, verbatim. HiyveProvider
|
|
375
|
+
// (src/main.tsx) fetches the room token itself; JoinForm drives the flow.
|
|
376
|
+
const aiNote = template.features.intelligence
|
|
377
|
+
? `\n// AI: add hooks from @hiyve/react-intelligence inside VideoRoom (HiyveProvider\n// already fetches the cloud token from /api/generate-cloud-token).\n`
|
|
378
|
+
: '';
|
|
379
|
+
return `import { useRoomFlow } from '@hiyve/react';
|
|
380
|
+
import { JoinForm, ConnectingScreen } from '@hiyve/react-ui';
|
|
381
|
+
import VideoRoom from './VideoRoom';
|
|
382
|
+
${aiNote}
|
|
383
|
+
function App() {
|
|
384
|
+
const { screen } = useRoomFlow();
|
|
385
|
+
|
|
386
|
+
switch (screen) {
|
|
387
|
+
case 'connecting':
|
|
388
|
+
return <ConnectingScreen />;
|
|
389
|
+
case 'in-room':
|
|
390
|
+
return <VideoRoom />;
|
|
391
|
+
default:
|
|
392
|
+
return <JoinForm autoConnect devicePreviewMode="inline" />;
|
|
309
393
|
}
|
|
310
|
-
|
|
311
|
-
return `import React, { useState, useCallback } from 'react';
|
|
312
|
-
${imports.join('\n')}
|
|
313
|
-
|
|
314
|
-
function Room({ onLeave }: { onLeave: () => void }) {
|
|
315
|
-
return (
|
|
316
|
-
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#121212' }}>
|
|
317
|
-
<div style={{ flex: 1, overflow: 'hidden' }}>
|
|
318
|
-
<VideoGrid localVideoElementId="local-video" />
|
|
319
|
-
</div>${extraComponents}
|
|
320
|
-
<ControlBar onLeave={onLeave} />
|
|
321
|
-
</div>
|
|
322
|
-
);
|
|
323
394
|
}
|
|
324
395
|
|
|
325
|
-
export default
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const handleJoin = useCallback(async () => {
|
|
329
|
-
const res = await fetch('/api/token', {
|
|
330
|
-
method: 'POST',
|
|
331
|
-
headers: { 'Content-Type': 'application/json' },
|
|
332
|
-
body: JSON.stringify({ roomName: 'my-room', userId: 'user-' + Date.now() }),
|
|
333
|
-
});
|
|
334
|
-
const data = await res.json();
|
|
335
|
-
setRoomToken(data.roomToken);
|
|
336
|
-
}, []);
|
|
396
|
+
export default App;
|
|
397
|
+
`;
|
|
398
|
+
}
|
|
337
399
|
|
|
338
|
-
|
|
400
|
+
export function generateVideoRoom() {
|
|
401
|
+
return `import { useState } from 'react';
|
|
402
|
+
import { useRoom, useConnection } from '@hiyve/react';
|
|
403
|
+
import { VideoGrid, ControlBar } from '@hiyve/react-ui';
|
|
339
404
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
Join Room
|
|
345
|
-
</button>
|
|
346
|
-
</div>
|
|
347
|
-
);
|
|
348
|
-
}
|
|
405
|
+
function VideoRoom() {
|
|
406
|
+
const { room } = useRoom();
|
|
407
|
+
const { leaveRoom } = useConnection();
|
|
408
|
+
const [layout, setLayout] = useState('grid');
|
|
349
409
|
|
|
350
410
|
return (
|
|
351
|
-
|
|
352
|
-
<
|
|
353
|
-
|
|
411
|
+
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
412
|
+
{room?.name && <h2 style={{ margin: '8px 16px' }}>{room.name}</h2>}
|
|
413
|
+
<VideoGrid localVideoElementId="local-video" layout={layout} showNames showLocalFlip />
|
|
414
|
+
<ControlBar
|
|
415
|
+
onLeave={leaveRoom}
|
|
416
|
+
showScreenShare
|
|
417
|
+
showLayoutSelector
|
|
418
|
+
layout={layout}
|
|
419
|
+
onLayoutChange={setLayout}
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
354
422
|
);
|
|
355
423
|
}
|
|
424
|
+
|
|
425
|
+
export default VideoRoom;
|
|
356
426
|
`;
|
|
357
427
|
}
|
|
358
428
|
|
|
359
|
-
function generateIndexHtml(name) {
|
|
429
|
+
export function generateIndexHtml(name) {
|
|
360
430
|
return `<!DOCTYPE html>
|
|
361
431
|
<html lang="en">
|
|
362
432
|
<head>
|
|
@@ -372,71 +442,25 @@ function generateIndexHtml(name) {
|
|
|
372
442
|
`;
|
|
373
443
|
}
|
|
374
444
|
|
|
375
|
-
function generateServer(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
445
|
+
export function generateServer() {
|
|
446
|
+
// The getting-started guide's server. mountHiyveRoutes exposes
|
|
447
|
+
// /generate-room-token, /generate-cloud-token, /create-join-token and
|
|
448
|
+
// /health under /api; loadHiyveConfig reads APIKEY, CLIENT_SECRET and
|
|
449
|
+
// SERVER_REGION from .env. Nothing here talks to signaling directly.
|
|
450
|
+
return `import 'dotenv/config';
|
|
451
|
+
import express from 'express';
|
|
452
|
+
import cors from 'cors';
|
|
453
|
+
import { mountHiyveRoutes, loadHiyveConfig } from '@hiyve/admin';
|
|
380
454
|
|
|
381
455
|
const app = express();
|
|
456
|
+
app.use(cors({ origin: ['http://localhost:5173'] }));
|
|
382
457
|
app.use(express.json());
|
|
383
458
|
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Generate a room token for the client.
|
|
389
|
-
* In production, add your own authentication before issuing tokens.
|
|
390
|
-
*/
|
|
391
|
-
app.post('/api/token', async (req, res) => {
|
|
392
|
-
const { roomName, userId, displayName } = req.body;
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
const response = await fetch(\`\${SIGNALING_URL}/api/rooms/token\`, {
|
|
396
|
-
method: 'POST',
|
|
397
|
-
headers: {
|
|
398
|
-
'Content-Type': 'application/json',
|
|
399
|
-
'Authorization': \`Bearer \${API_KEY}\`,
|
|
400
|
-
},
|
|
401
|
-
body: JSON.stringify({ roomName, userId, displayName: displayName || userId }),
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
if (!response.ok) {
|
|
405
|
-
throw new Error(\`Token request failed: \${response.status}\`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const data = await response.json();
|
|
409
|
-
${hasCloud ? `
|
|
410
|
-
// Also generate a cloud token for AI features
|
|
411
|
-
let cloudToken: string | undefined;
|
|
412
|
-
try {
|
|
413
|
-
const cloudRes = await fetch(\`\${CLOUD_URL}/auth/tokens/cloud\`, {
|
|
414
|
-
method: 'POST',
|
|
415
|
-
headers: {
|
|
416
|
-
'Content-Type': 'application/json',
|
|
417
|
-
'x-api-key': API_KEY!,
|
|
418
|
-
},
|
|
419
|
-
body: JSON.stringify({ userId }),
|
|
420
|
-
});
|
|
421
|
-
if (cloudRes.ok) {
|
|
422
|
-
const cloudData = await cloudRes.json();
|
|
423
|
-
cloudToken = cloudData.token;
|
|
424
|
-
}
|
|
425
|
-
} catch {
|
|
426
|
-
// Cloud token optional — continue without it
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
res.json({ roomToken: data.token, cloudToken });
|
|
430
|
-
` : ` res.json({ roomToken: data.token });
|
|
431
|
-
`} } catch (err: any) {
|
|
432
|
-
console.error('Token error:', err.message);
|
|
433
|
-
res.status(500).json({ error: 'Failed to generate token' });
|
|
434
|
-
}
|
|
435
|
-
});
|
|
459
|
+
const apiRouter = express.Router();
|
|
460
|
+
mountHiyveRoutes(apiRouter, loadHiyveConfig());
|
|
461
|
+
app.use('/api', apiRouter);
|
|
436
462
|
|
|
437
|
-
const PORT = process.env.PORT ||
|
|
438
|
-
app.listen(PORT, () => {
|
|
439
|
-
console.log(\`Server running on http://localhost:\${PORT}\`);
|
|
440
|
-
});
|
|
463
|
+
const PORT = Number(process.env.PORT) || ${SERVER_PORT};
|
|
464
|
+
app.listen(PORT, () => console.log(\`Server running on http://localhost:\${PORT}\`));
|
|
441
465
|
`;
|
|
442
466
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scaffolder must produce a project that installs and runs against the
|
|
3
|
+
* PUBLISHED packages and matches the getting-started guide. It drifted
|
|
4
|
+
* badly once (2026-09-01): MUI ^5 against a ^9 peer (ERESOLVE on install), a
|
|
5
|
+
* hand-rolled token route posting to a host with no DNS record, no region
|
|
6
|
+
* pin, and env var names the SDK does not read. These tests check the
|
|
7
|
+
* generated output against the real peer ranges in this workspace so the
|
|
8
|
+
* next drift fails here, not on a developer's first `npm install`.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import {
|
|
15
|
+
PINS,
|
|
16
|
+
DEFAULT_REGION,
|
|
17
|
+
generatePackageJson,
|
|
18
|
+
generateEnvExample,
|
|
19
|
+
generateMain,
|
|
20
|
+
generateApp,
|
|
21
|
+
generateVideoRoom,
|
|
22
|
+
generateServer,
|
|
23
|
+
generateViteConfig,
|
|
24
|
+
generateTsConfig,
|
|
25
|
+
} from './init.js';
|
|
26
|
+
|
|
27
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const workspacePkg = (name) =>
|
|
29
|
+
JSON.parse(readFileSync(join(here, '../../../..', name, 'package.json'), 'utf8'));
|
|
30
|
+
|
|
31
|
+
const TEMPLATES = {
|
|
32
|
+
basic: { packages: ['@hiyve/react', '@hiyve/react-ui', '@hiyve/admin'], features: { intelligence: false, collaboration: false } },
|
|
33
|
+
ai: { packages: ['@hiyve/react', '@hiyve/react-ui', '@hiyve/react-intelligence', '@hiyve/admin'], features: { intelligence: true, collaboration: false } },
|
|
34
|
+
full: { packages: ['@hiyve/react-room', '@hiyve/react-intelligence', '@hiyve/react-semantic-relay', '@hiyve/admin'], features: { intelligence: true, collaboration: true } },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Major of a ^x.y.z / ~x.y.z pin. */
|
|
38
|
+
const majorOf = (range) => Number.parseInt(range.replace(/^[\^~]/, ''), 10);
|
|
39
|
+
/** Majors a caret-only peer range admits, e.g. "^18.0.0 || ^19.0.0" -> [18, 19]. */
|
|
40
|
+
const peerMajors = (peer) => peer.split('||').map((r) => majorOf(r.trim()));
|
|
41
|
+
/** Does a caret pin fall inside a caret-only peer range? */
|
|
42
|
+
const rangeFitsPeer = (pin, peer) => peerMajors(peer).includes(majorOf(pin));
|
|
43
|
+
|
|
44
|
+
describe('generated package.json', () => {
|
|
45
|
+
const pkg = JSON.parse(generatePackageJson('demo', TEMPLATES.basic));
|
|
46
|
+
|
|
47
|
+
it('pins MUI inside the range @hiyve/react-ui and @hiyve/react-room actually require', () => {
|
|
48
|
+
for (const p of ['react-ui', 'react-room']) {
|
|
49
|
+
const peer = workspacePkg(`packages/${p}`).peerDependencies['@mui/material'];
|
|
50
|
+
expect(rangeFitsPeer(pkg.dependencies['@mui/material'], peer), `${p} wants ${peer}`).toBe(true);
|
|
51
|
+
}
|
|
52
|
+
expect(pkg.dependencies['@mui/icons-material']).toBe(pkg.dependencies['@mui/material']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('pins React inside the range @hiyve/react requires', () => {
|
|
56
|
+
const peer = workspacePkg('packages/react').peerDependencies.react;
|
|
57
|
+
expect(rangeFitsPeer(pkg.dependencies.react, peer)).toBe(true);
|
|
58
|
+
expect(pkg.dependencies['react-dom']).toBe(pkg.dependencies.react);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('never pins MUI 5 or React 18 — the drift that broke install', () => {
|
|
62
|
+
expect(majorOf(pkg.dependencies['@mui/material'])).toBeGreaterThanOrEqual(9);
|
|
63
|
+
expect(majorOf(pkg.dependencies.react)).toBeGreaterThanOrEqual(19);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('includes @hiyve/admin — the token server is the SDK, not hand-rolled', () => {
|
|
67
|
+
expect(pkg.dependencies['@hiyve/admin']).toBeDefined();
|
|
68
|
+
expect(pkg.dependencies.express).toBeDefined();
|
|
69
|
+
expect(pkg.dependencies.cors).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('every dev dependency the tsconfig relies on is present', () => {
|
|
73
|
+
for (const k of ['@types/node', 'typescript', 'vite', '@vitejs/plugin-react', 'tsx', 'concurrently']) {
|
|
74
|
+
expect(pkg.devDependencies[k], k).toBe(PINS[k]);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('generated server', () => {
|
|
80
|
+
const server = generateServer();
|
|
81
|
+
|
|
82
|
+
it('mounts the SDK routes instead of posting to a signaling host by hand', () => {
|
|
83
|
+
expect(server).toContain("from '@hiyve/admin'");
|
|
84
|
+
expect(server).toContain('mountHiyveRoutes(apiRouter, loadHiyveConfig())');
|
|
85
|
+
expect(server).toContain("app.use('/api', apiRouter)");
|
|
86
|
+
expect(server).not.toContain('signal.hiyve.dev');
|
|
87
|
+
expect(server).not.toContain('/api/rooms/token');
|
|
88
|
+
expect(server).not.toContain('HIYVE_API_KEY');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('listens where vite proxies /api to', () => {
|
|
92
|
+
const port = server.match(/\|\| (\d+);/)[1];
|
|
93
|
+
expect(generateViteConfig()).toContain(`http://localhost:${port}`);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('generated .env.example', () => {
|
|
98
|
+
it('uses the variable names loadHiyveConfig reads, and the region the client pins', () => {
|
|
99
|
+
const env = generateEnvExample(TEMPLATES.basic);
|
|
100
|
+
expect(env).toMatch(/^APIKEY=pk_/m);
|
|
101
|
+
expect(env).toMatch(/^CLIENT_SECRET=sk_/m);
|
|
102
|
+
expect(env).toContain(`SERVER_REGION=${DEFAULT_REGION}`);
|
|
103
|
+
expect(env).not.toContain('HIYVE_API_KEY');
|
|
104
|
+
expect(env).not.toContain('signal.hiyve.dev');
|
|
105
|
+
expect(env).toContain('console.hiyve.dev');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('region pin', () => {
|
|
110
|
+
it('basic/ai: HiyveProvider in main.tsx carries the region', () => {
|
|
111
|
+
expect(generateMain(TEMPLATES.basic)).toContain(`<HiyveProvider region="${DEFAULT_REGION}">`);
|
|
112
|
+
expect(generateMain(TEMPLATES.ai)).toContain(`<HiyveProvider region="${DEFAULT_REGION}">`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('full: HiyveRoom carries the region and main.tsx does not double-wrap', () => {
|
|
116
|
+
expect(generateApp(TEMPLATES.full)).toContain(`region={REGION}`);
|
|
117
|
+
expect(generateApp(TEMPLATES.full)).toContain(`const REGION = '${DEFAULT_REGION}'`);
|
|
118
|
+
expect(generateMain(TEMPLATES.full)).not.toContain('HiyveProvider');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('client and server agree on the region', () => {
|
|
122
|
+
const env = generateEnvExample(TEMPLATES.full);
|
|
123
|
+
const serverRegion = env.match(/^SERVER_REGION=(\S+)/m)[1];
|
|
124
|
+
expect(serverRegion).toBe(DEFAULT_REGION);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('generated client', () => {
|
|
129
|
+
it('basic/ai is the guide verbatim: useRoomFlow + JoinForm, SDK fetches the token', () => {
|
|
130
|
+
const app = generateApp(TEMPLATES.basic);
|
|
131
|
+
expect(app).toContain("import { useRoomFlow } from '@hiyve/react'");
|
|
132
|
+
expect(app).toContain('<JoinForm autoConnect devicePreviewMode="inline" />');
|
|
133
|
+
expect(app).not.toContain('fetch(');
|
|
134
|
+
expect(generateVideoRoom()).toContain('<VideoGrid localVideoElementId="local-video"');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('full fetches tokens from the SDK routes and reads the fields they return', () => {
|
|
138
|
+
const app = generateApp(TEMPLATES.full);
|
|
139
|
+
expect(app).toContain("fetch('/api/generate-room-token'");
|
|
140
|
+
expect(app).toContain('return data.roomToken');
|
|
141
|
+
expect(app).toContain("fetch('/api/generate-cloud-token'");
|
|
142
|
+
expect(app).toContain('return data.cloudToken');
|
|
143
|
+
expect(app).not.toContain('/api/token');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('tsconfig covers both the browser and the server sources', () => {
|
|
147
|
+
const ts = JSON.parse(generateTsConfig());
|
|
148
|
+
expect(ts.include).toEqual(expect.arrayContaining(['src', 'server']));
|
|
149
|
+
expect(ts.compilerOptions.types).toEqual(expect.arrayContaining(['vite/client', 'node']));
|
|
150
|
+
});
|
|
151
|
+
});
|