@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.
175 lines (162 loc) • 6.66 kB
JavaScript
// @ts-check
/**
* Type tables and components.needle.json manifest loader.
*
* Knows about primitive TS types, known Three.js types, known Needle Engine
* types, and how to resolve field types from the manifest.
*/
import { existsSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/** Primitive TS type strings that can safely appear in an ambient declaration. */
export const PRIMITIVE_TYPES = new Set(["number", "string", "boolean"]);
/**
* Known Three.js types → import("three").TypeName
* @type {Record<string, string>}
*/
export const THREE_TYPES = {
Color: `import("three").Color`,
ColorRepresentation: `import("three").ColorRepresentation`,
Euler: `import("three").Euler`,
Texture: `import("three").Texture`,
// Materials
Material: `import("three").Material`,
MeshStandardMaterial: `import("three").MeshStandardMaterial`,
// Objects
Object3D: `import("three").Object3D`,
Mesh: `import("three").Mesh`,
SkinnedMesh: `import("three").SkinnedMesh`,
// Other
Vector2: `import("three").Vector2`,
Vector3: `import("three").Vector3`,
Vector4: `import("three").Vector4`,
Matrix3: `import("three").Matrix3`,
Matrix4: `import("three").Matrix4`,
Quaternion: `import("three").Quaternion`,
// Animation
AnimationClip: `import("three").AnimationClip`,
AnimationMixer: `import("three").AnimationMixer`,
};
/**
* Known Needle Engine types → import("@needle-tools/engine").TypeName
* @type {Record<string, string>}
*/
export const NEEDLE_TYPES = {
AssetReference: `import("@needle-tools/engine").AssetReference`,
EventList: `import("@needle-tools/engine").EventList`,
GameObject: `import("@needle-tools/engine").GameObject`,
LookAtConstraint: `import("@needle-tools/engine").LookAtConstraint`,
RGBAColor: `import("@needle-tools/engine").RGBAColor`,
RenderTexture: `import("@needle-tools/engine").RenderTexture`,
Renderer: `import("@needle-tools/engine").Renderer`,
Rigidbody: `import("@needle-tools/engine").Rigidbody`,
Sprite: `import("@needle-tools/engine").Sprite`,
Vec2: `import("@needle-tools/engine").Vec2`,
};
/**
* Map a single non-array, non-primitive type token to its TS representation.
* Returns null if unknown.
* @param {string} token
* @returns {string | null}
*/
function mapKnownType(token) {
if (token in THREE_TYPES) return THREE_TYPES[token];
if (token in NEEDLE_TYPES) return NEEDLE_TYPES[token];
return null;
}
/**
* Convert a manifest type string to a safe ambient TS type.
* Primitives and known Three.js/Needle types are resolved precisely.
* For unknown types on a manifest component, falls back to
* `import("@needle-tools/engine").ComponentName["fieldName"]`.
* Truly unresolvable types → "unknown".
*
* @param {string} typeStr
* @param {string} [componentName] The manifest component class name (enables indexed-access fallback)
* @param {string} [fieldName] The field name on that component
* @returns {string}
*/
export function manifestTypeToTs(typeStr, componentName, fieldName) {
const parts = typeStr.split(" | ").map(p => p.trim());
const safeParts = parts.map(p => {
if (p === "undefined" || p === "null") return p;
const arrayMatch = p.match(/^(number|string|boolean)\[\]$/);
if (arrayMatch) return p;
if (PRIMITIVE_TYPES.has(p)) return p;
const arrayTypeMatch = p.match(/^(\w+)\[\]$/);
if (arrayTypeMatch) {
const base = arrayTypeMatch[1];
const mapped = mapKnownType(base);
if (mapped) return `${mapped}[]`;
}
const known = mapKnownType(p);
if (known) return known;
return null;
});
if (safeParts.every(p => p !== null)) {
return /** @type {string[]} */ (safeParts).join(" | ");
}
if (componentName && fieldName) {
return `import("@needle-tools/engine").${componentName}["${fieldName}"]`;
}
return "unknown";
}
/**
* Load components.needle.json and build a lookup:
* componentName → Map<fieldName, tsType>
* Inherited fields are flattened (inheritedFrom chain is resolved).
*
* @returns {Map<string, Map<string, string>>}
*/
export function loadComponentsManifest() {
/** @type {Map<string, Map<string, string>>} */
const manifest = new Map();
const manifestPath = join(__dirname, "../../components.needle.json");
if (!existsSync(manifestPath)) return manifest;
try {
/** @type {Array<{name: string, inheritedFrom?: string, children?: Array<{name: string, kind: string, type: string}>}>} */
const entries = JSON.parse(readFileSync(manifestPath, "utf8"));
/** @type {Map<string, Map<string, string>>} */
const ownFields = new Map();
/** @type {Map<string, string>} */
const inheritedFrom = new Map();
for (const entry of entries) {
if (!entry.name) continue;
inheritedFrom.set(entry.name, entry.inheritedFrom || "");
/** @type {Map<string, string>} */
const fields = new Map();
if (Array.isArray(entry.children)) {
for (const child of entry.children) {
if (child.kind === "property" && child.name && child.type) {
fields.set(child.name, manifestTypeToTs(child.type, entry.name, child.name));
}
}
}
ownFields.set(entry.name, fields);
}
/** @param {string} name @returns {Map<string, string>} */
function resolveFields(name) {
if (manifest.has(name)) return /** @type {Map<string, string>} */ (manifest.get(name));
const own = ownFields.get(name) ?? new Map();
const parent = inheritedFrom.get(name);
if (parent && ownFields.has(parent)) {
const parentFields = resolveFields(parent);
const merged = new Map([...parentFields, ...own]);
manifest.set(name, merged);
return merged;
}
manifest.set(name, own);
return own;
}
for (const name of ownFields.keys()) {
resolveFields(name);
}
} catch (e) {
console.warn("[needle:dts-generator] Failed to load components.needle.json:", (/** @type {any} */ (e))?.message ?? e);
}
return manifest;
}
/** @type {Map<string, Map<string, string>>} */
export const componentsManifest = loadComponentsManifest();