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.

218 lines (197 loc) 10.8 kB
import { createWriteStream, existsSync, mkdirSync } from 'fs'; import path from 'path'; import { needleLog } from './logging.js'; const projectDir = process.cwd() + "/"; /** * @typedef {"auto-resolve" | ((res: string, packageName: string, index: number, importPath: string) => string | null | void)} PackageResolveValue */ /** * @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, importPath) => { // Replace "three/addons" prefix with "three/examples/jsm", preserving any subpath const subpath = importPath.substring("three/addons".length); return "three/examples/jsm" + subpath; }, 'three/nodes': (res, packageName, index, importPath) => { return "three/examples/jsm/nodes/Nodes.js"; }, 'three': "auto-resolve", // Handle all previous imports where users did import using @needle-engine/src '@needle-tools/engine/src': (res, packageName, index, importPath) => { // resolve old engine/src imports UNLESS it's the asap plugin (the asap plugin currently only exists in the src folder) if (!importPath.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, importPath) => { // 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 (importPath.startsWith("@needle-tools/engine/engine")) { return res + "/lib"; } // Return null for bare "@needle-tools/engine" imports so the resolveId // hook skips resolution entirely and lets Vite's optimizer redirect to // the pre-bundled version. Without this, the alias plugin resolves to // the raw source path, bypassing pre-bundled CJS interop for transitive // deps like rtcpeerconnection-shim. return null; }, // 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 {"build" | "serve"} command * @param {import('../types/needleConfig').needleMeta | null | undefined} config * @param {import('../types').userSettings} userSettings * @returns {import('vite').Plugin | import('vite').Plugin[] | undefined} */ export function needleViteAlias(command, config, userSettings) { if (config?.noAlias === true || userSettings?.noAlias === true) return; const debug = userSettings.debugAlias; let outputDebugFile = null; function log(...msg) { needleLog("needle-alias", msg.join(" ")); if (debug) logToFile(...msg); } function logToFile(...msg) { outputDebugFile?.write(msg.join(" ") + "\n"); } if (debug) { const outputFilePath = path.resolve(projectDir, 'node_modules/.needle/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("Logging to:", outputFilePath); } // Pre-compute resolved paths for each package /** @type {Array<{name: string, fullpath: string, pathExists: boolean, isNestedInEngine: boolean, callback: ((res: string, packageName: string, index: number, importPath: string) => string | null | void) | null}>} */ const resolvers = []; /** @type {Array<{find: string, replacement: string}>} Aliases for nested engine auto-resolve packages */ const nestedAliases = []; for (const name in packages_to_resolve) { const value = packages_to_resolve[name]; const callback = typeof value === "function" ? value : null; const isAutoResolve = value === "auto-resolve"; let fullpath = path.resolve(projectDir, 'node_modules', name); let isNestedInEngine = false; { const pathInEngine = path.resolve(projectDir, 'node_modules', "@needle-tools/engine", "node_modules", name); if (existsSync(pathInEngine)) { fullpath = pathInEngine; isNestedInEngine = true; } } const pathExists = existsSync(fullpath); resolvers.push({ name, fullpath, pathExists, isNestedInEngine, callback }); // For auto-resolve packages nested in the engine, use resolve.alias so both // the optimizer and resolveId see the same path. This ensures Vite pre-bundles // these deps (handling CJS interop) while keeping a single copy. if (isAutoResolve && isNestedInEngine && pathExists) { nestedAliases.push({ find: name, replacement: fullpath }); } } const workingDirectory = `${process.cwd()}/`; /** @type {import("vite").Plugin} */ const aliasPlugin = { name: "needle-alias", // Register resolve.alias for nested engine packages so Vite's optimizer // can find and pre-bundle them (with proper CJS interop). config(config) { if (nestedAliases.length > 0) { if (!config.resolve) config.resolve = {}; // resolve.alias can be an array of {find,replacement} or a Record<string,string>. // Convert to array format if needed (works in Vite 2-8). if (!config.resolve.alias) { config.resolve.alias = []; } else if (!Array.isArray(config.resolve.alias)) { // Convert Record<string,string> to array format const entries = Object.entries(config.resolve.alias); config.resolve.alias = entries.map(([find, replacement]) => ({ find, replacement })); } for (const alias of nestedAliases) { config.resolve.alias.push(alias); needleLog("needle-alias", `Adding resolve.alias: ${alias.find}${alias.replacement.split('node_modules/').pop()}`); } } }, // Use resolveId instead of resolve.alias to support both Rollup and Rolldown (Vite 8+). // Rolldown does not support function-based alias replacements. enforce: 'pre', async resolveId(id, _importer, _options) { for (const { name, fullpath, pathExists, isNestedInEngine, callback } of resolvers) { // Check if import matches this resolver (exact match or starts with name/) if (id !== name && !id.startsWith(name + '/')) continue; if (callback !== null) { const overrideResult = callback(fullpath, name, 0, id); if (typeof overrideResult === "string") { if (debug) log(`Redirecting "${id}" → "${overrideResult}"`); // Let Vite resolve the result further (handles directories, package.json exports, etc.) return this.resolve(overrideResult, _importer, { skipSelf: true }); } // If the callback explicitly returned null, skip all resolution // and let Vite handle it (e.g. optimizer pre-bundled deps). if (overrideResult === null) break; } // For auto-resolve packages in the project's own node_modules, // let Vite handle resolution naturally (preserves CJS interop / optimizeDeps). // Only redirect when the package is nested inside @needle-tools/engine/node_modules. if (pathExists && (callback !== null || isNestedInEngine)) { if (debug) log(`Resolved "${id}" → "${fullpath.substring(workingDirectory.length).replaceAll("\\", "/")}"`); // Rewrite the import to resolve from the target directory, letting Vite handle package.json/exports const subpath = id.substring(name.length); // e.g. "" or "/something" const rewritten = fullpath + subpath; return this.resolve(rewritten, _importer, { skipSelf: true }); } break; } }, } /** @type {string|undefined} */ 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]; } // verbose logging for all imports if (lastImporter !== importer) { lastImporter = importer; logToFile(`[needle-alias] Resolving: ${importer} (file${options?.ssr ? ", SSR" : ""})`); } logToFile(`[needle-alias] → ${id}`); return; }, } if (debug) return [debuggingPlugin, aliasPlugin]; return [aliasPlugin]; }