@viji-dev/sdk 1.0.0 → 1.0.2
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 +70 -63
- package/bin/viji.js +9 -29
- package/dist/assets/artist-dts-BHUsvSI6.js +613 -0
- package/dist/assets/artist-dts-p5-Cyw8vmy_.js +736 -0
- package/dist/assets/core-CiQx3w0t.js +12 -0
- package/dist/assets/dark-plus-C3mMm8J8.js +1 -0
- package/dist/assets/docs-api-PBLtY4Ni.js +12381 -0
- package/dist/assets/engine-javascript-CXyY7cc8.js +141 -0
- package/dist/assets/essentia-wasm.web-0S-sW98u-CYV1l1zv.js +38 -0
- package/dist/assets/essentia.js-core.es-DnrJE0uR-DOSrF5_G.js +32 -0
- package/dist/assets/glsl-DMyvO4G4.js +1 -0
- package/dist/assets/index-BhFxsauQ.js +215 -0
- package/dist/assets/index-BqhVeA7U.css +1 -0
- package/dist/assets/index-T4TOjvD0.js +1 -0
- package/dist/assets/index-Wz9WqGqz.js +52 -0
- package/dist/assets/index-t24aGwla.js +1 -0
- package/dist/assets/javascript-wDzz0qaB.js +1 -0
- package/dist/assets/shader-uniforms-GdaUkQPK.js +1 -0
- package/dist/assets/typescript-BPQ3VLAy.js +1 -0
- package/dist/assets/viji.worker-CQSJ0SiO-ljtBlcNZ.js +27018 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +31 -35
- package/src/cli/commands/build.js +50 -99
- package/src/cli/commands/create.js +49 -46
- package/src/cli/commands/dev.js +30 -97
- package/src/cli/server/dev-server.js +233 -0
- package/src/cli/server/scene-scanner.js +93 -0
- package/src/cli/server/vite-scene-plugin.d.ts +2 -0
- package/src/cli/server/vite-scene-plugin.js +134 -0
- package/src/cli/utils/cli-utils.js +29 -139
- package/src/cli/utils/scene-compiler.js +10 -17
- package/src/templates/scene-templates.js +85 -0
- package/.gitignore +0 -29
- package/eslint.config.js +0 -37
- package/postcss.config.js +0 -6
- package/scenes/audio-visualizer/main.js +0 -287
- package/scenes/core-demo/main.js +0 -532
- package/scenes/demo-scene/main.js +0 -619
- package/scenes/global.d.ts +0 -15
- package/scenes/particle-system/main.js +0 -349
- package/scenes/tsconfig.json +0 -12
- package/scenes/video-mirror/main.ts +0 -436
- package/src/App.css +0 -42
- package/src/App.tsx +0 -279
- package/src/cli/commands/init.js +0 -262
- package/src/components/SDKPage.tsx +0 -337
- package/src/components/core/CoreContainer.tsx +0 -126
- package/src/components/ui/DeviceSelectionList.tsx +0 -137
- package/src/components/ui/FPSCounter.tsx +0 -78
- package/src/components/ui/FileDropzonePanel.tsx +0 -120
- package/src/components/ui/FileListPanel.tsx +0 -285
- package/src/components/ui/InputExpansionPanel.tsx +0 -31
- package/src/components/ui/MediaPlayerControls.tsx +0 -191
- package/src/components/ui/MenuContainer.tsx +0 -71
- package/src/components/ui/ParametersMenu.tsx +0 -797
- package/src/components/ui/ProjectSwitcherMenu.tsx +0 -192
- package/src/components/ui/QuickInputControls.tsx +0 -542
- package/src/components/ui/SDKMenuSystem.tsx +0 -96
- package/src/components/ui/SettingsMenu.tsx +0 -346
- package/src/components/ui/SimpleInputControls.tsx +0 -137
- package/src/index.css +0 -68
- package/src/main.tsx +0 -10
- package/src/scenes-hmr.ts +0 -158
- package/src/services/project-filesystem.ts +0 -436
- package/src/stores/scene-player/index.ts +0 -3
- package/src/stores/scene-player/input-manager.store.ts +0 -1045
- package/src/stores/scene-player/scene-session.store.ts +0 -659
- package/src/styles/globals.css +0 -111
- package/src/templates/minimal-template.js +0 -11
- package/src/utils/debounce.js +0 -34
- package/src/vite-env.d.ts +0 -1
- package/tailwind.config.js +0 -18
- package/tsconfig.app.json +0 -27
- package/tsconfig.json +0 -27
- package/tsconfig.node.json +0 -27
- package/vite.config.ts +0 -54
- /package/{public → dist}/favicon.png +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { exec } from 'node:child_process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { WebSocketServer } from 'ws';
|
|
7
|
+
import chokidar from 'chokidar';
|
|
8
|
+
import { scanScenes, scanSingleScene, readSceneCode } from './scene-scanner.js';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const MIME_TYPES = {
|
|
14
|
+
'.html': 'text/html',
|
|
15
|
+
'.js': 'application/javascript',
|
|
16
|
+
'.mjs': 'application/javascript',
|
|
17
|
+
'.css': 'text/css',
|
|
18
|
+
'.json': 'application/json',
|
|
19
|
+
'.png': 'image/png',
|
|
20
|
+
'.jpg': 'image/jpeg',
|
|
21
|
+
'.svg': 'image/svg+xml',
|
|
22
|
+
'.ico': 'image/x-icon',
|
|
23
|
+
'.woff': 'font/woff',
|
|
24
|
+
'.woff2': 'font/woff2',
|
|
25
|
+
'.wasm': 'application/wasm',
|
|
26
|
+
'.map': 'application/json',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function resolveSDKRoot() {
|
|
30
|
+
return path.resolve(__dirname, '..', '..', '..');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveCoreDist() {
|
|
34
|
+
const sdkRoot = resolveSDKRoot();
|
|
35
|
+
|
|
36
|
+
const localCore = path.join(sdkRoot, 'node_modules', '@viji-dev', 'core', 'dist');
|
|
37
|
+
if (fs.existsSync(localCore)) return localCore;
|
|
38
|
+
|
|
39
|
+
let dir = sdkRoot;
|
|
40
|
+
while (true) {
|
|
41
|
+
const candidate = path.join(dir, 'node_modules', '@viji-dev', 'core', 'dist');
|
|
42
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
43
|
+
const parent = path.dirname(dir);
|
|
44
|
+
if (parent === dir) break;
|
|
45
|
+
dir = parent;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function serveFile(res, filePath) {
|
|
52
|
+
const ext = path.extname(filePath);
|
|
53
|
+
const mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const data = fs.readFileSync(filePath);
|
|
57
|
+
res.writeHead(200, {
|
|
58
|
+
'Content-Type': mime,
|
|
59
|
+
'Cache-Control': 'no-cache',
|
|
60
|
+
'Access-Control-Allow-Origin': '*',
|
|
61
|
+
});
|
|
62
|
+
res.end(data);
|
|
63
|
+
} catch {
|
|
64
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
65
|
+
res.end('Not Found');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sendJSON(res, data, status = 200) {
|
|
70
|
+
res.writeHead(status, {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'Access-Control-Allow-Origin': '*',
|
|
73
|
+
});
|
|
74
|
+
res.end(JSON.stringify(data));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function startDevServer({ port, host, open, scenesDir }) {
|
|
78
|
+
const sdkRoot = resolveSDKRoot();
|
|
79
|
+
const distDir = path.join(sdkRoot, 'dist');
|
|
80
|
+
const coreDist = resolveCoreDist();
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(distDir) || !fs.existsSync(path.join(distDir, 'index.html'))) {
|
|
83
|
+
console.error('SDK UI not found. The dist/ folder is missing or incomplete.');
|
|
84
|
+
console.error(`Expected at: ${distDir}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const server = http.createServer(async (req, res) => {
|
|
89
|
+
const url = new URL(req.url, `http://${host}:${port}`);
|
|
90
|
+
const pathname = url.pathname;
|
|
91
|
+
|
|
92
|
+
if (req.method === 'OPTIONS') {
|
|
93
|
+
res.writeHead(204, {
|
|
94
|
+
'Access-Control-Allow-Origin': '*',
|
|
95
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
96
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
97
|
+
});
|
|
98
|
+
res.end();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (pathname === '/api/scenes') {
|
|
103
|
+
try {
|
|
104
|
+
const scenes = await scanScenes(scenesDir);
|
|
105
|
+
sendJSON(res, scenes);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
sendJSON(res, { error: err.message }, 500);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const codeMatch = pathname.match(/^\/api\/scenes\/([^/]+)\/code$/);
|
|
113
|
+
if (codeMatch) {
|
|
114
|
+
try {
|
|
115
|
+
const code = await readSceneCode(scenesDir, decodeURIComponent(codeMatch[1]));
|
|
116
|
+
if (code === null) {
|
|
117
|
+
sendJSON(res, { error: 'Scene not found' }, 404);
|
|
118
|
+
} else {
|
|
119
|
+
sendJSON(res, { code });
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
sendJSON(res, { error: err.message }, 500);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const sceneMatch = pathname.match(/^\/api\/scenes\/([^/]+)$/);
|
|
128
|
+
if (sceneMatch) {
|
|
129
|
+
try {
|
|
130
|
+
const scene = await scanSingleScene(scenesDir, decodeURIComponent(sceneMatch[1]));
|
|
131
|
+
if (!scene) {
|
|
132
|
+
sendJSON(res, { error: 'Scene not found' }, 404);
|
|
133
|
+
} else {
|
|
134
|
+
sendJSON(res, scene);
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
sendJSON(res, { error: err.message }, 500);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (coreDist && pathname.startsWith('/dist/assets/')) {
|
|
143
|
+
const assetPath = pathname.replace('/dist/assets/', '');
|
|
144
|
+
const fullPath = path.join(coreDist, 'assets', assetPath);
|
|
145
|
+
if (fs.existsSync(fullPath)) {
|
|
146
|
+
serveFile(res, fullPath);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let filePath;
|
|
152
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
153
|
+
filePath = path.join(distDir, 'index.html');
|
|
154
|
+
} else {
|
|
155
|
+
filePath = path.join(distDir, pathname);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
159
|
+
serveFile(res, filePath);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
serveFile(res, path.join(distDir, 'index.html'));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
167
|
+
const clients = new Set();
|
|
168
|
+
|
|
169
|
+
wss.on('connection', (ws) => {
|
|
170
|
+
clients.add(ws);
|
|
171
|
+
ws.on('close', () => clients.delete(ws));
|
|
172
|
+
ws.on('error', () => clients.delete(ws));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
function broadcast(message) {
|
|
176
|
+
const data = JSON.stringify(message);
|
|
177
|
+
for (const ws of clients) {
|
|
178
|
+
if (ws.readyState === 1) {
|
|
179
|
+
ws.send(data);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const watcher = chokidar.watch(scenesDir, {
|
|
185
|
+
ignoreInitial: true,
|
|
186
|
+
ignored: /(^|[/\\])\./,
|
|
187
|
+
persistent: true,
|
|
188
|
+
depth: 3,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
watcher.on('add', async (filePath) => {
|
|
192
|
+
const sceneName = extractSceneName(scenesDir, filePath);
|
|
193
|
+
if (!sceneName) return;
|
|
194
|
+
const scene = await scanSingleScene(scenesDir, sceneName);
|
|
195
|
+
if (scene) broadcast({ type: 'scene:add', scene });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
watcher.on('change', async (filePath) => {
|
|
199
|
+
const sceneName = extractSceneName(scenesDir, filePath);
|
|
200
|
+
if (!sceneName) return;
|
|
201
|
+
const scene = await scanSingleScene(scenesDir, sceneName);
|
|
202
|
+
if (scene) broadcast({ type: 'scene:update', scene });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
watcher.on('unlink', (filePath) => {
|
|
206
|
+
const sceneName = extractSceneName(scenesDir, filePath);
|
|
207
|
+
if (!sceneName) return;
|
|
208
|
+
broadcast({ type: 'scene:remove', sceneId: sceneName });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return new Promise((resolve) => {
|
|
212
|
+
server.listen(port, host, () => {
|
|
213
|
+
const url = `http://${host}:${port}`;
|
|
214
|
+
console.log(`Viji dev server running at ${url}`);
|
|
215
|
+
console.log(`Watching scenes in: ${scenesDir}`);
|
|
216
|
+
console.log('');
|
|
217
|
+
|
|
218
|
+
if (open) {
|
|
219
|
+
const cmd = process.platform === 'win32' ? 'start' :
|
|
220
|
+
process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
221
|
+
exec(`${cmd} ${url}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
resolve({ server, wss, watcher });
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function extractSceneName(scenesDir, filePath) {
|
|
230
|
+
const rel = path.relative(scenesDir, filePath);
|
|
231
|
+
const parts = rel.split(path.sep);
|
|
232
|
+
return parts.length >= 1 ? parts[0] : null;
|
|
233
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readdir, stat, readFile } from 'fs/promises';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
const SCENE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.glsl']);
|
|
6
|
+
|
|
7
|
+
function detectRenderer(code, ext) {
|
|
8
|
+
if (ext === '.glsl') return 'shader';
|
|
9
|
+
|
|
10
|
+
const directiveMatch = code.match(/\/\/\s*@renderer\s+(\w+)/);
|
|
11
|
+
if (directiveMatch) return directiveMatch[1];
|
|
12
|
+
|
|
13
|
+
if (/function\s+render\s*\(\s*viji\s*,\s*p5\s*\)/.test(code)) return 'p5';
|
|
14
|
+
|
|
15
|
+
return 'native';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function scanScenes(scenesDir) {
|
|
19
|
+
if (!existsSync(scenesDir)) return [];
|
|
20
|
+
|
|
21
|
+
const entries = await readdir(scenesDir, { withFileTypes: true });
|
|
22
|
+
const scenes = [];
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (!entry.isDirectory()) continue;
|
|
26
|
+
|
|
27
|
+
const sceneDir = join(scenesDir, entry.name);
|
|
28
|
+
const mainFile = await findMainFile(sceneDir);
|
|
29
|
+
if (!mainFile) continue;
|
|
30
|
+
|
|
31
|
+
const mainPath = join(sceneDir, mainFile);
|
|
32
|
+
const fileStat = await stat(mainPath);
|
|
33
|
+
const code = await readFile(mainPath, 'utf-8');
|
|
34
|
+
const ext = extname(mainFile);
|
|
35
|
+
|
|
36
|
+
scenes.push({
|
|
37
|
+
id: entry.name,
|
|
38
|
+
name: entry.name,
|
|
39
|
+
renderer: detectRenderer(code, ext),
|
|
40
|
+
mainFile,
|
|
41
|
+
createdAt: fileStat.birthtime.toISOString(),
|
|
42
|
+
lastModified: fileStat.mtime.toISOString(),
|
|
43
|
+
code,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return scenes.sort((a, b) => a.name.localeCompare(b.name));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function scanSingleScene(scenesDir, sceneName) {
|
|
51
|
+
const sceneDir = join(scenesDir, sceneName);
|
|
52
|
+
if (!existsSync(sceneDir)) return null;
|
|
53
|
+
|
|
54
|
+
const mainFile = await findMainFile(sceneDir);
|
|
55
|
+
if (!mainFile) return null;
|
|
56
|
+
|
|
57
|
+
const mainPath = join(sceneDir, mainFile);
|
|
58
|
+
const fileStat = await stat(mainPath);
|
|
59
|
+
const code = await readFile(mainPath, 'utf-8');
|
|
60
|
+
const ext = extname(mainFile);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
id: sceneName,
|
|
64
|
+
name: sceneName,
|
|
65
|
+
renderer: detectRenderer(code, ext),
|
|
66
|
+
mainFile,
|
|
67
|
+
createdAt: fileStat.birthtime.toISOString(),
|
|
68
|
+
lastModified: fileStat.mtime.toISOString(),
|
|
69
|
+
code,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function findMainFile(sceneDir) {
|
|
74
|
+
const candidates = ['main.js', 'main.ts', 'main.jsx', 'main.tsx', 'main.glsl'];
|
|
75
|
+
for (const name of candidates) {
|
|
76
|
+
if (existsSync(join(sceneDir, name))) return name;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const files = await readdir(sceneDir);
|
|
81
|
+
return files.find(f => SCENE_EXTENSIONS.has(extname(f))) ?? null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function readSceneCode(scenesDir, sceneName) {
|
|
88
|
+
const sceneDir = join(scenesDir, sceneName);
|
|
89
|
+
const mainFile = await findMainFile(sceneDir);
|
|
90
|
+
if (!mainFile) return null;
|
|
91
|
+
|
|
92
|
+
return readFile(join(sceneDir, mainFile), 'utf-8');
|
|
93
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { join, relative, sep } from 'node:path';
|
|
2
|
+
import { WebSocketServer } from 'ws';
|
|
3
|
+
import chokidar from 'chokidar';
|
|
4
|
+
import { scanScenes, scanSingleScene, readSceneCode } from './scene-scanner.js';
|
|
5
|
+
|
|
6
|
+
export function vijiScenePlugin() {
|
|
7
|
+
const scenesDir = join(process.cwd(), 'scenes');
|
|
8
|
+
let wss;
|
|
9
|
+
let watcher;
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
name: 'viji-scene-server',
|
|
13
|
+
|
|
14
|
+
configureServer(server) {
|
|
15
|
+
const clients = new Set();
|
|
16
|
+
|
|
17
|
+
wss = new WebSocketServer({ noServer: true });
|
|
18
|
+
|
|
19
|
+
server.httpServer.on('upgrade', (req, socket, head) => {
|
|
20
|
+
if (req.url === '/ws') {
|
|
21
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
22
|
+
wss.emit('connection', ws, req);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
wss.on('connection', (ws) => {
|
|
28
|
+
clients.add(ws);
|
|
29
|
+
ws.on('close', () => clients.delete(ws));
|
|
30
|
+
ws.on('error', () => clients.delete(ws));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function broadcast(message) {
|
|
34
|
+
const data = JSON.stringify(message);
|
|
35
|
+
for (const ws of clients) {
|
|
36
|
+
if (ws.readyState === 1) ws.send(data);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
watcher = chokidar.watch(scenesDir, {
|
|
41
|
+
ignoreInitial: true,
|
|
42
|
+
ignored: /(^|[/\\])\./,
|
|
43
|
+
persistent: true,
|
|
44
|
+
depth: 3,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
watcher.on('add', async (filePath) => {
|
|
48
|
+
const sceneName = extractSceneName(scenesDir, filePath);
|
|
49
|
+
if (!sceneName) return;
|
|
50
|
+
const scene = await scanSingleScene(scenesDir, sceneName);
|
|
51
|
+
if (scene) broadcast({ type: 'scene:add', scene });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
watcher.on('change', async (filePath) => {
|
|
55
|
+
const sceneName = extractSceneName(scenesDir, filePath);
|
|
56
|
+
if (!sceneName) return;
|
|
57
|
+
const scene = await scanSingleScene(scenesDir, sceneName);
|
|
58
|
+
if (scene) broadcast({ type: 'scene:update', scene });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
watcher.on('unlink', (filePath) => {
|
|
62
|
+
const sceneName = extractSceneName(scenesDir, filePath);
|
|
63
|
+
if (!sceneName) return;
|
|
64
|
+
broadcast({ type: 'scene:remove', sceneId: sceneName });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
server.middlewares.use(async (req, res, next) => {
|
|
68
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
69
|
+
const pathname = url.pathname;
|
|
70
|
+
|
|
71
|
+
if (pathname === '/api/scenes') {
|
|
72
|
+
try {
|
|
73
|
+
const scenes = await scanScenes(scenesDir);
|
|
74
|
+
sendJSON(res, scenes);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
sendJSON(res, { error: err.message }, 500);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const codeMatch = pathname.match(/^\/api\/scenes\/([^/]+)\/code$/);
|
|
82
|
+
if (codeMatch) {
|
|
83
|
+
try {
|
|
84
|
+
const code = await readSceneCode(scenesDir, decodeURIComponent(codeMatch[1]));
|
|
85
|
+
if (code === null) {
|
|
86
|
+
sendJSON(res, { error: 'Scene not found' }, 404);
|
|
87
|
+
} else {
|
|
88
|
+
sendJSON(res, { code });
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
sendJSON(res, { error: err.message }, 500);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sceneMatch = pathname.match(/^\/api\/scenes\/([^/]+)$/);
|
|
97
|
+
if (sceneMatch) {
|
|
98
|
+
try {
|
|
99
|
+
const scene = await scanSingleScene(scenesDir, decodeURIComponent(sceneMatch[1]));
|
|
100
|
+
if (!scene) {
|
|
101
|
+
sendJSON(res, { error: 'Scene not found' }, 404);
|
|
102
|
+
} else {
|
|
103
|
+
sendJSON(res, scene);
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
sendJSON(res, { error: err.message }, 500);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
next();
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
closeBundle() {
|
|
116
|
+
if (watcher) watcher.close();
|
|
117
|
+
if (wss) wss.close();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function sendJSON(res, data, status = 200) {
|
|
123
|
+
res.writeHead(status, {
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
'Access-Control-Allow-Origin': '*',
|
|
126
|
+
});
|
|
127
|
+
res.end(JSON.stringify(data));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function extractSceneName(scenesDir, filePath) {
|
|
131
|
+
const rel = relative(scenesDir, filePath);
|
|
132
|
+
const parts = rel.split(sep);
|
|
133
|
+
return parts.length >= 1 ? parts[0] : null;
|
|
134
|
+
}
|
|
@@ -1,128 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { existsSync, readFileSync } from 'fs';
|
|
6
|
-
import { join, dirname } from 'path';
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
7
3
|
|
|
8
4
|
export function validateProject(projectDir = process.cwd()) {
|
|
9
|
-
// Support two layouts:
|
|
10
|
-
// 1) SDK scene folder: scenes/<project>/main.(js|ts)
|
|
11
|
-
// 2) External project: src/main.(js|ts)
|
|
12
5
|
const candidates = [
|
|
13
6
|
join(projectDir, 'main.ts'),
|
|
14
7
|
join(projectDir, 'main.js'),
|
|
15
8
|
join(projectDir, 'main.tsx'),
|
|
16
9
|
join(projectDir, 'main.jsx'),
|
|
17
|
-
join(projectDir, '
|
|
18
|
-
join(projectDir, 'src', 'main.js'),
|
|
19
|
-
join(projectDir, 'src', 'main.tsx'),
|
|
20
|
-
join(projectDir, 'src', 'main.jsx')
|
|
10
|
+
join(projectDir, 'main.glsl'),
|
|
21
11
|
];
|
|
22
12
|
|
|
23
13
|
const mainScenePath = candidates.find(p => existsSync(p));
|
|
24
|
-
const packageJsonPath = join(projectDir, 'package.json');
|
|
25
14
|
|
|
26
15
|
const issues = [];
|
|
27
16
|
if (!mainScenePath) {
|
|
28
|
-
issues.push('No main.(js|ts|jsx|tsx) found in
|
|
17
|
+
issues.push('No main.(js|ts|jsx|tsx|glsl) found in scene folder');
|
|
29
18
|
}
|
|
30
19
|
|
|
31
20
|
return {
|
|
32
21
|
valid: issues.length === 0,
|
|
33
22
|
issues,
|
|
34
23
|
paths: {
|
|
35
|
-
packageJson: packageJsonPath,
|
|
36
24
|
mainScene: mainScenePath || join(projectDir, 'main.js'),
|
|
37
|
-
projectDir
|
|
38
|
-
}
|
|
25
|
+
projectDir,
|
|
26
|
+
},
|
|
39
27
|
};
|
|
40
28
|
}
|
|
41
29
|
|
|
42
|
-
export function getProjectInfo(projectDir = process.cwd()) {
|
|
43
|
-
const validation = validateProject(projectDir);
|
|
44
|
-
|
|
45
|
-
if (!validation.valid) {
|
|
46
|
-
return { valid: false, ...validation };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const packageJson = JSON.parse(readFileSync(validation.paths.packageJson, 'utf8'));
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
valid: true,
|
|
54
|
-
name: packageJson.name,
|
|
55
|
-
version: packageJson.version,
|
|
56
|
-
description: packageJson.description,
|
|
57
|
-
dependencies: packageJson.dependencies || {},
|
|
58
|
-
devDependencies: packageJson.devDependencies || {},
|
|
59
|
-
scripts: packageJson.scripts || {},
|
|
60
|
-
paths: validation.paths
|
|
61
|
-
};
|
|
62
|
-
} catch (error) {
|
|
63
|
-
return {
|
|
64
|
-
valid: false,
|
|
65
|
-
issues: ['Could not read project information'],
|
|
66
|
-
error: error.message
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function formatProjectPath(projectName, targetDir = process.cwd()) {
|
|
72
|
-
return join(targetDir, projectName);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function generateProjectId() {
|
|
76
|
-
return Math.random().toString(36).substr(2, 9);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
30
|
export function validateProjectName(name) {
|
|
80
31
|
const issues = [];
|
|
81
|
-
|
|
32
|
+
|
|
82
33
|
if (!name || name.trim().length === 0) {
|
|
83
|
-
issues.push('
|
|
34
|
+
issues.push('Scene name cannot be empty');
|
|
84
35
|
}
|
|
85
|
-
|
|
86
|
-
if (name.length > 214) {
|
|
87
|
-
issues.push('
|
|
36
|
+
|
|
37
|
+
if (name && name.length > 214) {
|
|
38
|
+
issues.push('Scene name too long (max 214 characters)');
|
|
88
39
|
}
|
|
89
|
-
|
|
90
|
-
if (name.toLowerCase() !== name) {
|
|
91
|
-
issues.push('
|
|
40
|
+
|
|
41
|
+
if (name && name.toLowerCase() !== name) {
|
|
42
|
+
issues.push('Scene name should be lowercase');
|
|
92
43
|
}
|
|
93
|
-
|
|
94
|
-
if (!/^[a-z0-9-_]+$/.test(name)) {
|
|
95
|
-
issues.push('
|
|
44
|
+
|
|
45
|
+
if (name && !/^[a-z0-9-_]+$/.test(name)) {
|
|
46
|
+
issues.push('Scene name can only contain lowercase letters, numbers, hyphens, and underscores');
|
|
96
47
|
}
|
|
97
|
-
|
|
98
|
-
if (name.startsWith('.') || name.startsWith('-') || name.startsWith('_')) {
|
|
99
|
-
issues.push('
|
|
48
|
+
|
|
49
|
+
if (name && (name.startsWith('.') || name.startsWith('-') || name.startsWith('_'))) {
|
|
50
|
+
issues.push('Scene name cannot start with a dot, hyphen, or underscore');
|
|
100
51
|
}
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
issues.push(`
|
|
52
|
+
|
|
53
|
+
const reserved = ['viji', 'viji-sdk', 'core', 'main', 'index', 'src', 'dist', 'build', 'node_modules'];
|
|
54
|
+
if (name && reserved.includes(name.toLowerCase())) {
|
|
55
|
+
issues.push(`Scene name "${name}" is reserved`);
|
|
105
56
|
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
valid: issues.length === 0,
|
|
109
|
-
issues
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
57
|
|
|
113
|
-
|
|
114
|
-
const colors = {
|
|
115
|
-
red: '\x1b[31m',
|
|
116
|
-
green: '\x1b[32m',
|
|
117
|
-
yellow: '\x1b[33m',
|
|
118
|
-
blue: '\x1b[34m',
|
|
119
|
-
magenta: '\x1b[35m',
|
|
120
|
-
cyan: '\x1b[36m',
|
|
121
|
-
white: '\x1b[37m',
|
|
122
|
-
reset: '\x1b[0m'
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
console.log(`${colors[color]}${icon} ${message}${colors.reset}`);
|
|
58
|
+
return { valid: issues.length === 0, issues };
|
|
126
59
|
}
|
|
127
60
|
|
|
128
61
|
export function formatDuration(ms) {
|
|
@@ -133,20 +66,18 @@ export function formatDuration(ms) {
|
|
|
133
66
|
|
|
134
67
|
export function formatFileSize(bytes) {
|
|
135
68
|
if (bytes === 0) return '0 B';
|
|
136
|
-
|
|
137
69
|
const k = 1024;
|
|
138
70
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
139
71
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
140
|
-
|
|
141
72
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
142
73
|
}
|
|
143
74
|
|
|
144
75
|
export function createSpinner(text = 'Loading...') {
|
|
145
|
-
const frames = ['
|
|
76
|
+
const frames = ['\u28CB', '\u28D9', '\u28F9', '\u28F8', '\u28FC', '\u28F4', '\u28E6', '\u28E7', '\u28C7', '\u28CF'];
|
|
146
77
|
let i = 0;
|
|
147
78
|
let intervalId;
|
|
148
|
-
|
|
149
|
-
|
|
79
|
+
|
|
80
|
+
return {
|
|
150
81
|
start() {
|
|
151
82
|
process.stdout.write(`${frames[0]} ${text}`);
|
|
152
83
|
intervalId = setInterval(() => {
|
|
@@ -154,55 +85,14 @@ export function createSpinner(text = 'Loading...') {
|
|
|
154
85
|
i = (i + 1) % frames.length;
|
|
155
86
|
}, 80);
|
|
156
87
|
},
|
|
157
|
-
|
|
158
88
|
stop(finalText) {
|
|
159
89
|
if (intervalId) {
|
|
160
90
|
clearInterval(intervalId);
|
|
161
91
|
process.stdout.write(`\r${finalText || text}\n`);
|
|
162
92
|
}
|
|
163
93
|
},
|
|
164
|
-
|
|
165
94
|
update(newText) {
|
|
166
95
|
text = newText;
|
|
167
|
-
}
|
|
96
|
+
},
|
|
168
97
|
};
|
|
169
|
-
|
|
170
|
-
return spinner;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function isVijiWorkspace(dir = process.cwd()) {
|
|
174
|
-
// Check for key workspace files
|
|
175
|
-
const requiredFiles = [
|
|
176
|
-
join(dir, 'src'),
|
|
177
|
-
join(dir, 'scenes'),
|
|
178
|
-
join(dir, 'vite.config.ts'),
|
|
179
|
-
join(dir, 'package.json')
|
|
180
|
-
];
|
|
181
|
-
|
|
182
|
-
// All required files must exist
|
|
183
|
-
if (!requiredFiles.every(file => existsSync(file))) {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Check if package.json has viji workspace marker
|
|
188
|
-
try {
|
|
189
|
-
const packageJson = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
190
|
-
return packageJson.viji && packageJson.viji.workspaceVersion;
|
|
191
|
-
} catch (error) {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
98
|
}
|
|
195
|
-
|
|
196
|
-
export function findWorkspaceRoot(startDir = process.cwd()) {
|
|
197
|
-
let currentDir = startDir;
|
|
198
|
-
|
|
199
|
-
while (currentDir !== dirname(currentDir)) {
|
|
200
|
-
if (isVijiWorkspace(currentDir)) {
|
|
201
|
-
return currentDir;
|
|
202
|
-
}
|
|
203
|
-
currentDir = dirname(currentDir);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
|