Files
ansiblings/packages/nopy/dist/cubes/loader.js
T
Benjamin Diedrichsen 736c01216a initial transfer
2026-07-27 13:09:00 +02:00

103 lines
3.8 KiB
JavaScript

/**
* Cube discovery and loading from the filesystem
* @module cubes/loader
*/
import path from 'node:path';
import { z } from 'zod';
import { fs } from 'zx';
import { loadConfig } from '../nopy.config.js';
import { Cube } from './types.js';
/**
* Traverses upwards from the current working directory to the root
* and collects all directories that contain a `.npcubes` marker file.
*
* Also includes directories specified in the `.nopyrc.json` configuration.
*
* @returns Array of absolute paths to directories containing cubes
*/
export function findCubeDirectories() {
let currentDir = process.cwd();
const config = loadConfig();
const dirSet = new Set(config.cubeDirs.map((dir) => path.resolve(process.cwd(), dir)));
while (true) {
const targetFile = path.join(currentDir, '.npcubes');
if (fs.existsSync(targetFile) && fs.statSync(targetFile).isFile()) {
dirSet.add(currentDir);
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
break; // Stop when reaching the root
}
currentDir = parentDir;
}
return [...dirSet];
}
/**
* Extracts cube ID from name pattern [id] or explicit id field
*/
function extractCubeId(manifest) {
if (manifest.id)
return manifest.id;
const match = manifest.name.match(/^\[([^\]]+)\]/);
return match ? match[1] : undefined;
}
/**
* Loads all cubes from discovered cube directories.
*/
export async function loadCubes() {
const cubesFolders = findCubeDirectories();
const cubes = {};
const errors = [];
async function scanDirectory(currentDir, baseDir) {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
const files = entries.filter((e) => e.isFile());
const manifestFile = files.find((f) => f.name === 'manifest.mjs' || f.name.endsWith('.manifest.mjs'));
const deployFile = files.find((f) => f.name === 'deploy.py' || f.name.endsWith('.deploy.py'));
if (manifestFile && deployFile) {
const cubePath = currentDir;
const manifestPath = path.join(cubePath, manifestFile.name);
try {
const manifest = (await import(manifestPath)).default;
if (!manifest || typeof manifest !== 'object') {
errors.push(`Invalid manifest export in ${manifestPath}`);
}
else if (!manifest.name) {
errors.push(`Invalid manifest format in ${manifestPath}: missing 'name'`);
}
else {
const cubeId = extractCubeId(manifest) || path.basename(cubePath);
if (cubes[cubeId]) {
errors.push(`Duplicate cube id '${cubeId}'`);
return;
}
// Ensure basic properties
manifest.id = cubeId;
manifest.schema = manifest.schema ?? z.object({});
cubes[cubeId] = new Cube(manifest, cubePath, deployFile.name);
}
}
catch (err) {
errors.push(`Failed to load manifest ${manifestPath}: ${err}`);
}
}
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
await scanDirectory(path.join(currentDir, entry.name), baseDir);
}
}
}
await Promise.all(cubesFolders.map(async (folder) => {
if (fs.existsSync(folder)) {
await scanDirectory(folder, folder);
}
}));
return { cubes, errors };
}
/**
* Gets information about a single cube by name.
*/
export async function getCube(cubeName) {
const { cubes } = await loadCubes();
return cubes[cubeName];
}