mirror of
https://github.com/unraid/api.git
synced 2026-01-03 15:09:48 -06:00
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Introduced Docker management UI components: Overview, Logs, Console, Preview, and Edit. - Added responsive Card/Detail layouts with grouping, bulk actions, and tabs. - New UnraidToaster component and global toaster configuration. - Component auto-mounting improved with async loading and multi-selector support. - UI/UX - Overhauled theme system (light/dark tokens, primary/orange accents) and added theme variants. - Header OS version now includes integrated changelog modal. - Registration displays warning states; multiple visual polish updates. - API - CPU load now includes percentGuest and percentSteal metrics. - Chores - Migrated web app to Vite; updated artifacts and manifests. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: mdatelle <mike@datelle.net> Co-authored-by: Michael Datelle <mdatelle@icloud.com>
228 lines
7.6 KiB
TypeScript
228 lines
7.6 KiB
TypeScript
import { join } from "path";
|
|
import { $, cd } from "zx";
|
|
import { existsSync } from "node:fs";
|
|
import { readdir, writeFile } from "node:fs/promises";
|
|
import { getTxzName, pluginName, startingDir } from "./utils/consts";
|
|
import { ensureNodeJs } from "./utils/nodejs-helper";
|
|
|
|
import { setupTxzEnv, TxzEnv } from "./cli/setup-txz-environment";
|
|
import { cleanupTxzFiles } from "./utils/cleanup";
|
|
import { apiDir } from "./utils/paths";
|
|
import { getVendorBundleName, getVendorFullPath } from "./build-vendor-store";
|
|
import { getAssetUrl } from "./utils/bucket-urls";
|
|
import { validateStandaloneManifest, getStandaloneManifestPath } from "./utils/manifest-validator";
|
|
|
|
|
|
// Check for manifest files in expected locations
|
|
const findManifestFiles = async (dir: string): Promise<string[]> => {
|
|
const files: string[] = [];
|
|
|
|
// Check standalone subdirectory (preferred)
|
|
try {
|
|
const standaloneDir = join(dir, "standalone");
|
|
const entries = await readdir(standaloneDir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.isFile() && entry.name === "standalone.manifest.json") {
|
|
files.push("standalone/standalone.manifest.json");
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Directory doesn't exist, continue checking other locations
|
|
}
|
|
|
|
// Check root directory for backwards compatibility
|
|
try {
|
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.isFile() && entry.name === "standalone.manifest.json") {
|
|
files.push("standalone.manifest.json");
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
console.warn(`Directory does not exist: ${dir}`);
|
|
return [];
|
|
}
|
|
throw error; // Re-throw other errors
|
|
}
|
|
|
|
return files;
|
|
};
|
|
|
|
// Function to store vendor archive information in a recoverable location
|
|
const storeVendorArchiveInfo = async (version: string, vendorUrl: string, vendorFilename: string) => {
|
|
try {
|
|
if (!version || !vendorUrl || !vendorFilename) {
|
|
throw new Error("Cannot store vendor archive info: Missing required parameters");
|
|
}
|
|
|
|
// Create a config directory in the source tree
|
|
const configDir = join(
|
|
startingDir,
|
|
"source",
|
|
"dynamix.unraid.net",
|
|
"usr",
|
|
"local",
|
|
"share",
|
|
"dynamix.unraid.net",
|
|
"config"
|
|
);
|
|
|
|
// Ensure directory exists
|
|
await $`mkdir -p ${configDir}`;
|
|
|
|
// Get the full path for vendor archive
|
|
const vendorFullPath = getVendorFullPath(version);
|
|
|
|
// Create a JSON config file with vendor information
|
|
const configData = {
|
|
// vendor_store_url: vendorUrl,
|
|
vendor_store_path: vendorFullPath,
|
|
api_version: version
|
|
};
|
|
|
|
// Validate all fields are present
|
|
Object.entries(configData).forEach(([key, value]) => {
|
|
if (!value) {
|
|
throw new Error(`Cannot store vendor archive info: Missing value for ${key}`);
|
|
}
|
|
});
|
|
|
|
const configPath = join(configDir, "vendor_archive.json");
|
|
await writeFile(configPath, JSON.stringify(configData, null, 2));
|
|
|
|
console.log(`Vendor archive information stored in ${configPath}`);
|
|
console.log(`API Version: ${version}`);
|
|
console.log(`Vendor URL: ${vendorUrl}`);
|
|
console.log(`Vendor Full Path: ${vendorFullPath}`);
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`Failed to store vendor archive information: ${error.message}`);
|
|
throw error; // Re-throw to prevent build from succeeding with invalid vendor info
|
|
}
|
|
};
|
|
|
|
const validateSourceDir = async (validatedEnv: TxzEnv) => {
|
|
if (!validatedEnv.ci) {
|
|
console.log("Validating TXZ source directory");
|
|
}
|
|
const sourceDir = join(startingDir, "source");
|
|
if (!existsSync(sourceDir)) {
|
|
throw new Error(`Source directory ${sourceDir} does not exist`);
|
|
}
|
|
// Validate existence of webcomponent files:
|
|
// source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
|
const webcomponentDir = join(
|
|
sourceDir,
|
|
"dynamix.unraid.net",
|
|
"usr",
|
|
"local",
|
|
"emhttp",
|
|
"plugins",
|
|
"dynamix.my.servers",
|
|
"unraid-components"
|
|
);
|
|
if (!existsSync(webcomponentDir)) {
|
|
throw new Error(`Webcomponent directory ${webcomponentDir} does not exist`);
|
|
}
|
|
|
|
const manifestFiles = await findManifestFiles(webcomponentDir);
|
|
const hasStandaloneManifest = manifestFiles.some(file =>
|
|
file === "standalone.manifest.json" || file === "standalone/standalone.manifest.json"
|
|
);
|
|
|
|
// Only require standalone.manifest.json for new standalone apps
|
|
if (!hasStandaloneManifest) {
|
|
console.log("Existing Manifest Files:", manifestFiles);
|
|
throw new Error(
|
|
`Webcomponents missing required file: standalone.manifest.json - ` +
|
|
`run 'pnpm build' in web to generate standalone.manifest.json in the standalone/ subdirectory`
|
|
);
|
|
}
|
|
|
|
// Validate the manifest contents
|
|
const manifestPath = getStandaloneManifestPath(webcomponentDir);
|
|
if (manifestPath) {
|
|
const validation = await validateStandaloneManifest(manifestPath);
|
|
|
|
if (!validation.isValid) {
|
|
console.error("Standalone manifest validation failed:");
|
|
validation.errors.forEach(error => console.error(` ❌ ${error}`));
|
|
if (validation.warnings.length > 0) {
|
|
console.warn("Warnings:");
|
|
validation.warnings.forEach(warning => console.warn(` ⚠️ ${warning}`));
|
|
}
|
|
throw new Error("Standalone manifest validation failed. See errors above.");
|
|
}
|
|
|
|
if (validation.warnings.length > 0) {
|
|
console.warn("Standalone manifest validation warnings:");
|
|
validation.warnings.forEach(warning => console.warn(` ⚠️ ${warning}`));
|
|
}
|
|
|
|
console.log("✅ Standalone manifest validation passed");
|
|
}
|
|
|
|
if (!existsSync(apiDir)) {
|
|
throw new Error(`API directory ${apiDir} does not exist`);
|
|
}
|
|
const packageJson = join(apiDir, "package.json");
|
|
if (!existsSync(packageJson)) {
|
|
throw new Error(`API package.json file ${packageJson} does not exist`);
|
|
}
|
|
// Now CHMOD the api/dist directory files to allow execution
|
|
await $`chmod +x ${apiDir}/dist/*.js`;
|
|
};
|
|
|
|
const buildTxz = async (validatedEnv: TxzEnv) => {
|
|
await validateSourceDir(validatedEnv);
|
|
|
|
// Use version from validated environment
|
|
const version = validatedEnv.apiVersion;
|
|
|
|
// Always use version when getting txz name
|
|
const txzName = getTxzName({ version, build: validatedEnv.buildNumber.toString() });
|
|
console.log(`Package name: ${txzName}`);
|
|
const txzPath = join(validatedEnv.txzOutputDir, txzName);
|
|
|
|
// Use the getVendorBundleName function for consistent naming
|
|
const vendorFilename = getVendorBundleName(version);
|
|
// Use the baseUrl and tag from validatedEnv, consistent with build-plugin.ts
|
|
const vendorUrl = getAssetUrl({
|
|
baseUrl: validatedEnv.baseUrl,
|
|
tag: validatedEnv.tag
|
|
}, vendorFilename);
|
|
|
|
console.log(`Storing vendor archive information: ${vendorUrl} -> ${vendorFilename}`);
|
|
await storeVendorArchiveInfo(version, vendorUrl, vendorFilename);
|
|
|
|
await Promise.all([
|
|
ensureNodeJs(),
|
|
]);
|
|
|
|
// Create package - must be run from within the pre-pack directory
|
|
// Use cd option to run command from prePackDir
|
|
await cd(join(startingDir, "source", pluginName));
|
|
$.verbose = true;
|
|
|
|
// Create the package using the default package name
|
|
await $`${join(startingDir, "scripts/makepkg")} --chown y --compress -${
|
|
validatedEnv.compress
|
|
} --linkadd y ${txzPath}`;
|
|
$.verbose = false;
|
|
await cd(startingDir);
|
|
};
|
|
|
|
const main = async () => {
|
|
try {
|
|
const validatedEnv = await setupTxzEnv(process.argv);
|
|
await cleanupTxzFiles();
|
|
await buildTxz(validatedEnv);
|
|
} catch (error) {
|
|
console.error(error);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
await main();
|