UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

211 lines (183 loc) • 8.89 kB
import { addLog, LogType } from "./debug/debug_overlay.js"; import { addScriptToArrays, removeScriptFromContext } from "./engine_mainloop_utils.js" import type { IComponent } from "./engine_types.js"; import { TypeStore } from "./engine_typestore.js"; import { getParam } from "./engine_utils.js"; const debug = getParam("debughotreload"); declare type BeforeUpdateArgs = { type: string, updates: Array<{ path: string, timestamp: number, acceptedPath: string, explicitImportRequired: boolean, type: string }>, } //@ts-ignore if (import.meta.hot) { //@ts-ignore import.meta.hot.on('vite:beforeUpdate', (cb: BeforeUpdateArgs) => { if (debug) console.log(cb); for (const update of cb.updates) { console.log("[Needle Engine] Hot reloading " + update.path); } }); } let isApplyingChanges = false; const instances: Map<string, object[]> = new Map(); /** @internal true during hot reload, can be used to modify behaviour in onEnable and onDisable */ export function isHotReloading() { return isApplyingChanges; } export function isHotReloadEnabled() { return globalThis["NEEDLE_HOT_RELOAD_ENABLED"] === true; } /** @internal */ export function registerHotReloadType(instance: object) { if (isApplyingChanges) { if (debug) console.warn("[Needle Engine] Hot reloading is in progress, not registering instance", instance); return; } if(debug) console.log("[Needle Engine] Registering hot reload instance", instance); const type = instance.constructor; const name = type.name; if (!instances.has(name)) { instances.set(name, [instance]); } else { instances.get(name)?.push(instance); } } /** @internal */ export function unregisterHotReloadType(instance: object) { if (isApplyingChanges) { if (debug) console.warn("[Needle Engine] Hot reloading is in progress, not unregistering instance", instance); return; } if(debug) console.log("[Needle Engine] Unregistering hot reload instance", instance); const type = instance.constructor; const name = type.name; const instancesOfType = instances.get(name); if (!instancesOfType) return; const idx = instancesOfType.indexOf(instance); if (idx === -1) return; instancesOfType.splice(idx, 1); } let didRegisterUnhandledExceptionListener = false; function reloadPageOnHotReloadError() { if (debug) return; if (didRegisterUnhandledExceptionListener) return; didRegisterUnhandledExceptionListener = true; const error = console.error; console.error = (...args: any[]) => { if (args.length) { const arg: string = args[0]; // When making changes in e.g. the engine package and then making changes in project scripts again that import the engine package: hot reload fails and reports redefinitions of types, we just reload the page in those cases for now // editing a script in one package seems to work for now so it should be good enough for a start if (typeof arg === "string" && arg.includes("[hmr] Failed to reload ")) { console.log("[Needle Engine] Hot reloading failed") window.location.reload(); return; } } error.apply(console, args); }; } export function applyHMRChanges(newModule): boolean { if (debug) console.log("[HMR] Apply changes", newModule, Object.keys(newModule)); reloadPageOnHotReloadError(); // console.dir(newModule); for (const key of Object.keys(newModule)) { try { isApplyingChanges = true; const typeToUpdate = TypeStore.get(key); if (!typeToUpdate) { if (debug) console.log("[HMR] Type not found: " + key) continue; } const newType = newModule[key]; const instancesOfType = instances.get(newType.name); let hotReloadMessage = "[Needle Engine] Updating type: " + key; const typesCount = instancesOfType?.length ?? -1; if (typesCount > 0) hotReloadMessage += " x" + typesCount; else hotReloadMessage += " (No instances registered)"; console.log(hotReloadMessage); // Update prototype (methods and properties) const previousMethods = Object.getOwnPropertyNames(typeToUpdate.prototype); const methodsAndProperties = Object.getOwnPropertyDescriptors(newType.prototype); for (const typeKey in methodsAndProperties) { const desc = methodsAndProperties[typeKey]; if (!desc.writable) continue; typeToUpdate.prototype[typeKey] = newModule[key].prototype[typeKey]; } // Remove methods that are no longer present for (const typeKey of previousMethods) { if (!methodsAndProperties[typeKey]) { delete typeToUpdate.prototype[typeKey]; } } // Update fields (we only add new fields if they are undefined) // we create a instance to get access to the fields if (instancesOfType) { const newTypeInstance = new newType(); const keys = Object.getOwnPropertyDescriptors(newTypeInstance); for (const inst of instancesOfType) { const componentInstance = inst as unknown as IComponent; const isComponent = componentInstance.isComponent === true; const active = isComponent ? componentInstance.activeAndEnabled : true; const context = isComponent ? componentInstance.context : undefined; try { if (isComponent && context) { removeScriptFromContext(componentInstance, context); } if (isComponent && active) { componentInstance.enabled = false; } if (inst["onBeforeHotReloadFields"]) { const res = inst["onBeforeHotReloadFields"](); if (res === false) continue; } for (const key in keys) { const desc = keys[key]; if (!desc.writable) continue; if (inst[key] === undefined) { inst[key] = newTypeInstance[key]; } // if its a function but not on the prototype // then its a bound method that needs to be rebound else if (typeof inst[key] === "function" && !inst[key].prototype) { const boundMethod = inst[key]; // try to get the target method name const targetMethodName = boundMethod.name; const prefix = "bound "; // < magic prefix if (targetMethodName === prefix) continue; const name = boundMethod.name.substring(prefix.length); // if the target method name still exists on the new prototype // we want to rebind it and assign it to the field // Beware that this will not work if the method is added to some event listener etc const newTarget = newType.prototype[name]; if (newTarget) inst[key] = newTarget.bind(inst); } } if (inst["onAfterHotReloadFields"]) inst["onAfterHotReloadFields"](); } finally { if (isComponent && context) { addScriptToArrays(componentInstance, context); } if (isComponent && active) { componentInstance.enabled = true; } } } } } catch (err) { if (debug) console.error(err); // we only want to invalidate changes if we debug hot reload else return false; } finally { isApplyingChanges = false; addLog(LogType.Log, "Script changes applied (HMR)") } } return true; }