@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
JavaScript
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;
}
}