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 = / c o n s t \s + _ _ H Y P E R F Y _ C O N F I G _ _ \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 : / \/ a p p s \/ .* \/ i n d e x \. j s $ / } , 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+ / c o n s t \s + _ _ H Y P E R F Y _ C O N F I G _ _ \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+ }
0 commit comments