@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.
165 lines (146 loc) • 6.33 kB
JavaScript
import { resolve, join, isAbsolute, relative } from 'path'
import { existsSync, statSync, mkdirSync, readdirSync, copyFileSync, mkdir, rmSync } from 'fs';
import { builtAssetsDirectory, tryLoadProjectConfig } from './config.js';
import { copyFilesSync, formatBytes } from '../common/files.js';
import { needleBlue, needleDim, needleLog, needleSupportsColor } from './logging.js';
const pluginName = "needle-copy-files";
/** Copy files on build from assets to dist.
* @param {"build" | "serve"} command
* @param {import('../types/needleConfig').needleMeta | null | undefined} config
* @param {import('../types').userSettings} userSettings
* @returns {import('vite').Plugin | null}
*/
export function needleCopyFiles(command, config, userSettings) {
if (config?.noCopy === true || userSettings?.noCopy === true) {
return null;
}
return {
name: 'needle-copy-files',
// explicitly don't enforce post or pre because we want to run before the build-pipeline plugin
enforce: "pre",
buildStart() {
return run("start", config);
},
closeBundle() {
return run("end", config);
},
}
}
/**
* @param {"start" | "end"} buildstep
* @param {import('../types').userSettings} config
*/
async function run(buildstep, config) {
const copyIncludesFromEngine = config?.copyIncludesFromEngine ?? true;
const copied = { count: 0, bytes: 0 };
const baseDir = process.cwd();
const override = buildstep === "start";
const supportsColor = needleSupportsColor();
const key = (text) => supportsColor ? needleBlue(text) : text;
const rel = (pathValue) => {
const result = relative(baseDir, pathValue).replaceAll("\\", "/");
if (!result || result.length === 0) return ".";
return result;
};
let assetsDirName = "assets";
let outdirName = "dist";
const needleConfig = tryLoadProjectConfig();
if (needleConfig) {
assetsDirName = needleConfig.assetsDirectory || assetsDirName;
while (assetsDirName.startsWith('/')) assetsDirName = assetsDirName.substring(1);
if (needleConfig.buildDirectory)
outdirName = needleConfig.buildDirectory;
}
// For the "end" step, the first log line carries a leading newline to visually
// separate this section from the build-pipeline output above it.
let firstLog = true;
const logLine = (msg) => {
needleLog(pluginName, msg, "log", {
dimBody: false,
leadingNewline: firstLog && buildstep === "end",
});
firstLog = false;
};
if (copyIncludesFromEngine !== false) {
// copy include from engine
const engineIncludeDir = resolve(baseDir, 'node_modules', '@needle-tools', 'engine', 'src', 'include');
if (existsSync(engineIncludeDir)) {
logLine(needleDim(`Copy Needle Engine includes: ${rel(engineIncludeDir)} → ${rel(resolve(baseDir, 'include'))}`));
const projectIncludeDir = resolve(baseDir, 'include');
copyFilesSync(engineIncludeDir, projectIncludeDir, true, copied);
}
}
const outDir = resolve(baseDir, outdirName);
if (!existsSync(outDir)) {
mkdirSync(outDir, { recursive: true });
}
// copy a list of files or directories declared in build.copy = [] in the needle.config.json
/*
"build": {
"copy": ["myFolder", "myFile.txt"]
}
*/
if (needleConfig?.build?.copy) {
const arr = needleConfig.build.copy;
for (let i = 0; i < arr.length; i++) {
const entry = arr[i];
if (Array.isArray(entry)) {
console.log("WARN: build.copy can only contain string paths to copy to. Found array instead.");
continue;
}
const src = resolve(baseDir, entry);
const dest = resolvePath(outDir, entry);
if (existsSync(src) && dest) {
logLine(`Configured copy: ${rel(src)} → ${rel(dest)}`);
copyFilesSync(src, dest, override, copied);
}
}
}
// copy assets dir
const assetsDir = resolve(baseDir, assetsDirName);
if (existsSync(assetsDir)) {
const targetDir = resolve(outDir, 'assets');
// ensure that the target directory exists and is cleared if it already exists
// otherwise we might run into issues where the build pipeline is running for already compressed files
if (override && existsSync(targetDir)) {
logLine(needleDim(`Clearing target directory "${rel(targetDir)}"`));
rmSync(targetDir, { recursive: true, force: true });
}
logLine(`${key("Copying assets")}: ${rel(assetsDir)} → ${rel(targetDir)}`);
copyFilesSync(assetsDir, targetDir, override, copied);
}
else logLine(`No assets directory found. Skipping copy of ${assetsDirName} resolved to ${rel(assetsDir)}`);
// copy include dir
const includeDir = resolve(baseDir, 'include');
if (existsSync(includeDir)) {
const targetDir = resolve(outDir, 'include');
logLine(needleDim(`Include: ${rel(includeDir)} → ${rel(targetDir)}`));
copyFilesSync(includeDir, targetDir, override, copied);
}
logLine(`${key("Copied files")}: ${copied.count}, ${formatBytes(copied.bytes || 0)}`);
if (buildstep === "end") {
logLine("✨ Happy creating! 🌵");
}
}
/** resolves relative or absolute paths to a path inside the out directory
* for example D:/myFile.txt would resolve to outDir/myFile.txt
* wherereas "some/relative/path" would become outDir/some/relative/path
* @param {string} outDir
* @param {string} pathValue
*/
function resolvePath(outDir, pathValue) {
if (isAbsolute(pathValue)) {
var exists = existsSync(pathValue);
if (!exists) return null;
var stats = exists && statSync(pathValue);
if (stats.isDirectory()) {
const dirName = pathValue.replaceAll('\\', '/').split('/').pop();
if (!dirName) return null;
return resolve(outDir, dirName);
}
const fileName = pathValue.replaceAll('\\', '/').split('/').pop();
if (!fileName) return null;
return resolve(outDir, fileName);
}
return resolve(outDir, pathValue);
}