From 3f20caaa292362b54b9264f2def0a692ed32c6cb Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Tue, 13 May 2025 17:38:54 +0800 Subject: [PATCH 01/11] remove loader and fix vector issue --- src/core/createNodeClientWorld.js | 2 +- src/core/extras/simpleCamLerp.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/createNodeClientWorld.js b/src/core/createNodeClientWorld.js index 35cc35b9..85bf4049 100644 --- a/src/core/createNodeClientWorld.js +++ b/src/core/createNodeClientWorld.js @@ -14,7 +14,7 @@ export function createNodeClientWorld() { world.register('client', NodeClient) world.register('controls', ClientControls) world.register('network', ClientNetwork) - world.register('loader', ServerLoader) // TODO: ClientLoader should be named BrowserLoader and ServerLoader should be called NodeLoader + // world.register('loader', ServerLoader) // TODO: ClientLoader should be named BrowserLoader and ServerLoader should be called NodeLoader world.register('environment', NodeEnvironment) // world.register('actions', ClientActions) // world.register('lods', LODs) diff --git a/src/core/extras/simpleCamLerp.js b/src/core/extras/simpleCamLerp.js index d49e1b74..2061ae42 100644 --- a/src/core/extras/simpleCamLerp.js +++ b/src/core/extras/simpleCamLerp.js @@ -4,6 +4,7 @@ import { Layers } from './Layers' const BACKWARD = new THREE.Vector3(0, 0, 1) const v1 = new THREE.Vector3() +const v2= new THREE.Vector3() let sweepGeometry @@ -25,7 +26,7 @@ export function simpleCamLerp(world, camera, target, delta) { // } // EXPERIMENTAL: snap camera position instead - camera.position.copy(target.position) + camera.position = v2.copy(target.position) // raycast backward to check for zoom collision if (!sweepGeometry) sweepGeometry = new PHYSX.PxSphereGeometry(0.2) From 1084d3e7d075653373f9081e78ad3104b06e85a4 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Tue, 13 May 2025 17:39:09 +0800 Subject: [PATCH 02/11] remove controls --- src/core/createNodeClientWorld.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/createNodeClientWorld.js b/src/core/createNodeClientWorld.js index 85bf4049..7a4862d6 100644 --- a/src/core/createNodeClientWorld.js +++ b/src/core/createNodeClientWorld.js @@ -12,7 +12,7 @@ import { NodeEnvironment } from './systems/NodeEnvironment' export function createNodeClientWorld() { const world = new World() world.register('client', NodeClient) - world.register('controls', ClientControls) + // world.register('controls', ClientControls) world.register('network', ClientNetwork) // world.register('loader', ServerLoader) // TODO: ClientLoader should be named BrowserLoader and ServerLoader should be called NodeLoader world.register('environment', NodeEnvironment) From 235cc1cd53b176c47ee21c1a00e1bbd8f67da1d5 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Tue, 13 May 2025 17:41:23 +0800 Subject: [PATCH 03/11] remove import --- src/core/createNodeClientWorld.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/createNodeClientWorld.js b/src/core/createNodeClientWorld.js index 7a4862d6..dda370fa 100644 --- a/src/core/createNodeClientWorld.js +++ b/src/core/createNodeClientWorld.js @@ -1,9 +1,9 @@ import { World } from './World' import { NodeClient } from './systems/NodeClient' -import { ClientControls } from './systems/ClientControls' +// import { ClientControls } from './systems/ClientControls' import { ClientNetwork } from './systems/ClientNetwork' -import { ServerLoader } from './systems/ServerLoader' +// import { ServerLoader } from './systems/ServerLoader' import { NodeEnvironment } from './systems/NodeEnvironment' // import { ClientActions } from './systems/ClientActions' // import { LODs } from './systems/LODs' From 1f86d63c2aab02d89a2485fc5cc1ad69d004873e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 06:01:02 +0000 Subject: [PATCH 04/11] feat: Prepare for NPM packaging I've set up the project for NPM packaging of the Node.js client. Key changes include: - Updates `src/node-client/index.js` to export System, Node, and World. - Modifies `scripts/build-node-client.mjs` to: - Output to `dist/npm/`. - Copy PhysX assets to `dist/npm/vendor/`. - Generate a `package.json` for the NPM package in `dist/npm/`. - Generate a fallback `index.d.ts` in `dist/npm/` (automated generation via dts-bundle-generator failed when I checked it). - Adds `typescript` and `dts-bundle-generator` as devDependencies. - Updates the main `package.json` with a `publish:node` script and adds `dist/npm/` to the `files` array. - Adds a GitHub Actions workflow (`.github/workflows/npm-publish.yml`) to automate publishing to NPM on new GitHub releases. - Updates `README.md` with installation and publishing instructions. Note: Further work is required to address: - A runtime error "navigator is not defined" when using the client in Node.js. - Issues with the accuracy and completeness of the generated `index.d.ts` TypeScript definitions. This commit includes all preparatory work up to the point of encountering these critical issues when I checked it. --- .github/workflows/npm-publish.yml | 30 +++++ README.md | 99 +++++++++++++++ package.json | 12 +- scripts/build-node-client.mjs | 192 +++++++++++++++++++++++++++++- src/node-client/index.js | 2 + tsconfig.dts.json | 37 ++++++ 6 files changed, 364 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/npm-publish.yml create mode 100644 tsconfig.dts.json diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 00000000..ee55a6cf --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,30 @@ +name: Publish Node.js Client to NPM + +on: + release: + types: [created] + +jobs: + publish-npm: + runs-on: ubuntu-latest + permissions: + contents: read # Needed to check out the repository + # id-token: write # Potentially needed for provenance if publishing with that feature, but not strictly required for basic NPM publish + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' # .nvmrc exists and should specify the correct Node version (e.g., 22.x) + registry-url: 'https://registry.npmjs.org/' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build and publish to NPM + run: npm run publish:node + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index 9585e722..7df65d16 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,75 @@ For containerized deployment, check [DOCKER.md](DOCKER.md) for detailed instruct - **Educational Experiences** - Develop interactive learning spaces - **Creative Showcases** - Display 3D art and interactive installations +## 📦 Using Hyperfy as a Node.js Module + +Beyond running a full Hyperfy server, you can integrate Hyperfy's core functionalities into your own Node.js applications by using the official NPM package. This allows you to create and manage Hyperfy worlds, entities, and systems programmatically. + +### Installation + +To install the Hyperfy Node.js client package, use npm: + +```bash +npm install hyperfy +``` +*(Note: Ensure the package name `hyperfy` matches the name intended for NPM publication.)* + +### Example Usage + +Here's a basic example of how to import and use components from the `hyperfy` package: + +```javascript +// example.js +import { createNodeClientWorld, System, Node, World } from 'hyperfy'; + +async function main() { + // The createNodeClientWorld function initializes a world suitable for server-side/headless operations. + // It may require specific options depending on your setup (e.g., for PhysX, assets). + const world = await createNodeClientWorld({ + // Example options (refer to documentation for details): + // physxWorker: new Worker(new URL('./physx-worker.js', import.meta.url)), // If PhysX is needed + // assetsDir: './assets', // Path to your assets directory + }); + + console.log('Hyperfy World instance created:', world); + + // Example: Define a simple system + class MySystem extends System { + start() { + console.log('MySystem started!'); + } + + update(delta) { + // Called every frame + } + } + + // Register the system with the world + world.register('mySystem', MySystem); + + // Example: Create and add a node + const myNode = new Node({ id: 'myCube', name: 'My Cube' }); + myNode.position.set(0, 1, -2); // Set position (Vector3) + // world.addNode(myNode); // How nodes are added might depend on specific world setup + + // Initialize and start the world (if not auto-started by createNodeClientWorld) + // await world.init({}); // Pass necessary initialization options + // world.start(); + + console.log('Node created:', myNode.id, myNode.name, myNode.position.x, myNode.position.y, myNode.position.z); + + // To run the world's simulation loop (if applicable for your use case): + // function gameLoop() { + // world.tick(performance.now()); + // setImmediate(gameLoop); // or requestAnimationFrame if in a context that supports it + // } + // gameLoop(); +} + +main().catch(console.error); +``` +This example demonstrates basic setup. Refer to the core module documentation for more in-depth usage of `World`, `Node`, `System`, and other components. + ## 📚 Documentation & Resources - **[Community Documentation](https://hyperfy.how)** - Comprehensive guides and reference @@ -111,6 +180,36 @@ Contributions are welcome! Please check out our [contributing guidelines](CONTRI 4. Push to the branch: `git push origin feature/amazing-feature` 5. Open a pull request +### Publishing the Node.js Client Package + +The Hyperfy Node.js client is packaged for NPM distribution. Publishing is handled both automatically via GitHub Actions and can be done manually. + +#### Automated Publishing (GitHub Releases) + +- **Trigger**: New versions of the `hyperfy` NPM package are automatically built and published when a new release is created on the GitHub repository. +- **Workflow**: This process is managed by the `.github/workflows/npm-publish.yml` GitHub Actions workflow. +- **Requirements**: For automated publishing to succeed, the `NPM_TOKEN` secret must be configured in the GitHub repository settings. This token grants the workflow permission to publish to NPM. + +#### Manual Publishing + +To publish the package manually, follow these steps: + +1. **Ensure Version is Updated**: Before publishing, verify that the `version` field in the main `package.json` is updated to the new version number you intend to release. The build script (`scripts/build-node-client.mjs`) uses this version for the package generated in `dist/npm/`. +2. **Authenticate with NPM**: You must be logged into NPM on your local machine with an account that has publish permissions for the `hyperfy` package. If you are not logged in, run: + ```bash + npm login + ``` +3. **Run Publish Script**: Execute the following command from the root of the repository: + ```bash + npm run publish:node + ``` + This script will: + * Build the Node.js client package into the `dist/npm/` directory. + * Change the current directory to `dist/npm/`. + * Run `npm publish` from within `dist/npm/`. + +**Important**: Always ensure that the package is thoroughly tested and the version number is correct before publishing, whether manually or by creating a GitHub release. + ## 🌱 Project Status This project is still in alpha as we transition all of our [reference platform](https://github.com/hyperfy-xyz/hyperfy-ref) code into fully self hostable worlds. diff --git a/package.json b/package.json index f6ab498c..de49bb84 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,14 @@ "lint": "eslint . --ext .js,.jsx", "lint:fix": "eslint . --ext .js,.jsx --fix", "format": "prettier --write .", - "check": "npm run lint && npm run format" + "check": "npm run lint && npm run format", + "publish:node": "node scripts/build-node-client.mjs && cd dist/npm && npm publish" }, + "files": [ + "dist/npm/", + "README.md", + "LICENSE" + ], "dependencies": { "@fastify/compress": "^8.0.1", "@fastify/cors": "^10.0.1", @@ -77,7 +83,9 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-react": "^7.34.0", "eslint-plugin-react-hooks": "^4.6.0", - "prettier": "^3.4.2" + "prettier": "^3.4.2", + "typescript": "^5.0.0", + "dts-bundle-generator": "^9.0.0" }, "engines": { "npm": ">=10.0.0", diff --git a/scripts/build-node-client.mjs b/scripts/build-node-client.mjs index b4755ac4..559a21b5 100644 --- a/scripts/build-node-client.mjs +++ b/scripts/build-node-client.mjs @@ -9,9 +9,11 @@ import { polyfillNode } from 'esbuild-plugin-polyfill-node' const dev = process.argv.includes('--dev') const dirname = path.dirname(fileURLToPath(import.meta.url)) const rootDir = path.join(dirname, '../') -const buildDir = path.join(rootDir, 'build') +const buildDir = path.join(rootDir, 'build') // This can remain for other potential build outputs or be removed if not used elsewhere +const npmPackageDir = path.join(rootDir, 'dist/npm') -// await fs.emptyDir(buildDir) +// await fs.emptyDir(buildDir) // Keep if buildDir is still used for other things +await fs.emptyDir(npmPackageDir) // Ensure the new package directory is clean /** * Build Node Client @@ -24,9 +26,12 @@ const buildDir = path.join(rootDir, 'build') let spawn { + // Read root package.json for details + const rootPackageJson = await fs.readJson(path.join(rootDir, 'package.json')) + const nodeClientCtx = await esbuild.context({ entryPoints: ['src/node-client/index.js'], - outfile: 'build/world-node-client.js', + outfile: path.join(npmPackageDir, 'index.js'), // Changed output path platform: 'node', format: 'esm', bundle: true, @@ -40,20 +45,194 @@ let spawn name: 'server-finalize-plugin', setup(build) { build.onEnd(async result => { + if (result.errors.length > 0) { + console.error('Build failed with errors:', result.errors) + if (!dev) process.exit(1) // Exit if not in dev mode + return + } + // copy over physx js + // copy over physx js and wasm to a vendor subdirectory + const vendorDir = path.join(npmPackageDir, 'vendor') + await fs.ensureDir(vendorDir) // Ensure vendor directory exists const physxIdlSrc = path.join(rootDir, 'src/core/physx-js-webidl.js') - const physxIdlDest = path.join(rootDir, 'build/physx-js-webidl.js') + const physxIdlDest = path.join(vendorDir, 'physx-js-webidl.js') // Changed destination await fs.copy(physxIdlSrc, physxIdlDest) // copy over physx wasm const physxWasmSrc = path.join(rootDir, 'src/core/physx-js-webidl.wasm') - const physxWasmDest = path.join(rootDir, 'build/physx-js-webidl.wasm') + const physxWasmDest = path.join(vendorDir, 'physx-js-webidl.wasm') // Changed destination await fs.copy(physxWasmSrc, physxWasmDest) + + // Generate package.json for the NPM package + const packageJson = { + name: rootPackageJson.name || 'hyperfy', // fallback if not in root + version: rootPackageJson.version || '0.0.0', // fallback + type: 'module', + main: 'index.js', + types: 'index.d.ts', + license: rootPackageJson.license || 'UNLICENSED', // fallback + dependencies: { + ses: rootPackageJson.dependencies?.ses || 'latest', + eventemitter3: rootPackageJson.dependencies?.eventemitter3 || 'latest', + three: rootPackageJson.dependencies?.three || 'latest', + 'lodash-es': rootPackageJson.dependencies?.['lodash-es'] || 'latest', + // msgpackr was removed as it wasn't found in direct imports of the client bundle + }, + } + } + await fs.writeJson(path.join(npmPackageDir, 'package.json'), packageJson, { spaces: 2 }) + + } + await fs.writeJson(path.join(npmPackageDir, 'package.json'), packageJson, { spaces: 2 }) + + // Generate index.d.ts using dts-bundle-generator with a tsconfig file + try { + const tsconfigPath = path.join(rootDir, 'tsconfig.dts.json'); // Path to the new tsconfig + const inputFile = path.join(rootDir, 'src/node-client/index.js'); // Entry point for dts-bundle-generator + const outputFile = path.join(npmPackageDir, 'index.d.ts'); + + // Ensure tsconfig.dts.json exists (created in previous step) + if (!fs.existsSync(tsconfigPath)) { + throw new Error(`tsconfig.dts.json not found at ${tsconfigPath}. Please create it.`); + } + + // The input file for dts-bundle-generator should be relative to the CWD (rootDir) or absolute. + // The output file path should also be correctly specified. + execSync(`npx dts-bundle-generator --project "${tsconfigPath}" -o "${outputFile}" "${inputFile}"`, { + stdio: 'inherit', // Show output from the command + cwd: rootDir, // Run from project root to ensure paths in tsconfig are resolved correctly + }); + console.log('index.d.ts generated successfully using dts-bundle-generator and tsconfig.dts.json.'); + } catch (error) { + console.error('Error generating index.d.ts with dts-bundle-generator:', error.message); + if (error.stdout) console.error('stdout:', error.stdout.toString()); + if (error.stderr) console.error('stderr:', error.stderr.toString()); + console.warn('Falling back to a more structured (but still potentially incomplete) manual index.d.ts generation.'); + const fallbackDtsContent = `// Fallback index.d.ts - dts-bundle-generator failed. +// This is a manually structured declaration file. It may need to be updated if the API changes. +// For best results, ensure dts-bundle-generator can run successfully. +// Check build logs for errors from dts-bundle-generator and address them. + +// Assuming the package name will be 'hyperfy' or similar when used as a module. +// Adjust module name if necessary based on actual usage. +declare module '${rootPackageJson.name || 'hyperfy'}' { + + // Basic placeholder types - Ideally, these would be imported or defined by 'three' + // If 'three' types are correctly picked up by the consumer's TypeScript, these might not be needed here. + export interface Vector3 { + x: number; y: number; z: number; + set(x: number, y: number, z: number): this; + fromArray(array: number[], offset?: number): this; + // Add other common Vector3 methods/properties if needed + } + export interface Quaternion { + x: number; y: number; z: number; w: number; + set(x: number, y: number, z: number, w: number): this; + fromArray(array: number[], offset?: number): this; + // Add other common Quaternion methods/properties if needed + } + export interface Euler { + x: number; y: number; z: number; order: string; + setFromQuaternion(q: Quaternion, order?: string, update?: boolean): this; + // Add other common Euler methods/properties if needed + } + export interface Matrix4 { + // Add common Matrix4 methods/properties if needed + } + + // Forward declaration for World if needed by System or Node constructor signatures + export class World {} + + export class Node { + id: string; + name: string; + parent: Node | null; + children: Node[]; + position: Vector3; + quaternion: Quaternion; + rotation: Euler; + scale: Vector3; + matrix: Matrix4; + matrixWorld: Matrix4; + active: boolean; + + constructor(data?: Record); // Loosen data type for fallback + + add(node: Node): this; + remove(node: Node): this; + get(id: string): Node | null; + traverse(callback: (node: Node) => void): void; + // Minimal common methods based on src/core/nodes/Node.js + // Consider adding: clone, copy, getWorldPosition, updateTransform, clean, etc. + // JSDoc from source files should ideally provide these details for dts-bundle-generator. + } + + export class System { + world: World; + constructor(world: World); + + init?(options?: any): Promise | void; + start?(): void; + preTick?(): void; + // Add other lifecycle methods: preFixedUpdate, fixedUpdate, postFixedUpdate, preUpdate, update, postUpdate, lateUpdate, postLateUpdate, commit, postTick, destroy + } + + export interface Storage { + get(key: string, defaultValue?: any): any; + set(key: string, value: any): void; + remove(key: string): void; + } + export const storage: Storage; + + export class World extends EventEmitter { // Assuming World extends EventEmitter based on src/core/World.js + // Systems - these would ideally have their own class types if complex + settings: System; // Replace 'System' with actual 'SettingsSystem' type if defined + collections: System; + apps: System; + anchors: System; + events: System; + scripts: System; + chat: System; + blueprints: System; + entities: System; + physics: System; + stage: System; + // Add other systems if present (e.g., from createNodeClientWorld) + client?: System; + controls?: System; + network?: System; + loader?: System; + environment?: System; + + constructor(); + + register(key: string, systemClass: typeof System): System; // systemClass should be newable + init(options: { storage: Storage; assetsDir?: string; /* other options */ }): Promise; + start(): void; + tick(time: number): void; + // Add other public methods from World.js, e.g., resolveURL, setHot, get, etc. + // JSDoc from source files should ideally provide these details for dts-bundle-generator. + } + + // Assuming EventEmitter is a known type, e.g., from 'eventemitter3' + // If not, a basic EventEmitter type would need to be declared here. + // For simplicity in fallback, it's omitted but acknowledged. + // import { EventEmitter } from 'eventemitter3'; // This would be ideal if 'eventemitter3' types are available + + export function createNodeClientWorld(options?: any): Promise; // Refine options type +} +`; + await fs.writeFile(path.join(npmPackageDir, 'index.d.ts'), fallbackDtsContent); + } + // start the server or stop here if (dev) { // (re)start server spawn?.kill('SIGTERM') - spawn = fork(path.join(rootDir, 'build/world-node-client.js')) + // Ensure the fork path points to the new output location if dev mode needs to run the packaged output + spawn = fork(path.join(npmPackageDir, 'index.js')) } else { + console.log('NPM package built successfully to', npmPackageDir) process.exit(0) } }) @@ -65,5 +244,6 @@ let spawn await nodeClientCtx.watch() } else { await nodeClientCtx.rebuild() + // nodeClientCtx.dispose() // Dispose context after build for non-watch mode } } diff --git a/src/node-client/index.js b/src/node-client/index.js index 7d9b7a09..b9f09150 100644 --- a/src/node-client/index.js +++ b/src/node-client/index.js @@ -9,3 +9,5 @@ globalThis.__dirname = path.dirname(fileURLToPath(import.meta.url)) export { createNodeClientWorld } from '../core/createNodeClientWorld' export { System } from '../core/systems/System' export { storage } from '../core/storage' +export { World } from '../core/World.js' +export { Node } from '../core/nodes/Node.js' diff --git a/tsconfig.dts.json b/tsconfig.dts.json new file mode 100644 index 00000000..4e4fe12b --- /dev/null +++ b/tsconfig.dts.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "esModuleInterop": true, + "moduleResolution": "bundler", // "bundler" or "node" are good choices. "bundler" is more modern. + "module": "esnext", + "target": "esnext", + "skipLibCheck": true, // Setting to true to be more robust against potential issues in dependencies' types + "jsx": "react", // Keep if any JSX is present, otherwise can be removed + "lib": ["esnext", "dom"], + "baseUrl": "./", // Set baseUrl to project root for simpler path definitions if needed + "paths": { + // This assumes that imports like '../core/extras/three' or './extras/three' + // will be resolved correctly by node resolution, and that 'src/core/extras/three.js' + // itself correctly exports types from 'three' (e.g., via JSDoc or re-exporting). + // If 'three' types are not found, specific paths might be needed, e.g.: + // "three": ["node_modules/three/src/Three.d.ts"] + }, + "resolveJsonModule": true, + "strict": false, // Be more lenient for .d.ts generation from JS source + "typeRoots": ["./node_modules/@types", "./node_modules/three/src"] // Ensure three's types are discoverable + }, + "files": [ + "src/node-client/index.js" // Entry file relative to this tsconfig.json (project root) + ], + "include": [ + "src/**/*.js" // Broaden include to help tsc resolve all necessary files from src + ], + "exclude": [ + "node_modules", + "dist", + "build", // Excluding other build directories + "scripts" // Excluding scripts + ] +} From 9db0b84ce37bd700f91fc183ff62e46055963e32 Mon Sep 17 00:00:00 2001 From: Shaw Date: Wed, 4 Jun 2025 17:59:48 -0700 Subject: [PATCH 05/11] config npm and add types --- package-lock.json | 233 +++++++++++- package.json | 4 +- scripts/build-node-client.mjs | 546 +++++++++++++++++------------ src/client/utils.js | 8 +- src/core/systems/ClientControls.js | 2 +- src/core/systems/ClientPrefs.js | 2 +- src/core/systems/XR.js | 15 +- src/node-client/index.js | 20 ++ tsconfig.dts.json | 53 ++- 9 files changed, 625 insertions(+), 258 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c0f7bb4..030909b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,12 +47,14 @@ "devDependencies": { "@babel/eslint-parser": "^7.23.10", "@babel/preset-react": "^7.23.10", + "dts-bundle-generator": "^9.0.0", "esbuild": "^0.24.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-react": "^7.34.0", "eslint-plugin-react-hooks": "^4.6.0", - "prettier": "^3.4.2" + "prettier": "^3.4.2", + "typescript": "^5.0.0" }, "engines": { "node": "22.11.0", @@ -2271,6 +2273,100 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2978,6 +3074,23 @@ "node": ">= 12.0.0" } }, + "node_modules/dts-bundle-generator": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-9.5.1.tgz", + "integrity": "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "typescript": ">=5.0.2", + "yargs": "^17.6.0" + }, + "bin": { + "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4103,6 +4216,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", @@ -6302,6 +6425,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7431,6 +7564,20 @@ "rxjs": "*" } }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -7782,6 +7929,16 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -7790,6 +7947,80 @@ "license": "ISC", "peer": true }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", diff --git a/package.json b/package.json index 3dddc092..d3356032 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,10 @@ "lint:fix": "eslint . --ext .js,.jsx --fix", "format": "prettier --write .", "check": "npm run lint && npm run format", - "publish:node": "node scripts/build-node-client.mjs && cd dist/npm && npm publish" + "publish:node": "node scripts/build-node-client.mjs && npm publish" }, "files": [ - "dist/npm/", + "build/", "README.md", "LICENSE" ], diff --git a/scripts/build-node-client.mjs b/scripts/build-node-client.mjs index 559a21b5..49b4f33b 100644 --- a/scripts/build-node-client.mjs +++ b/scripts/build-node-client.mjs @@ -10,10 +10,10 @@ const dev = process.argv.includes('--dev') const dirname = path.dirname(fileURLToPath(import.meta.url)) const rootDir = path.join(dirname, '../') const buildDir = path.join(rootDir, 'build') // This can remain for other potential build outputs or be removed if not used elsewhere -const npmPackageDir = path.join(rootDir, 'dist/npm') +const npmPackageDir = buildDir // await fs.emptyDir(buildDir) // Keep if buildDir is still used for other things -await fs.emptyDir(npmPackageDir) // Ensure the new package directory is clean +// await fs.emptyDir(npmPackageDir) // Ensure the new package directory is clean /** * Build Node Client @@ -25,225 +25,341 @@ await fs.emptyDir(npmPackageDir) // Ensure the new package directory is clean let spawn -{ - // Read root package.json for details - const rootPackageJson = await fs.readJson(path.join(rootDir, 'package.json')) - - const nodeClientCtx = await esbuild.context({ - entryPoints: ['src/node-client/index.js'], - outfile: path.join(npmPackageDir, 'index.js'), // Changed output path - platform: 'node', - format: 'esm', - bundle: true, - treeShaking: true, - minify: false, - sourcemap: true, - packages: 'external', - loader: {}, - plugins: [ - { - name: 'server-finalize-plugin', - setup(build) { - build.onEnd(async result => { - if (result.errors.length > 0) { - console.error('Build failed with errors:', result.errors) - if (!dev) process.exit(1) // Exit if not in dev mode - return - } - - // copy over physx js - // copy over physx js and wasm to a vendor subdirectory - const vendorDir = path.join(npmPackageDir, 'vendor') - await fs.ensureDir(vendorDir) // Ensure vendor directory exists - const physxIdlSrc = path.join(rootDir, 'src/core/physx-js-webidl.js') - const physxIdlDest = path.join(vendorDir, 'physx-js-webidl.js') // Changed destination - await fs.copy(physxIdlSrc, physxIdlDest) - // copy over physx wasm - const physxWasmSrc = path.join(rootDir, 'src/core/physx-js-webidl.wasm') - const physxWasmDest = path.join(vendorDir, 'physx-js-webidl.wasm') // Changed destination - await fs.copy(physxWasmSrc, physxWasmDest) - - // Generate package.json for the NPM package - const packageJson = { - name: rootPackageJson.name || 'hyperfy', // fallback if not in root - version: rootPackageJson.version || '0.0.0', // fallback - type: 'module', - main: 'index.js', - types: 'index.d.ts', - license: rootPackageJson.license || 'UNLICENSED', // fallback - dependencies: { - ses: rootPackageJson.dependencies?.ses || 'latest', - eventemitter3: rootPackageJson.dependencies?.eventemitter3 || 'latest', - three: rootPackageJson.dependencies?.three || 'latest', - 'lodash-es': rootPackageJson.dependencies?.['lodash-es'] || 'latest', - // msgpackr was removed as it wasn't found in direct imports of the client bundle - }, - } - } - await fs.writeJson(path.join(npmPackageDir, 'package.json'), packageJson, { spaces: 2 }) - - } - await fs.writeJson(path.join(npmPackageDir, 'package.json'), packageJson, { spaces: 2 }) - - // Generate index.d.ts using dts-bundle-generator with a tsconfig file - try { - const tsconfigPath = path.join(rootDir, 'tsconfig.dts.json'); // Path to the new tsconfig - const inputFile = path.join(rootDir, 'src/node-client/index.js'); // Entry point for dts-bundle-generator - const outputFile = path.join(npmPackageDir, 'index.d.ts'); - - // Ensure tsconfig.dts.json exists (created in previous step) - if (!fs.existsSync(tsconfigPath)) { - throw new Error(`tsconfig.dts.json not found at ${tsconfigPath}. Please create it.`); +async function buildNodeClient() { + try { + // Ensure the NPM package directory is clean and exists + await fs.emptyDir(npmPackageDir) + await fs.ensureDir(npmPackageDir) + + console.log(`Building Node.js client for ${dev ? 'development' : 'production'}...`) + + // Read root package.json for details like version, license, dependencies + const rootPackageJson = await fs.readJson(path.join(rootDir, 'package.json')) + + const nodeClientCtx = await esbuild.context({ + entryPoints: [path.join(rootDir, 'src/node-client/index.js')], + outfile: path.join(npmPackageDir, 'index.js'), + platform: 'node', + format: 'esm', + bundle: true, + treeShaking: true, + minify: !dev, + sourcemap: dev ? 'inline' : true, + packages: 'external', // Externalize dependencies to be handled by package.json + // loader: {}, // Add if specific loaders are needed + // plugins: [polyfillNode()], // Consider if specific Node polyfills are absolutely required, but 'external' handles most cases + plugins: [ + { + name: 'node-client-finalize-plugin', + setup(build) { + build.onEnd(async result => { + if (result.errors.length > 0) { + console.error('Build failed with errors:', result.errors) + if (!dev) process.exit(1) + return } - - // The input file for dts-bundle-generator should be relative to the CWD (rootDir) or absolute. - // The output file path should also be correctly specified. - execSync(`npx dts-bundle-generator --project "${tsconfigPath}" -o "${outputFile}" "${inputFile}"`, { - stdio: 'inherit', // Show output from the command - cwd: rootDir, // Run from project root to ensure paths in tsconfig are resolved correctly - }); - console.log('index.d.ts generated successfully using dts-bundle-generator and tsconfig.dts.json.'); - } catch (error) { - console.error('Error generating index.d.ts with dts-bundle-generator:', error.message); - if (error.stdout) console.error('stdout:', error.stdout.toString()); - if (error.stderr) console.error('stderr:', error.stderr.toString()); - console.warn('Falling back to a more structured (but still potentially incomplete) manual index.d.ts generation.'); - const fallbackDtsContent = `// Fallback index.d.ts - dts-bundle-generator failed. -// This is a manually structured declaration file. It may need to be updated if the API changes. -// For best results, ensure dts-bundle-generator can run successfully. -// Check build logs for errors from dts-bundle-generator and address them. - -// Assuming the package name will be 'hyperfy' or similar when used as a module. -// Adjust module name if necessary based on actual usage. -declare module '${rootPackageJson.name || 'hyperfy'}' { - - // Basic placeholder types - Ideally, these would be imported or defined by 'three' - // If 'three' types are correctly picked up by the consumer's TypeScript, these might not be needed here. - export interface Vector3 { - x: number; y: number; z: number; - set(x: number, y: number, z: number): this; - fromArray(array: number[], offset?: number): this; - // Add other common Vector3 methods/properties if needed - } - export interface Quaternion { - x: number; y: number; z: number; w: number; - set(x: number, y: number, z: number, w: number): this; - fromArray(array: number[], offset?: number): this; - // Add other common Quaternion methods/properties if needed - } - export interface Euler { - x: number; y: number; z: number; order: string; - setFromQuaternion(q: Quaternion, order?: string, update?: boolean): this; - // Add other common Euler methods/properties if needed - } - export interface Matrix4 { - // Add common Matrix4 methods/properties if needed - } - // Forward declaration for World if needed by System or Node constructor signatures - export class World {} - - export class Node { - id: string; - name: string; - parent: Node | null; - children: Node[]; - position: Vector3; - quaternion: Quaternion; - rotation: Euler; - scale: Vector3; - matrix: Matrix4; - matrixWorld: Matrix4; - active: boolean; - - constructor(data?: Record); // Loosen data type for fallback - - add(node: Node): this; - remove(node: Node): this; - get(id: string): Node | null; - traverse(callback: (node: Node) => void): void; - // Minimal common methods based on src/core/nodes/Node.js - // Consider adding: clone, copy, getWorldPosition, updateTransform, clean, etc. - // JSDoc from source files should ideally provide these details for dts-bundle-generator. - } + console.log('Build successful. Finalizing package...') - export class System { - world: World; - constructor(world: World); + // 1. Copy PhysX assets + const vendorDir = path.join(npmPackageDir, 'vendor') + await fs.ensureDir(vendorDir) + const physxFiles = ['physx-js-webidl.js', 'physx-js-webidl.wasm'] + for (const file of physxFiles) { + const src = path.join(rootDir, 'src/core', file) + const dest = path.join(vendorDir, file) + if (await fs.pathExists(src)) { + await fs.copy(src, dest) + console.log(`Copied ${file} to ${dest}`) + } else { + console.warn(`PhysX asset ${src} not found. Skipping.`) + } + } - init?(options?: any): Promise | void; - start?(): void; - preTick?(): void; - // Add other lifecycle methods: preFixedUpdate, fixedUpdate, postFixedUpdate, preUpdate, update, postUpdate, lateUpdate, postLateUpdate, commit, postTick, destroy - } - - export interface Storage { - get(key: string, defaultValue?: any): any; - set(key: string, value: any): void; - remove(key: string): void; - } - export const storage: Storage; - - export class World extends EventEmitter { // Assuming World extends EventEmitter based on src/core/World.js - // Systems - these would ideally have their own class types if complex - settings: System; // Replace 'System' with actual 'SettingsSystem' type if defined - collections: System; - apps: System; - anchors: System; - events: System; - scripts: System; - chat: System; - blueprints: System; - entities: System; - physics: System; - stage: System; - // Add other systems if present (e.g., from createNodeClientWorld) - client?: System; - controls?: System; - network?: System; - loader?: System; - environment?: System; - - constructor(); - - register(key: string, systemClass: typeof System): System; // systemClass should be newable - init(options: { storage: Storage; assetsDir?: string; /* other options */ }): Promise; - start(): void; - tick(time: number): void; - // Add other public methods from World.js, e.g., resolveURL, setHot, get, etc. - // JSDoc from source files should ideally provide these details for dts-bundle-generator. - } - - // Assuming EventEmitter is a known type, e.g., from 'eventemitter3' - // If not, a basic EventEmitter type would need to be declared here. - // For simplicity in fallback, it's omitted but acknowledged. - // import { EventEmitter } from 'eventemitter3'; // This would be ideal if 'eventemitter3' types are available + // 2. Generate package.json for the NPM package + const packageJson = { + name: rootPackageJson.name || 'hyperfy', // Or your chosen scoped package name + version: rootPackageJson.version, // Must be synced + description: rootPackageJson.description || 'Node.js client for Hyperfy virtual worlds', + main: 'index.js', + types: 'index.d.ts', + type: 'module', + files: [ + 'index.js', + 'index.d.ts', + 'vendor/', + 'README.md', + 'LICENSE', + ], + // Dependencies that are truly bundled or very core could be here + // For 'three' and 'eventemitter3', peerDependencies are better. + dependencies: { + // 'ses': rootPackageJson.dependencies?.ses, // If needed and not externalized correctly + }, + peerDependencies: { + 'three': rootPackageJson.dependencies?.three || '>=0.173.0 <0.174.0', + 'eventemitter3': rootPackageJson.dependencies?.eventemitter3 || '^5.0.0', + 'lodash-es': rootPackageJson.dependencies?.['lodash-es'] || '^4.17.0', // if used by the client bundle directly + }, + peerDependenciesMeta: { + 'three': { optional: false }, + 'eventemitter3': { optional: false }, + 'lodash-es': { optional: true }, // Make optional if not strictly required + }, + engines: { + node: rootPackageJson.engines?.node || '>=18.0.0', + }, + homepage: rootPackageJson.homepage, + repository: { + ...rootPackageJson.repository, + directory: 'build', // Point to the package subdirectory + }, + bugs: rootPackageJson.bugs, + keywords: rootPackageJson.keywords, + license: rootPackageJson.license, + } + await fs.writeJson(path.join(npmPackageDir, 'package.json'), packageJson, { spaces: 2 }) + console.log('Generated package.json in', npmPackageDir) + + // 3. Copy README.md and LICENSE + const rootFilesToCopy = ['README.md', 'LICENSE'] + for (const file of rootFilesToCopy) { + const src = path.join(rootDir, file) + const dest = path.join(npmPackageDir, file) + if (await fs.pathExists(src)) { + await fs.copy(src, dest) + console.log(`Copied ${file} to ${npmPackageDir}`) + } else { + console.warn(`Root file ${src} not found. Skipping.`) + } + } + + // 4. Generate index.d.ts + const tsconfigPath = path.join(rootDir, 'tsconfig.dts.json') + const inputFileForDts = path.join(rootDir, 'src/node-client/index.js') // Relative to CWD for the command + const outputFileDts = path.join(npmPackageDir, 'index.d.ts') + + try { + if (!fs.existsSync(tsconfigPath)) { + throw new Error(`tsconfig.dts.json not found at ${tsconfigPath}. Please create it or ensure it's correctly named.`) + } + console.log(`Attempting to generate index.d.ts using dts-bundle-generator with tsconfig: ${tsconfigPath}`) + execSync(`npx dts-bundle-generator --project "${tsconfigPath}" -o "${outputFileDts}" "${inputFileForDts}"`, { + stdio: 'inherit', + cwd: rootDir, // Important: run from project root + }) + console.log('index.d.ts generated successfully using dts-bundle-generator.') + } catch (error) { + console.error('Error generating index.d.ts with dts-bundle-generator:', error.message) + // console.error('stdout:', error.stdout?.toString()) // dts-bundle-generator might not populate these well on error + // console.error('stderr:', error.stderr?.toString()) + console.warn('Falling back to manually specified index.d.ts content.') + + // --- Start of comprehensive fallback index.d.ts --- + const fallbackDtsContent = ` +// Type definitions for hyperfy Node.js client (fallback) +// Project: ${rootPackageJson.homepage || 'https://github.com/hyperfy-xyz/hyperfy'} +// Definitions by: Hyperfy Team (Update with actual author) + +import { Vector3, Quaternion, Euler, Matrix4, Object3D, PerspectiveCamera } from 'three' +import OriginalEventEmitter from 'eventemitter3' - export function createNodeClientWorld(options?: any): Promise; // Refine options type +export interface IStorage { + get(key: string, defaultValue?: T): T | null + set(key: string, value: T): void + remove(key: string): void } -`; - await fs.writeFile(path.join(npmPackageDir, 'index.d.ts'), fallbackDtsContent); - } - - // start the server or stop here - if (dev) { - // (re)start server - spawn?.kill('SIGTERM') - // Ensure the fork path points to the new output location if dev mode needs to run the packaged output - spawn = fork(path.join(npmPackageDir, 'index.js')) - } else { - console.log('NPM package built successfully to', npmPackageDir) - process.exit(0) - } - }) + +export interface PointerEvent { [key: string]: any } + +export interface NodeData { + id?: string + name?: string + active?: boolean + position?: [number, number, number] | Vector3 + quaternion?: [number, number, number, number] | Quaternion + scale?: [number, number, number] | Vector3 + onPointerEnter?: (event: PointerEvent) => void + onPointerLeave?: (event: PointerEvent) => void + onPointerDown?: (event: PointerEvent) => void + onPointerUp?: (event: PointerEvent) => void + cursor?: string + [key: string]: any +} + +export interface WorldInitOptions { + storage?: IStorage + assetsDir?: string + assetsUrl?: string +} + +export declare class System extends OriginalEventEmitter { + constructor(world: World) + world: World + init?(options?: WorldInitOptions): Promise | void + start?(): void + preTick?(): void + preFixedUpdate?(willFixedStep?: boolean): void + fixedUpdate?(delta: number): void + postFixedUpdate?(delta?: number): void + preUpdate?(alpha?: number): void + update?(delta: number, alpha?: number): void + postUpdate?(delta?: number): void + lateUpdate?(delta?: number, alpha?: number): void + postLateUpdate?(delta?: number): void + commit?(): void + postTick?(): void + destroy?(): void +} + +export declare class Node { + constructor(data?: NodeData) + id: string + name: string + parent: Node | null + children: Node[] + ctx: any + position: Vector3 + quaternion: Quaternion + rotation: Euler + scale: Vector3 + matrix: Matrix4 + matrixWorld: Matrix4 + mounted: boolean + isDirty: boolean + isTransformed: boolean + protected _active: boolean + get active(): boolean + set active(value: boolean) + onPointerEnter?: (event: PointerEvent) => void + onPointerLeave?: (event: PointerEvent) => void + onPointerDown?: (event: PointerEvent) => void + onPointerUp?: (event: PointerEvent) => void + cursor?: string + activate(ctx?: any): void + deactivate(): void + add(node: Node): this + remove(node: Node): this + setTransformed(): void + setDirty(): void + clean(): void + mount(): void + commit(didTransform: boolean): void + unmount(): void + updateTransform(): void + traverse(callback: (node: Node) => void): void + clone(recursive?: boolean): this + copy(source: this, recursive?: boolean): this + get(id: string): Node | null + getWorldPosition(target?: Vector3): Vector3 + getWorldMatrix(target?: Matrix4): Matrix4 + getStats(recursive?: boolean, stats?: any): any + applyStats(stats: any): void + getProxy(): any +} + +export declare class World extends OriginalEventEmitter { + constructor() + maxDeltaTime: number + fixedDeltaTime: number + frame: number + time: number + accumulator: number + systems: System[] + networkRate: number + assetsUrl: string | null + assetsDir: string | null + hot: Set + rig: Object3D + camera: PerspectiveCamera + storage?: IStorage + settings: System + collections: System + apps: System + anchors: System + events: System + scripts: System + chat: System + blueprints: System + entities: System + physics: System + stage: System + client: NodeClient + controls: ClientControls + network: ClientNetwork + loader: ServerLoader + environment: NodeEnvironment + register(key: string, systemClass: new (world: World) => T): T + init(options: WorldInitOptions): Promise + start(): void + tick(time: number): void + preTick(): void + preFixedUpdate(willFixedStep: boolean): void + fixedUpdate(delta: number): void + postFixedUpdate(delta: number): void + preUpdate(alpha: number): void + update(delta: number, alpha?: number): void + postUpdate(delta: number): void + lateUpdate(delta: number, alpha?: number): void + postLateUpdate(delta: number): void + commit(): void + postTick(): void + setupMaterial?(material: any): void + setHot(item: any, hot: boolean): void + resolveURL(url: string, allowLocal?: boolean): string + inject?(runtime: any): void + destroy(): void +} + +export declare class NodeClient extends System { /* Actual API for NodeClient */ } +export declare class ClientControls extends System { isMac: boolean; /* Actual API for ClientControls */ } +export declare class ClientNetwork extends System { /* Actual API for ClientNetwork */ } +export declare class ServerLoader extends System { /* Actual API for ServerLoader */ } +export declare class NodeEnvironment extends System { /* Actual API for NodeEnvironment */ } + +// Define specific types for default systems if available, e.g.: +// export declare class SettingsSystem extends System { /* ... */ } +// export declare class EntitiesSystem extends System { /* ... */ } + +export declare function createNodeClientWorld(): World +export declare const storage: IStorage +export declare function getPhysXAssetPath(assetName: string): string +` + // --- End of comprehensive fallback index.d.ts --- + await fs.writeFile(outputFileDts, fallbackDtsContent.trim()) + console.log('Written fallback index.d.ts to', outputFileDts) + } + + if (dev) { + spawn?.kill('SIGTERM') + spawn = fork(path.join(npmPackageDir, 'index.js')) // Runs the newly built package entry point + console.log('Development server (re)started with new build.') + } else { + console.log('NPM package built successfully to:', npmPackageDir) + // For non-dev builds, we let the calling script (e.g., publish:node) handle process exit or next steps. + } + }) + }, }, - }, - ], - }) - if (dev) { - await nodeClientCtx.watch() - } else { - await nodeClientCtx.rebuild() - // nodeClientCtx.dispose() // Dispose context after build for non-watch mode + ], + }) + + if (dev) { + await nodeClientCtx.watch() + console.log('Watching for changes...') + } else { + await nodeClientCtx.rebuild() + await nodeClientCtx.dispose() // Dispose context after build for non-watch mode + console.log('Build complete.') + } + } catch (error) { + console.error('Unhandled error during build process:', error) + if (!dev) process.exit(1) } } + +// Execute the build +buildNodeClient().catch(err => { + console.error('Failed to execute buildNodeClient:', err) + if (!dev) process.exit(1) +}) diff --git a/src/client/utils.js b/src/client/utils.js index 520b99ef..94a90688 100644 --- a/src/client/utils.js +++ b/src/client/utils.js @@ -16,7 +16,7 @@ export function cls(...args) { // export const isTouch = !!navigator.userAgent.match(/OculusBrowser|iPhone|iPad|iPod|Android/i) // if at least two indicators point to touch, consider it primarily touch-based: -const coarse = window.matchMedia('(pointer: coarse)').matches -const noHover = window.matchMedia('(hover: none)').matches -const hasTouch = navigator.maxTouchPoints > 0 -export const isTouch = (coarse && hasTouch) || (noHover && hasTouch) +const coarse = typeof window !== 'undefined' && typeof window.matchMedia === 'function' ? window.matchMedia('(pointer: coarse)').matches : false; +const noHover = typeof window !== 'undefined' && typeof window.matchMedia === 'function' ? window.matchMedia('(hover: none)').matches : false; +const hasTouch = typeof navigator !== 'undefined' && typeof navigator.maxTouchPoints === 'number' ? navigator.maxTouchPoints > 0 : false; +export const isTouch = (coarse && hasTouch) || (noHover && hasTouch); diff --git a/src/core/systems/ClientControls.js b/src/core/systems/ClientControls.js index 424e96ab..2ff13e06 100644 --- a/src/core/systems/ClientControls.js +++ b/src/core/systems/ClientControls.js @@ -50,7 +50,7 @@ export class ClientControls extends System { this.actions = [] this.buttonsDown = new Set() this.isUserGesture = false - this.isMac = /Mac/.test(navigator.platform) + this.isMac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; this.pointer = { locked: false, shouldLock: false, diff --git a/src/core/systems/ClientPrefs.js b/src/core/systems/ClientPrefs.js index b0de689f..1438a047 100644 --- a/src/core/systems/ClientPrefs.js +++ b/src/core/systems/ClientPrefs.js @@ -12,7 +12,7 @@ export class ClientPrefs extends System { constructor(world) { super(world) - const isQuest = /OculusBrowser/.test(navigator.userAgent) + const isQuest = typeof navigator !== 'undefined' && navigator.userAgent ? /OculusBrowser/.test(navigator.userAgent) : false; const data = storage.get('prefs', {}) diff --git a/src/core/systems/XR.js b/src/core/systems/XR.js index 51f11f7c..4161eda5 100644 --- a/src/core/systems/XR.js +++ b/src/core/systems/XR.js @@ -27,12 +27,21 @@ export class XR extends System { } async init() { - this.supportsVR = await navigator.xr?.isSessionSupported('immersive-vr') - this.supportsAR = await navigator.xr?.isSessionSupported('immersive-ar') + if (typeof navigator !== 'undefined' && navigator.xr) { + this.supportsVR = await navigator.xr.isSessionSupported?.('immersive-vr') + this.supportsAR = await navigator.xr.isSessionSupported?.('immersive-ar') + } else { + this.supportsVR = false + this.supportsAR = false + } } async enter() { - const session = await navigator.xr?.requestSession('immersive-vr', { + if (typeof navigator === 'undefined' || !navigator.xr) { + console.warn('XR.enter() called in an environment without navigator.xr support.') + return + } + const session = await navigator.xr.requestSession?.('immersive-vr', { requiredFeatures: ['local-floor'], }) try { diff --git a/src/node-client/index.js b/src/node-client/index.js index b9f09150..30875f8a 100644 --- a/src/node-client/index.js +++ b/src/node-client/index.js @@ -11,3 +11,23 @@ export { System } from '../core/systems/System' export { storage } from '../core/storage' export { World } from '../core/World.js' export { Node } from '../core/nodes/Node.js' +export { NodeClient } from '../core/systems/NodeClient.js' +export { ClientControls } from '../core/systems/ClientControls.js' +export { ClientNetwork } from '../core/systems/ClientNetwork.js' +export { ServerLoader } from '../core/systems/ServerLoader.js' +export { NodeEnvironment } from '../core/systems/NodeEnvironment.js' + +/** + * Returns the absolute path to a PhysX asset within the packaged 'vendor' directory. + * This assumes that the 'vendor' directory is at the root of the installed package. + * @param assetName The name of the PhysX asset (e.g., 'physx.wasm'). + */ +export function getPhysXAssetPath(assetName) { + // In ESM, __dirname is not available directly like in CJS. + // This constructs a path relative to the current module file. + // Assumes index.js is at the root of the dist/npm package. + // If index.js is nested, this path needs adjustment (e.g., path.join(__dirname, '../vendor', assetName)) + const currentModulePath = fileURLToPath(import.meta.url); + const packageRootPath = path.dirname(currentModulePath); + return path.join(packageRootPath, 'vendor', assetName); +} diff --git a/tsconfig.dts.json b/tsconfig.dts.json index 4e4fe12b..44f2e77b 100644 --- a/tsconfig.dts.json +++ b/tsconfig.dts.json @@ -1,37 +1,28 @@ { "compilerOptions": { - "allowJs": true, - "declaration": true, - "emitDeclarationOnly": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", "esModuleInterop": true, - "moduleResolution": "bundler", // "bundler" or "node" are good choices. "bundler" is more modern. - "module": "esnext", - "target": "esnext", - "skipLibCheck": true, // Setting to true to be more robust against potential issues in dependencies' types - "jsx": "react", // Keep if any JSX is present, otherwise can be removed - "lib": ["esnext", "dom"], - "baseUrl": "./", // Set baseUrl to project root for simpler path definitions if needed + "allowJs": true, + "checkJs": false, // Optional: set to true to type-check JS files (can be noisy) + "strict": false, // dts-bundle-generator might work better with strict mode off for JS projects + "noEmit": true, // We don't want tsc to emit .js files, only dts-bundle-generator uses this for type info + "baseUrl": "./", "paths": { - // This assumes that imports like '../core/extras/three' or './extras/three' - // will be resolved correctly by node resolution, and that 'src/core/extras/three.js' - // itself correctly exports types from 'three' (e.g., via JSDoc or re-exporting). - // If 'three' types are not found, specific paths might be needed, e.g.: - // "three": ["node_modules/three/src/Three.d.ts"] + "*": ["node_modules/*", "src/*"] }, - "resolveJsonModule": true, - "strict": false, // Be more lenient for .d.ts generation from JS source - "typeRoots": ["./node_modules/@types", "./node_modules/three/src"] // Ensure three's types are discoverable - }, - "files": [ - "src/node-client/index.js" // Entry file relative to this tsconfig.json (project root) - ], - "include": [ - "src/**/*.js" // Broaden include to help tsc resolve all necessary files from src - ], - "exclude": [ - "node_modules", - "dist", - "build", // Excluding other build directories - "scripts" // Excluding scripts - ] + // Include source files that dts-bundle-generator should analyze + // This should at least include the entry point and related core files. + // Be more specific if possible to speed up generation and reduce noise. + "include": [ + "src/node-client/**/*.js", + "src/core/**/*.js" + // Add other paths if necessary for types used by the node-client + ], + "exclude": [ + "node_modules", + "build", + ] + } } From 934aa515b81f57ff2cedc413cdbcd2b522d16135 Mon Sep 17 00:00:00 2001 From: Shaw Date: Wed, 4 Jun 2025 18:05:47 -0700 Subject: [PATCH 06/11] types --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d3356032..2f4b7a7c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "hyperfy", "version": "0.13.0", "type": "module", - "main": "index.js", + "main": "build/index.js", + "types": "build/index.d.ts", "homepage": "https://github.com/hyperfy-xyz/hyperfy#readme", "bugs": { "url": "https://github.com/hyperfy-xyz/hyperfy/issues" From 49b694846dfe7597aaad3848827c5548cb229a80 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Fri, 6 Jun 2025 11:48:23 +0800 Subject: [PATCH 07/11] build node client --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f4b7a7c..f93cb0b5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ ], "scripts": { "dev": "node scripts/build.mjs --dev", - "build": "node scripts/build.mjs", + "build": "node scripts/build.mjs && node scripts/build-node-client.mjs", "start": "node build/index.js", "world:clean": "node scripts/clean-world.mjs", "viewer:dev": "node scripts/build-viewer.mjs --dev", From 9cd0849f208b598fb4d2b9134439e813eb41fafa Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Fri, 6 Jun 2025 11:49:59 +0800 Subject: [PATCH 08/11] Remove vendor folder; load PhysX assets directly from build directory --- scripts/build-node-client.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/build-node-client.mjs b/scripts/build-node-client.mjs index 49b4f33b..c05a5b5a 100644 --- a/scripts/build-node-client.mjs +++ b/scripts/build-node-client.mjs @@ -61,13 +61,10 @@ async function buildNodeClient() { console.log('Build successful. Finalizing package...') - // 1. Copy PhysX assets - const vendorDir = path.join(npmPackageDir, 'vendor') - await fs.ensureDir(vendorDir) const physxFiles = ['physx-js-webidl.js', 'physx-js-webidl.wasm'] for (const file of physxFiles) { const src = path.join(rootDir, 'src/core', file) - const dest = path.join(vendorDir, file) + const dest = path.join(npmPackageDir, file) if (await fs.pathExists(src)) { await fs.copy(src, dest) console.log(`Copied ${file} to ${dest}`) From a1539c0d3c89a7a1d22a1039ed803d8cc4e54e0a Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Fri, 6 Jun 2025 20:52:41 +0800 Subject: [PATCH 09/11] revert --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f93cb0b5..2f4b7a7c 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ ], "scripts": { "dev": "node scripts/build.mjs --dev", - "build": "node scripts/build.mjs && node scripts/build-node-client.mjs", + "build": "node scripts/build.mjs", "start": "node build/index.js", "world:clean": "node scripts/clean-world.mjs", "viewer:dev": "node scripts/build-viewer.mjs --dev", From b2f1d0634db2257ebaa9b5c25e9577bc5ce4d592 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Fri, 6 Jun 2025 20:53:20 +0800 Subject: [PATCH 10/11] keep name --- scripts/build-node-client.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-node-client.mjs b/scripts/build-node-client.mjs index c05a5b5a..6b11a26f 100644 --- a/scripts/build-node-client.mjs +++ b/scripts/build-node-client.mjs @@ -44,6 +44,7 @@ async function buildNodeClient() { bundle: true, treeShaking: true, minify: !dev, + keepNames: true, sourcemap: dev ? 'inline' : true, packages: 'external', // Externalize dependencies to be handled by package.json // loader: {}, // Add if specific loaders are needed From ba87ca13f2d9c3eae60924c83d509cce221066f1 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Fri, 6 Jun 2025 21:00:16 +0800 Subject: [PATCH 11/11] export additional modules from node-client entry --- src/node-client/index.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/node-client/index.js b/src/node-client/index.js index 30875f8a..d7ee17ed 100644 --- a/src/node-client/index.js +++ b/src/node-client/index.js @@ -6,17 +6,33 @@ import { fileURLToPath } from 'url' // support `__dirname` in ESM globalThis.__dirname = path.dirname(fileURLToPath(import.meta.url)) + +export * as THREE from 'three'; + export { createNodeClientWorld } from '../core/createNodeClientWorld' -export { System } from '../core/systems/System' export { storage } from '../core/storage' export { World } from '../core/World.js' -export { Node } from '../core/nodes/Node.js' +export { loadPhysX } from '../core/loadPhysX.js' +export { uuid } from '../core/utils.js' + +export { System } from '../core/systems/System' export { NodeClient } from '../core/systems/NodeClient.js' export { ClientControls } from '../core/systems/ClientControls.js' export { ClientNetwork } from '../core/systems/ClientNetwork.js' export { ServerLoader } from '../core/systems/ServerLoader.js' export { NodeEnvironment } from '../core/systems/NodeEnvironment.js' +export { Node } from '../core/nodes/Node.js' + +export { Emotes } from '../core/extras/playerEmotes.js' +export { createEmoteFactory } from '../core/extras/createEmoteFactory.js' +export { createNode } from '../core/extras/createNode.js' +export { glbToNodes } from '../core/extras/glbToNodes.js' +export { Vector3Enhanced } from '../core/extras/Vector3Enhanced.js' + +export { GLTFLoader } from '../core/libs/gltfloader/GLTFLoader.js' +export { CSM } from '../core/libs/csm/CSM' + /** * Returns the absolute path to a PhysX asset within the packaged 'vendor' directory. * This assumes that the 'vendor' directory is at the root of the installed package.