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.

238 lines (207 loc) • 9.02 kB
import { exec, execSync } from 'child_process'; import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs'; import path from 'path'; const prefix = "[needle-dependency-watcher] "; function log(...msg) { console.log(prefix, ...msg) } /** * @param {import('../types').userSettings} userSettings */ export const needleDependencyWatcher = (command, config, userSettings) => { if (command === "build") return; if (userSettings?.noDependencyWatcher === true) return; const dir = process.cwd(); const packageJsonPath = path.join(dir, "package.json"); const viteCacheDir = path.join(dir, "node_modules", ".vite"); return { name: 'needle-dependency-watcher', configureServer(server) { watchPackageJson(server, dir, packageJsonPath, viteCacheDir); manageClients(server); } } } const currentClients = new Set(); function manageClients(server) { server.ws.on("connection", (socket) => { currentClients.add(socket); socket.on("close", () => { currentClients.delete(socket); }); }); } async function triggerReloadOnClients() { log(`Triggering reload on ${currentClients.size} clients...`) for (const client of currentClients) { client.send(JSON.stringify({ type: "full-reload" })); } return new Promise((resolve) => { setTimeout(resolve, 100); }); } let packageJsonStat; let lastEditTime; let packageJsonSize; let packageJson; let requireInstall = false; function watchPackageJson(server, projectDir, packageJsonPath, cachePath) { if (!existsSync(packageJsonPath)) { return; } log("Watching project", packageJsonPath) lastRestartTime = 0; packageJsonStat = statSync(packageJsonPath); lastEditTime = packageJsonStat.mtime; packageJsonSize = packageJsonStat.size; packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); setTimeout(() => { requireInstall = testIfInstallIsRequired(projectDir, packageJson); }, 1000); setInterval(() => { packageJsonStat = statSync(packageJsonPath); let modified = false; if (packageJsonStat.mtime > lastEditTime) { modified = true; } if (packageJsonStat.size !== packageJsonSize) { modified = true; } if (modified || requireInstall) { let requireReload = false; if (!requireInstall) { requireInstall = testIfInstallIsRequired(projectDir, packageJson); } // test if dependencies changed let newPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); if (newPackageJson.dependencies) { for (const key in newPackageJson.dependencies) { if (packageJson.dependencies[key] !== newPackageJson.dependencies[key] && newPackageJson.dependencies[key] !== undefined) { log("Detected new dependency: " + key) requireReload = true; requireInstall = true; } } } if (packageJson.devDependencies) { for (const key in packageJson.devDependencies) { if (packageJson.devDependencies[key] !== newPackageJson.devDependencies[key] && newPackageJson.devDependencies[key] !== undefined) { log("Detected new devDependency: " + key) requireReload = true; requireInstall = true; } } } if (modified) { log("package.json has changed. Require install: " + (requireInstall ? "yes" : "no")) } packageJsonSize = packageJsonStat.size; lastEditTime = packageJsonStat.mtime; if (requireReload || requireInstall) { restart(server, projectDir, cachePath); } } }, 2000); } function testIfInstallIsRequired(projectDir, packageJson) { if (packageJson.dependencies) { for (const key in packageJson.dependencies) { // make sure the dependency is installed const depPath = path.join(projectDir, "node_modules", key); if (!existsSync(depPath)) { const val = packageJson.dependencies[key]; if (val.startsWith("file:")) { // check if the local path exists: const dirPath = val.substr(5); // check first the value in case it's a absolute path if (!existsSync(dirPath)) { // then check concatenated with the project dir in case it's a relative path const relPath = path.join(projectDir, val.substr(5)); if (!existsSync(relPath)) { // if neither directories exist then the local path is invalid and we ignore it continue; } } } log("Dependency not installed: " + key) return true; } else { let expectedVersion = ""; const value = packageJson.dependencies[key]; if (value.startsWith("file:")) { const packageJsonPath = path.join(projectDir, value.substr(5), "package.json"); if (existsSync(packageJsonPath)) { expectedVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version; } } else { expectedVersion = value; } if (expectedVersion?.length > 0) { const isRange = expectedVersion.includes("x") || expectedVersion.includes("^") || expectedVersion.includes(">") || expectedVersion.includes("<") || expectedVersion.includes("~"); const isTagName = expectedVersion === "stable" || expectedVersion === "latest" || expectedVersion === "next" || expectedVersion === "beta" || expectedVersion === "alpha" || expectedVersion === "canary" || expectedVersion === "experimental"; if (!isRange && !isTagName) { const packageJsonPath = path.join(depPath, "package.json"); /** @type {String} */ const installedVersion = JSON.parse(readFileSync(packageJsonPath, "utf8")).version; // fix check for cases where the version contains a alias e.g. npm:three@0.160.3 if (expectedVersion.trimEnd().endsWith(installedVersion.trim()) == false) { log(`Dependency ${key} is installed but version is not the right one. Expected \"${expectedVersion}/" but got \"${installedVersion}\"`) return true; } } } } } } return false; } let isRunningRestart = false; let restartId = 0; let lastRestartTime = 0; async function restart(server, projectDir, cachePath) { if (isRunningRestart) return; isRunningRestart = true; try { const id = ++restartId; if (requireInstall) { requireInstall = false; log("Installing dependencies...") execSync("npm install", { cwd: projectDir, stdio: "inherit" }); requireInstall = false; } if (id !== restartId) return; if (Date.now() - lastRestartTime < 3000) return; log("Restarting server...") lastRestartTime = Date.now(); requireInstall = false; if (existsSync(cachePath)) rmSync(cachePath, { recursive: true, force: true }); await triggerReloadOnClients(); // touch vite config to trigger reload // const viteConfigPath = path.join(projectDir, "vite.config.js"); // if (existsSync(viteConfigPath)) { // const content = readFileSync(viteConfigPath, "utf8"); // writeFileSync(viteConfigPath, content, "utf8"); // isRunningRestart = false; // return; // } // check if server is running if (server.httpServer?.listening) { server.restart(); } isRunningRestart = false; console.log("-----------------------------------------------") } catch (err) { log("Error restarting server", err); isRunningRestart = false; } }