@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.
235 lines (209 loc) • 8.19 kB
JavaScript
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
import path from 'path';
import { tryParseNeedleEngineSrcAttributeFromHtml } from '../common/needle-engine.js';
import { preloadScriptPaths } from './dependencies.js';
import { makeFilesLocalIsEnabled } from './local-files.js';
import { needleLog } from './logging.js';
const code = `import('@needle-tools/engine/src/asap/needle-asap.ts');`
/** Injects needle asap script into the index.html for when the main needle engine bundle is still being downloaded.
* @param {"build" | "serve"} command
* @param {import('../types/needleConfig').needleMeta | null | undefined} config
* @param {import('../types').userSettings} userSettings
* @returns {Promise<import('vite').Plugin[] | null>}
*/
export async function needleAsap(command, config, userSettings) {
if (userSettings.noAsap) return null;
fixMainTs();
if (command != "build") {
return null;
}
let logoSvg = "";
try {
const assets = await import("../../src/engine/assets/static.js");
logoSvg = assets.NEEDLE_LOGO_SVG_URL;
}
catch (err) {
needleLog("needle:asap", "Could not load needle logo svg: " + err.message, "warn", { dimBody: false });
}
/** @type {import("vite").ResolvedConfig | null} */
let viteConfig = null;
return [{
name: 'needle:asap',
configResolved(config) {
viteConfig = config;
},
transformIndexHtml: {
order: 'pre',
handler(html, _ctx) {
/** @type {import('vite').HtmlTagDescriptor[]} */
const tags = [];
try {
generateGltfPreloadLinks(config, html, tags);
}
catch (err) {
console.error("Error generating gltf preload links", err);
}
if (!makeFilesLocalIsEnabled(userSettings)) {
// preconnect to gstatic.com and fonts.googleapis.com to safe 100ms according to lighthouse
tags.push({
tag: 'link',
attrs: {
rel: "preconnect",
href: "https://fonts.gstatic.com",
}
});
tags.push({
tag: 'link',
attrs: {
rel: "preconnect",
href: "https://fonts.googleapis.com",
}
});
}
// we insert the logo late because the logo svg string is quite long
if (logoSvg?.length) {
tags.push({
tag: 'link',
attrs: {
rel: "preload",
fetchpriority: "high",
as: "image",
href: logoSvg,
type: "image/svg+xml",
}
})
}
return {
html,
tags
}
}
},
},
{
name: "needle:asap:post",
transformIndexHtml: {
order: 'post',
handler(html, _ctx) {
/** @type {import('vite').HtmlTagDescriptor[]} */
const tags = [];
if (viteConfig) {
// we need to wait for the files to exist
generateScriptPreloadLinks(viteConfig, tags);
}
return {
html,
tags
}
}
}
}]
}
function fixMainTs() {
// TODO: remove me
// we could also do this via a transform call - not sure if it's not better for users to see this change that's why i chose to modify the file on disc
const mainTsFilePath = process.cwd() + '/src/main.ts';
if (existsSync(mainTsFilePath)) {
let code = readFileSync(mainTsFilePath, 'utf-8');
if (code.includes('import \"@needle-tools/engine\"')) {
needleLog("needle:asap", "Changed main.ts and replaced needle engine import with async import", "log", { dimBody: false });
code = code.replace(/import \"@needle-tools\/engine\"/g, 'import("@needle-tools/engine") /* async import of needle engine */');
writeFileSync(mainTsFilePath, code);
}
}
}
/**
* @param {import('vite').ResolvedConfig} _config
* @param {import('vite').HtmlTagDescriptor[]} tags
*/
function generateScriptPreloadLinks(_config, tags) {
try {
const chunks = preloadScriptPaths;
// console.log("ASAP", chunks)
if (chunks.length > 0) {
for (const chunk of chunks) {
tags.push({
tag: 'link',
attrs: {
rel: "modulepreload",
as: "script",
href: chunk,
}
});
}
}
}
catch (err) {
needleLog("needle:asap", "Error generating script preload links: " + err.message, "error", { dimBody: false });
}
}
// https://regex101.com/r/I9k2nx/1
// @ts-ignore
const codegenRegex = /\"(?<gltf>.+(.glb|.gltf)(\?.*)?)\"/gm;
/**
* @param {import('../types').needleConfig} config
* @param {string} html
* @param {import('vite').HtmlTagDescriptor[]} tags
**/
function generateGltfPreloadLinks(config, html, tags) {
// TODO: try to get the <needle-engine src> element src attribute and preload that
const needleEngineMatches = tryParseNeedleEngineSrcAttributeFromHtml(html);
if (needleEngineMatches?.length) {
for (const item of needleEngineMatches) {
insertPreloadLink(tags, item, "model/gltf+json");
}
}
// TODO: we should export the entrypoint files to our meta file perhaps to make it easier to generate preload links
// We can not simply preload ALL the files in the assets folder because that would be too much and we don't know what the user ACTUALLY wants
let codegen_path = "/src/generated/";
if (config.codegenDirectory) {
codegen_path = config.codegenDirectory;
}
codegen_path = path.join(process.cwd(), codegen_path, "gen.js");
if (existsSync(codegen_path)) {
const code = readFileSync(codegen_path, "utf8");
if (code) {
const matches = code.matchAll(codegenRegex);
if (matches) {
while (true) {
const match = matches.next();
if (match.done) break;
const value = match.value?.groups?.gltf;
if (value) {
// if it's not a url we need to check if the file exists
const isUrl = value.startsWith("http");
if (!isUrl) {
let filepath = value.split("?")[0];
filepath = decodeURIComponent(filepath);
const fullpath = path.join(process.cwd(), filepath);
if (!existsSync(fullpath)) {
if (process.env.DEBUG) needleLog("needle:asap", `Could not insert glTF preload link: file not found at "${filepath}"`, "log", { dimBody: true });
continue;
}
}
needleLog("needle:asap", `Insert glTF preload link: ${value}`);
insertPreloadLink(tags, value, "model/gltf+json");
}
}
}
}
}
}
/**
* @param {import('vite').HtmlTagDescriptor[]} tags
* @param {string} href
* @param {string} type
*/
function insertPreloadLink(tags, href, type) {
if (!href) return;
tags.push({
tag: 'link',
attrs: {
rel: "preload",
as: "fetch",
href: href,
type: type,
crossorigin: true,
}
});
}