@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.
190 lines (167 loc) • 8.53 kB
JavaScript
import { createWriteStream, existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
import path from 'path';
const projectDir = process.cwd() + "/";
/**
* @typedef {"auto-resolve" | ((res: string, packageName: string, index: number, path: string) => string | null | void)} PackageResolveValue
*/
/** these are alias callbacks as in the vite.alias dictionary
* the first argument is the already resoled absolute path (it is only invoked if the path was found in node_modules)
* the 2,3,4 args are the same as in vite.alias (packageName, index, path);
*/
/**
* @type {Record<string, PackageResolveValue>}
*/
const packages_to_resolve = {
// We are currently overriding "three" resolution to ensure that all dependencies resolve to the same three.js version.
// This is hacky, but the alternative is potentially having conflicting three.js versions since some packages are
// stubborn with their peer dependencies or just slow (slower as we) with updating.
// NOT adding this allows node.js to correctly resolve `exports` specified in three.js package.json;
// since we're overriding resolution here we need to manually resolve the subset of exports that we use.
'three/addons': (res, packageName, index, path) => {
return "three/examples/jsm";
},
'three/nodes': (res, packageName, index, path) => {
return "three/examples/jsm/nodes/Nodes.js";
},
'three': (res, packageName, index, _path) => {
return path.resolve(projectDir, 'node_modules', 'three');
},
// Handle all previous imports where users did import using @needle-engine/src
'@needle-tools/engine/src': (res, packageName, index, path) => {
// resolve old engine/src imports UNLESS it's the asap plugin (the asap plugin currently only exists in the src folder)
if (!path.startsWith("@needle-tools/engine/src/asap")) {
return res + "/../lib";
}
},
/*
Removed the previously present @needle-tools/engine entry
because this is automatically done by vite according to whatever we define in our package.json exports
This did previously prevent us from declaring proper exports in package.json
*/
'@needle-tools/engine': (res, packageName, index, _path) => {
// Check if the import is something like @needle-tools/engine/engine/engine_utils
// in which case we want to resolve into the lib directory
if (_path.startsWith("@needle-tools/engine/engine")) {
return res + "/lib";
}
const node_modules_path = path.resolve(projectDir, 'node_modules', '@needle-tools/engine');
if (existsSync(node_modules_path)) {
return node_modules_path;
}
},
// Auto resolve to 'node_modules/<name>' or '@needle-tools/engine/node_modules/<name>'
'peerjs': "auto-resolve",
'websocket-ts': "auto-resolve",
'md5': "auto-resolve",
'three-mesh-bvh': "auto-resolve",
'postprocessing': "auto-resolve",
'@dimforge/rapier3d-compat': "auto-resolve",
}
/**
* @param {import('../types').userSettings} userSettings
*/
export const needleViteAlias = (command, config, userSettings) => {
if (config?.noAlias === true || userSettings?.noAlias === true)
return;
const debug = userSettings.debugAlias;
let outputDebugFile = null;
function log(...msg) {
console.log(...msg);
if (debug && outputDebugFile) outputDebugFile.write(msg.join(" ") + "\n");
}
if (debug) {
const outputFilePath = path.resolve(projectDir, 'node_modules/.vite/needle.alias.log');
const outputDirectory = path.dirname(outputFilePath);
if (!existsSync(outputDirectory)) mkdirSync(outputDirectory, { recursive: true });
outputDebugFile = createWriteStream(outputFilePath, { flags: "a" });
const timestamp = new Date().toISOString();
outputDebugFile.write("\n\n\n--------------------------\n");
outputDebugFile.write(`[needle-alias] Logging to: ${outputFilePath} (${timestamp})\n`);
log("[needle-alias] Logging to: ", outputFilePath);
}
/** @type {import("vite").Plugin} */
const aliasPlugin = {
name: "needle-alias",
config(config) {
if (debug) console.log('[needle-alias] ProjectDirectory: ' + projectDir);
if (!config.resolve) config.resolve = {};
if (!config.resolve.alias) config.resolve.alias = {};
const aliasDict = config.resolve.alias;
for (const name in packages_to_resolve) {
if (!aliasDict[name]) {
addPathResolver(name, aliasDict, packages_to_resolve[name], debug ?? false);
}
}
if (debug) {
const testResults = [];
for (const name in aliasDict) {
const entry = aliasDict[name];
let res = entry;
if (typeof entry === "function") res = entry(name, 0, name);
testResults.push({ name, entry: res });
}
console.log('[needle-alias] Aliases: ', testResults);
}
},
}
let lastImporter = "";
/** This plugin logs all imports. This helps to find cases where incorrect folders are found/resolved. */
/** @type {import("vite").Plugin} */
const debuggingPlugin = {
name: "needle:alias-debug",
// needs to run before regular resolver
enforce: 'pre',
resolveId(id, importer, options) {
if (importer) {
// simplify paths for better readability
if (importer.includes("js/package~")) importer = "package~" + importer.split("js/package~")[1];
if (importer.includes("node_modules/@needle-tools")) importer = "node_modules/@needle-tools" + importer.split("node_modules/@needle-tools")[1];
if (importer.includes("node_modules/.vite")) importer = ".vite" + importer.split("node_modules/.vite")[1];
}
// could filter here, e.g. for things related to three
// if (id.includes("three")) return;
// verbose logging for all imports
if (lastImporter !== importer) {
lastImporter = importer;
log(`[needle-alias] Resolving: ${importer} (file${options?.ssr ? ", SSR" : ""})`);
}
log(`[needle-alias] → ${id}`);
return;
},
}
if (debug) return [debuggingPlugin, aliasPlugin];
return [aliasPlugin];
/**
* Adds a path resolver to the alias dictionary.
* @param {string} name - The name of the package to resolve.
* @param {import("vite").AliasOptions} aliasDict - The alias dictionary to add the resolver to.
* @param {PackageResolveValue | null} value - A callback function to override the default resolution behavior.
* @param {boolean} debug - Whether to log debug information.
* @returns {void}
*/
function addPathResolver(name, aliasDict, value, debug) {
// If a package at the node_modules path exist we resolve the request there
// introduced in 89a50718c38940abb99ee16c5e029065e41d7d65
const callback = typeof value === "function" ? value : null;
let fullpath = path.resolve(projectDir, 'node_modules', name);
if (!existsSync(fullpath)) {
fullpath = path.resolve(projectDir, 'node_modules', "@needle-tools/engine", "node_modules", name);
}
if (existsSync(fullpath)) {
aliasDict[name] = (packageName, index, path) => {
if (callback !== null) {
const overrideResult = callback(fullpath, packageName, index, path);
if (typeof overrideResult === "string")
if (existsSync(overrideResult)) {
if (debug) console.warn(`[needle-alias] \"${path}\" was requested and resolved to \"${overrideResult}\"`);
return overrideResult;
}
}
if (debug && path != packageName) {
console.warn(`[needle-alias] \"${path}\" was requested and resolved to \"${fullpath}\"`);
}
return fullpath;
}
}
}
};