Skip to content

Commit 3a1e3a6

Browse files
committed
custom server
1 parent b720709 commit 3a1e3a6

File tree

14 files changed

+8431
-3
lines changed

14 files changed

+8431
-3
lines changed

app-server/TODO.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
- [ ] test merged servers (esbuild and hotreload)
2+
- [ ] simplify 'blueprint' object in `world-links.json`
3+
- [ ] add scripting docs
4+
- [ ] how to handle deletions?
5+
- [ ] claude instructions
6+
- [ ] maybe commands or subagent definitions
7+
- [ ] typescript
8+
- [ ] `__HYPERFY_CONFIG__` and `app.configure` are redundant. think about how to merge without changing syntax too much.
9+
- [ ] save auth token of connection
10+
- [ ] make browserless connection via node client (update apps on many worlds)
11+
- [ ] maybe remove `eval` from `build.mjs`
12+
- [ ] better conflict resolution (linking app with already existing name)
13+
- [ ] auto link on deploy
14+
- [ ] merge cli and server into single terminal interface program

app-server/apps/cube/index.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { send } from '~/utils.js'
2+
3+
const __HYPERFY_CONFIG__ = {
4+
model: "cube",
5+
props: {
6+
message: "hello worlder!"
7+
}
8+
};
9+
10+
app.configure([
11+
{
12+
key: 'message',
13+
type: 'text',
14+
label: 'Message',
15+
hint: 'A message to be sent to the client.'
16+
},
17+
{
18+
key: 'send',
19+
type: 'button',
20+
label: 'Send',
21+
hint: 'Send the message to the boys.',
22+
onClick: () => {
23+
send(app.props.message)
24+
}
25+
}
26+
])
27+
28+
if(world.isServer) {
29+
app.on('sendMessage', (message) => {
30+
console.log('Message received:', message)
31+
})
32+
}

app-server/assets/cube.glb

1.89 KB
Binary file not shown.

app-server/build.mjs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import * as esbuild from 'esbuild'
2+
import fs from 'fs/promises'
3+
import path from 'path'
4+
import crypto from 'crypto'
5+
6+
const dev = process.argv.includes('--dev')
7+
8+
// Hash function copied from utils-server.js
9+
async function hashFile(file) {
10+
const hash = crypto.createHash('sha256')
11+
hash.update(file)
12+
return hash.digest('hex')
13+
}
14+
15+
// Get all available apps from the apps directory
16+
async function getAvailableApps() {
17+
const appsDir = 'apps'
18+
try {
19+
const entries = await fs.readdir(appsDir, { withFileTypes: true })
20+
const appFolders = entries
21+
.filter(entry => entry.isDirectory())
22+
.map(entry => entry.name)
23+
24+
// Filter for apps that have an index.js file
25+
const validApps = []
26+
for (const appName of appFolders) {
27+
const indexPath = path.join(appsDir, appName, 'index.js')
28+
try {
29+
await fs.access(indexPath)
30+
validApps.push(appName)
31+
} catch {
32+
// index.js doesn't exist, skip this app
33+
}
34+
}
35+
36+
return validApps
37+
} catch (error) {
38+
console.error('Error reading apps directory:', error)
39+
return []
40+
}
41+
}
42+
43+
// Extract config from source file
44+
function extractConfig(source) {
45+
// Look for const __HYPERFY_CONFIG__ = { ... }
46+
const configRegex = /const\s+__HYPERFY_CONFIG__\s*=\s*({[\s\S]*?});/
47+
const match = source.match(configRegex)
48+
49+
if (!match) {
50+
throw new Error('No __HYPERFY_CONFIG__ found in source file')
51+
}
52+
53+
try {
54+
// Use eval to parse the object literal (safe since we control the source)
55+
const configObj = eval(`(${match[1]})`)
56+
return configObj
57+
} catch (error) {
58+
throw new Error(`Failed to parse __HYPERFY_CONFIG__: ${error.message}`)
59+
}
60+
}
61+
62+
// Generate config.json in build directory for a specific app
63+
async function generateConfig(appName) {
64+
// Read the source index.js file to extract config
65+
const sourceIndexPath = path.join('apps', appName, 'index.js')
66+
const sourceContent = await fs.readFile(sourceIndexPath, 'utf-8')
67+
const sourceConfig = extractConfig(sourceContent)
68+
69+
// Hash the built JS file
70+
const builtJsPath = path.join('build', appName, 'index.js')
71+
const jsContent = await fs.readFile(builtJsPath)
72+
const jsHash = await hashFile(jsContent)
73+
74+
// Hash the model asset file
75+
const modelAssetPath = path.join('assets', `${sourceConfig.model}.glb`)
76+
let modelHash
77+
try {
78+
const modelContent = await fs.readFile(modelAssetPath)
79+
modelHash = await hashFile(modelContent)
80+
} catch (error) {
81+
throw new Error(`Model asset not found: ${modelAssetPath}. Expected file: ${sourceConfig.model}.glb`)
82+
}
83+
84+
// Generate the new config
85+
const buildConfig = {
86+
name: appName,
87+
script: `asset://${jsHash}.js`,
88+
model: `asset://${modelHash}.glb`,
89+
props: sourceConfig.props
90+
}
91+
92+
// Ensure build directory exists for this app
93+
const buildDir = path.join('build', appName)
94+
await fs.mkdir(buildDir, { recursive: true })
95+
96+
// Write the config to build directory
97+
const buildConfigPath = path.join('build', appName, 'config.json')
98+
await fs.writeFile(buildConfigPath, JSON.stringify(buildConfig, null, 2))
99+
100+
console.log(`Generated config.json for ${appName}`)
101+
}
102+
103+
// Generate configs for all built apps
104+
async function generateAllConfigs(appNames) {
105+
for (const appName of appNames) {
106+
try {
107+
await generateConfig(appName)
108+
} catch (error) {
109+
console.error(`Failed to generate config for ${appName}:`, error.message)
110+
}
111+
}
112+
}
113+
114+
// Get all available apps
115+
const availableApps = await getAvailableApps()
116+
117+
if (availableApps.length === 0) {
118+
console.log('No apps found to build')
119+
process.exit(0)
120+
}
121+
122+
console.log(`Found ${availableApps.length} app(s) to build: ${availableApps.join(', ')}`)
123+
124+
// Build each app separately
125+
const buildPromises = availableApps.map(async (appName) => {
126+
console.log(`Building ${appName}...`)
127+
128+
const ctx = await esbuild.context({
129+
// Build settings applied to all apps
130+
format: 'esm',
131+
bundle: true,
132+
minify: false, // true for production builds
133+
sourcemap: true, // for debugging
134+
external: ['hyperfy'], // runtime-provided globals
135+
136+
// Shared code resolution paths
137+
alias: {
138+
'~': './',
139+
},
140+
141+
splitting: false,
142+
treeShaking: true,
143+
entryPoints: [path.join('apps', appName, 'index.js')],
144+
outfile: path.join('build', appName, 'index.js'),
145+
plugins: [
146+
{
147+
name: 'config-stripper',
148+
setup(build) {
149+
// Remove __HYPERFY_CONFIG__ from the source during build
150+
build.onLoad({ filter: /\/apps\/.*\/index\.js$/ }, async (args) => {
151+
const source = await fs.readFile(args.path, 'utf-8')
152+
153+
// Remove the __HYPERFY_CONFIG__ const declaration
154+
const strippedSource = source.replace(
155+
/const\s+__HYPERFY_CONFIG__\s*=\s*{[\s\S]*?};\s*/,
156+
''
157+
)
158+
159+
return {
160+
contents: strippedSource,
161+
loader: 'js'
162+
}
163+
})
164+
}
165+
},
166+
{
167+
name: 'config-generator',
168+
setup(build) {
169+
build.onEnd(async () => {
170+
try {
171+
await generateConfig(appName)
172+
} catch (error) {
173+
console.error(`Failed to generate config for ${appName}:`, error.message)
174+
}
175+
})
176+
}
177+
}
178+
]
179+
})
180+
181+
if (dev) {
182+
await ctx.watch()
183+
console.log(`Watching ${appName} for changes...`)
184+
return ctx // Return context for cleanup later
185+
} else {
186+
await ctx.rebuild()
187+
await ctx.dispose()
188+
console.log(`Built ${appName} successfully`)
189+
return null
190+
}
191+
})
192+
193+
if (dev) {
194+
const contexts = await Promise.all(buildPromises)
195+
console.log('Watching all apps for changes...')
196+
197+
// Handle cleanup on exit
198+
process.on('SIGINT', async () => {
199+
console.log('\nCleaning up...')
200+
await Promise.all(contexts.filter(ctx => ctx).map(ctx => ctx.dispose()))
201+
process.exit(0)
202+
})
203+
} else {
204+
await Promise.all(buildPromises)
205+
console.log('All apps built successfully')
206+
}

app-server/checkDupes.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env node
2+
3+
import fs from 'fs/promises'
4+
import path from 'path'
5+
import crypto from 'crypto'
6+
7+
// Hash function (same as in build.mjs)
8+
async function hashFile(file) {
9+
const hash = crypto.createHash('sha256')
10+
hash.update(file)
11+
return hash.digest('hex')
12+
}
13+
14+
// Get all files in assets directory
15+
async function getAssetFiles(assetsDir = 'assets') {
16+
const files = []
17+
18+
try {
19+
const entries = await fs.readdir(assetsDir, { withFileTypes: true })
20+
21+
for (const entry of entries) {
22+
if (entry.isFile() && entry.name !== 'names.json') {
23+
const filePath = path.join(assetsDir, entry.name)
24+
files.push({
25+
name: entry.name,
26+
path: filePath
27+
})
28+
}
29+
}
30+
} catch (error) {
31+
console.error(`Error reading assets directory: ${error.message}`)
32+
return []
33+
}
34+
35+
return files
36+
}
37+
38+
// Check for duplicate files by comparing hashes
39+
async function checkDuplicates() {
40+
console.log('🔍 Checking for duplicate assets...\n')
41+
42+
const assetFiles = await getAssetFiles()
43+
44+
if (assetFiles.length === 0) {
45+
console.log('📁 No asset files found')
46+
return
47+
}
48+
49+
console.log(`📂 Found ${assetFiles.length} asset file(s)`)
50+
51+
const hashMap = new Map() // hash -> array of files with that hash
52+
const fileHashes = new Map() // filename -> hash
53+
54+
// Calculate hashes for all files
55+
for (const file of assetFiles) {
56+
try {
57+
const content = await fs.readFile(file.path)
58+
const hash = await hashFile(content)
59+
60+
fileHashes.set(file.name, hash)
61+
62+
if (!hashMap.has(hash)) {
63+
hashMap.set(hash, [])
64+
}
65+
hashMap.get(hash).push(file)
66+
67+
console.log(`📄 ${file.name} -> ${hash}`)
68+
} catch (error) {
69+
console.error(`❌ Error processing ${file.name}: ${error.message}`)
70+
}
71+
}
72+
73+
console.log()
74+
75+
// Find duplicates
76+
const duplicates = []
77+
for (const [hash, files] of hashMap.entries()) {
78+
if (files.length > 1) {
79+
duplicates.push({ hash, files })
80+
}
81+
}
82+
83+
if (duplicates.length === 0) {
84+
console.log('✅ No duplicate assets found! All files are unique.')
85+
} else {
86+
console.log(`⚠️ Found ${duplicates.length} group(s) of duplicate assets:\n`)
87+
88+
duplicates.forEach((group, index) => {
89+
console.log(`🔗 Duplicate Group ${index + 1} (hash: ${group.hash}):`)
90+
group.files.forEach(file => {
91+
console.log(` - ${file.name}`)
92+
})
93+
console.log()
94+
})
95+
96+
console.log('💡 Consider removing duplicate files to save space and avoid confusion.')
97+
}
98+
99+
// Summary
100+
console.log('\n📊 Summary:')
101+
console.log(` Total files: ${assetFiles.length}`)
102+
console.log(` Unique files: ${hashMap.size}`)
103+
console.log(` Duplicate groups: ${duplicates.length}`)
104+
105+
if (duplicates.length > 0) {
106+
const duplicateFileCount = duplicates.reduce((sum, group) => sum + group.files.length - 1, 0)
107+
console.log(` Duplicate files: ${duplicateFileCount}`)
108+
}
109+
}
110+
111+
// Run the check
112+
checkDuplicates().catch(error => {
113+
console.error('❌ Error checking duplicates:', error.message)
114+
process.exit(1)
115+
})

0 commit comments

Comments
 (0)