@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.
232 lines (206 loc) • 7.66 kB
JavaScript
import https from 'https';
import { createHash } from 'crypto';
import { runInNewContext } from 'vm';
/**
* @typedef {{pluginContext:import('rollup').TransformPluginContext, cache:Cache}} Context
*/
/**
* Download files and rewrite code
* @param {string} command - The command that is being run
* @param {object} config - The config object
* @param {import('../types/userconfig.js').userSettings} userSettings
* @returns {import('vite').Plugin}
*/
export const needleMakeFilesLocal = (command, config, userSettings) => {
if (!userSettings?.makeFilesLocal?.enabled) {
return;
}
console.log(`[needle:local-files] Local files plugin is enabled`);
const cache = new Cache();
return {
name: "needle:local-files",
enforce: 'pre',
apply: "build",
// transform bundle
async transform(src, id) {
src = await makeLocal(src, "ext/", "", {
pluginContext: this,
cache: cache,
});
return {
code: src,
map: null,
};
}
}
}
/**
* Rewrites the source code to make local files
* @param {string} src - The source code to rewrite
* @param {string} basePath - The base path where the files will be saved
* @param {string} currentDir - The current directory of the file being processed
* @param {Context} context - The Vite plugin context
*/
async function makeLocal(src, basePath, currentDir, context) {
// find all google font urls fonts.googleapis.com
const googleFontRegex = /["'](https:\/\/fonts\.googleapis\.com\/.+?)["']/g;
let match;
while ((match = googleFontRegex.exec(src)) !== null) {
const fontUrl = match[1];
console.log(`\nFound google font URL: ${fontUrl}`);
// Check if the font URL is already in the cache
const cachedPath = context.cache.getFromCache(fontUrl);
if (cachedPath) {
console.log(`Using cached font URL: ${cachedPath}`);
src = src.replace(fontUrl, cachedPath);
continue; // Skip downloading if already cached
}
let font = await downloadText(fontUrl);
const familyNameMatch = /family=([^&]+)/.exec(fontUrl);
const familyName = getValidFilename(familyNameMatch[1], font);
font = await makeLocal(font, basePath, basePath, context);
const fontFileName = `font-${familyName}.css`;
const outputPath = basePath + fontFileName;
const referenceId = context.pluginContext.emitFile({
type: 'asset',
fileName: outputPath,
source: font,
});
const localPath = `${context.pluginContext.getFileName(referenceId)}`;
const newPath = getRelativeToBasePath(localPath, currentDir);
context.cache.addToCache(fontUrl, newPath);
src = src.replace(fontUrl, newPath);
}
const gstatic = /["'(](https:\/\/fonts\.gstatic\.com\/)(.+?)["')]/g;
while ((match = gstatic.exec(src)) !== null) {
const fontPath = match[2];
const fontUrl = match[1] + fontPath;
console.log(`\nFound gstatic URL: ${fontUrl}`);
// Check if the font URL is already in the cache
const cachedPath = context.cache.getFromCache(fontUrl);
if (cachedPath) {
console.log(`Using cached gstatic font URL: ${cachedPath}`);
src = src.replace(fontUrl, cachedPath);
continue; // Skip downloading if already cached
}
let font = await downloadBinary(fontUrl);
const filename = getValidFilename(fontPath, font);
console.log(`Saving font to: ${basePath + filename}`);
const referenceId = context.pluginContext.emitFile({
type: 'asset',
fileName: basePath + filename,
source: font,
});
const localPath = `${context.pluginContext.getFileName(referenceId)}`;
const newPath = getRelativeToBasePath(localPath, currentDir);
context.cache.addToCache(fontUrl, newPath);
src = src.replace(fontUrl, newPath);
}
return src;
}
class Cache {
__cache = new Map();
/**
* Adds a key-value pair to the cache.
* @param {string} key - The key to store the value under
* @param {string|Uint8Array} value - The value to store in the cache
* @returns {void}
*/
addToCache(key, value) {
if (this.__cache.has(key)) {
console.warn(`Key ${key} already exists in cache, overwriting.`);
}
this.__cache.set(key, value);
}
/**
* Retrieves a value from the cache by its key.
* @param {string} key - The key to look up in the cache
*/
getFromCache(key) {
if (this.__cache.has(key)) {
return this.__cache.get(key);
} else {
return null;
}
}
}
function getRelativeToBasePath(path, basePath) {
if (basePath?.length && path.startsWith(basePath)) {
return "./" + path.substring(basePath.length);
}
return path;
}
/**
* Generates a valid filename from a given path.
* @param {string} path - The path to generate a filename from
* @param {string|Uint8Array} content - The content to hash for uniqueness (not used in this example)
*/
function getValidFilename(path, content) {
// Remove any characters that are not valid in filenames
let name = path.replace(/[^a-z0-9_\-\.\+]/gi, '-');
const maxLength = 200;
if (path.length > maxLength) {
// If the name is too long, hash it to create a unique identifier
const hash = createContentMd5(content);
let ext = "";
const extIndex = name.lastIndexOf('.');
if (extIndex !== -1) {
ext = name.substring(extIndex + 1);
name = name.substring(0, extIndex);
}
name = `${name.substring(0, maxLength)}-${hash}${ext ? `.${ext}` : ''}`;
}
return name;
}
/**
* Creates a hash of the content using MD5.
* @param {string|Uint8Array} str - The content to hash
*/
function createContentMd5(str) {
return createHash('md5')
.update(str)
.digest('hex');
}
/**
* @param {string} url - The URL of the font to download
*/
function downloadText(url) {
return new Promise((res => {
https.get(url, (response) => {
if (response.statusCode !== 200) {
console.error(`Failed to download font from ${url}: ${response.statusCode}`);
res(null);
return;
}
let data = '';
response.on('data', (chunk) => {
data += chunk;
});
response.on('end', () => {
// Here you can save the data to a file or process it as needed
console.log(`Downloaded from ${url}`);
res(data);
});
});
}))
}
function downloadBinary(url) {
return new Promise((res, rej) => {
https.get(url, (response) => {
if (response.statusCode !== 200) {
console.error(`Failed to download font from ${url}: ${response.statusCode}`);
rej(new Error(`Failed to download font from ${url}: ${response.statusCode}`));
return;
}
const chunks = [];
response.on('data', (chunk) => {
chunks.push(chunk);
});
response.on('end', () => {
// Here you can save the data to a file or process it as needed
console.log(`Downloaded from ${url}`);
res(Buffer.concat(chunks));
});
});
});
}