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.

192 lines (165 loc) 7.73 kB
// @ts-check /** * Vite plugin: needle:dts-generator * * Thin wrapper around plugins/dts-generator/index.js. * Regenerates `needle-bindings.d.ts` on startup and whenever a .glb / .gltf * file changes in the assets directory. * * The generated file is written to `{codegenDirectory}/needle-bindings.d.ts` * (falls back to `src/generated/needle-bindings.d.ts`). * * Usage — the plugin is already wired into needlePlugins(). To use standalone: * * import { needleDtsGenerator } from "@needle-tools/engine/plugins/vite/dts-generator.js"; * // in vite.config.js plugins array: * needleDtsGenerator(command, needleConfig, userSettings) */ import { join, resolve, dirname } from 'path'; import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync } from 'fs'; import { tryLoadProjectConfig } from './config.js'; import { generateBindingsDts } from '../dts-generator/index.js'; import { needleLog } from './logging.js'; // Two dirs up from plugins/vite/ → package root. // Use fileURLToPath so %7E and other URL-encoded characters in the path are decoded correctly. // realpathSync follows symlinks so the path is stable even in npm link / monorepo setups. import { fileURLToPath } from 'url'; let _packageRoot = /** @type {string | null} */ (null); try { _packageRoot = realpathSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..')); } catch (_e) { // If we can't resolve the package root (e.g. unusual install layout), the plugin // will silently skip generation rather than crashing the dev server. } /** * Ensure `.vscode/settings.json` references the generated `needle-html-data.json` * so VS Code provides `data-bind-needle` completions in HTML files automatically. * Only adds the entry if it isn't already present — never overwrites other settings. * * @param {string} projectRoot * @param {string} htmlDataPath Absolute path to the generated needle-html-data.json */ function ensureVscodeHtmlCustomData(projectRoot, htmlDataPath) { const vscodeDir = join(projectRoot, ".vscode"); const settingsPath = join(vscodeDir, "settings.json"); // Relative path from project root for portability const relPath = htmlDataPath.replace(projectRoot + "/", "").replace(projectRoot + "\\", ""); /** @type {Record<string, unknown>} */ let settings = {}; if (existsSync(settingsPath)) { try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch (_e) { /* malformed JSON — leave settings empty, will add key */ } } const key = "html.customData"; const existing = Array.isArray(settings[key]) ? /** @type {string[]} */ (settings[key]) : []; if (existing.includes(relPath)) return; // already registered settings[key] = [...existing, relPath]; mkdirSync(vscodeDir, { recursive: true }); writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8"); needleLog("needle:dts-generator", `registered HTML completions in .vscode/settings.json`); } /** * @param {"build" | "serve"} _command Vite command (unused — runs in both modes) * @param {import('../types/needleConfig').needleMeta | null | undefined} _config * @param {import('../types').userSettings} [_userSettings] * @returns {import('vite').Plugin | null} */ export function needleDtsGenerator(_command, _config, _userSettings) { if(_userSettings?.dts?.enabled === false) { return null; } let projectRoot = process.cwd(); function resolveCodegenDir() { const projectConfig = tryLoadProjectConfig(); return projectConfig?.codegenDirectory ? resolve(projectRoot, projectConfig.codegenDirectory) : join(projectRoot, "src", "generated"); } function resolveOutputPath() { // Write to a .needle dotfolder at the package root. // Not in the `files` allowlist → never npm-published. // ../../.needle/ from plugins/types/ resolves correctly for both // symlinked (js/package~/) and published (node_modules/@needle-tools/engine/) layouts. return join(/** @type {string} */ (_packageRoot), ".needle", "generated", "needle-bindings.gen.d.ts"); } function resolveAssetsDir() { const projectConfig = tryLoadProjectConfig(); return projectConfig?.assetsDirectory ? resolve(projectRoot, projectConfig.assetsDirectory) : join(projectRoot, "assets"); } /** @type {import('vite').ViteDevServer | undefined} */ let devServer; /** @type {Promise<void> | null} */ let _runInFlight = null; function run() { if (_runInFlight) return _runInFlight; _runInFlight = _doRun().finally(() => { _runInFlight = null; }); return _runInFlight; } async function _doRun() { try { if (!_packageRoot) return; const assetsDir = resolveAssetsDir(); const outputPath = resolveOutputPath(); const codegenDir = resolveCodegenDir(); const count = await generateBindingsDts({ assetsDir, outputPath, projectRoot, codegenDir }); // HTML custom data sits next to the generated dts in .needle/generated/ const htmlDataPath = join(/** @type {string} */ (_packageRoot), ".needle", "generated", "needle-html-data.json"); ensureVscodeHtmlCustomData(projectRoot, htmlDataPath); if (count !== false) { needleLog("needle:dts-generator", `${count} binding(s) → ${outputPath.replace(process.cwd(), ".")}`); if (devServer) { const hot = devServer.hot ?? devServer.ws; hot.send({ type: "full-reload", path: "*" }); } } else { needleLog("needle:dts-generator", `up-to-date → ${outputPath.replace(process.cwd(), ".")}`); } } catch (err) { needleLog("needle:dts-generator", "Failed: " + (/** @type {any} */ (err)?.message ?? err)); } } return { name: "needle:dts-generator", /** @param {import('vite').ResolvedConfig} config */ configResolved(config) { projectRoot = config.root ?? process.cwd(); }, buildStart() { // In serve mode, the configureServer post-hook runs instead. // Only run here for actual builds (no devServer). if (!devServer) return run(); return undefined; }, /** @param {import('vite').ViteDevServer} server */ configureServer(server) { devServer = server; // Watch assets directory for GLB/glTF changes and regenerate const assetsDir = resolveAssetsDir(); server.watcher.add(assetsDir); // Also watch files that determine which GLBs are entrypoints const indexHtmlPath = join(projectRoot, "index.html"); const genJsPath = join(resolveCodegenDir(), "gen.js"); server.watcher.add(indexHtmlPath); server.watcher.add(genJsPath); server.watcher.on("change", (file) => { if ( (/\.(glb|gltf)$/i.test(file) && file.startsWith(assetsDir)) || file === indexHtmlPath || file === genJsPath ) { run(); } }); server.watcher.on("add", (file) => { if (/\.(glb|gltf)$/i.test(file) && file.startsWith(assetsDir)) { run(); } }); // Return post-hook so Vite awaits the initial run before printing "ready" return () => run(); }, }; }