mirror of
https://github.com/unraid/api.git
synced 2026-01-06 08:39:54 -06:00
Feat/vue (#1655)
<!-- 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>
This commit is contained in:
@@ -10,34 +10,34 @@ 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";
|
||||
|
||||
|
||||
// Recursively search for manifest files
|
||||
// Check for manifest files in expected locations
|
||||
const findManifestFiles = async (dir: string): Promise<string[]> => {
|
||||
const files: string[] = [];
|
||||
|
||||
// Check standalone subdirectory (preferred)
|
||||
try {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
|
||||
const standaloneDir = join(dir, "standalone");
|
||||
const entries = await readdir(standaloneDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
try {
|
||||
files.push(...(await findManifestFiles(fullPath)));
|
||||
} catch (error) {
|
||||
// Log and continue if a subdirectory can't be read
|
||||
console.warn(`Warning: Could not read directory ${fullPath}: ${error.message}`);
|
||||
}
|
||||
} else if (
|
||||
entry.isFile() &&
|
||||
(entry.name === "manifest.json" ||
|
||||
entry.name === "ui.manifest.json" ||
|
||||
entry.name === "standalone.manifest.json")
|
||||
) {
|
||||
files.push(entry.name);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.warn(`Directory does not exist: ${dir}`);
|
||||
@@ -45,6 +45,8 @@ const findManifestFiles = async (dir: string): Promise<string[]> => {
|
||||
}
|
||||
throw error; // Re-throw other errors
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
// Function to store vendor archive information in a recoverable location
|
||||
@@ -125,24 +127,41 @@ const validateSourceDir = async (validatedEnv: TxzEnv) => {
|
||||
}
|
||||
|
||||
const manifestFiles = await findManifestFiles(webcomponentDir);
|
||||
const hasManifest = manifestFiles.includes("manifest.json");
|
||||
const hasStandaloneManifest = manifestFiles.includes("standalone.manifest.json");
|
||||
const hasUiManifest = manifestFiles.includes("ui.manifest.json");
|
||||
const hasStandaloneManifest = manifestFiles.some(file =>
|
||||
file === "standalone.manifest.json" || file === "standalone/standalone.manifest.json"
|
||||
);
|
||||
|
||||
// Accept either manifest.json (old web components) or standalone.manifest.json (new standalone apps)
|
||||
if ((!hasManifest && !hasStandaloneManifest) || !hasUiManifest) {
|
||||
// Only require standalone.manifest.json for new standalone apps
|
||||
if (!hasStandaloneManifest) {
|
||||
console.log("Existing Manifest Files:", manifestFiles);
|
||||
const missingFiles: string[] = [];
|
||||
if (!hasManifest && !hasStandaloneManifest) missingFiles.push("manifest.json or standalone.manifest.json");
|
||||
if (!hasUiManifest) missingFiles.push("ui.manifest.json");
|
||||
|
||||
throw new Error(
|
||||
`Webcomponents missing required file(s): ${missingFiles.join(", ")} - ` +
|
||||
`${!hasUiManifest ? "run 'pnpm build:wc' in unraid-ui for ui.manifest.json" : ""}` +
|
||||
`${(!hasManifest && !hasStandaloneManifest) && !hasUiManifest ? " and " : ""}` +
|
||||
`${(!hasManifest && !hasStandaloneManifest) ? "run 'pnpm build' in web for standalone.manifest.json" : ""}`
|
||||
`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`);
|
||||
|
||||
290
plugin/builder/utils/manifest-validator.test.ts
Normal file
290
plugin/builder/utils/manifest-validator.test.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdir, writeFile, rm } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
import {
|
||||
validateStandaloneManifest,
|
||||
getStandaloneManifestPath,
|
||||
type StandaloneManifest
|
||||
} from "./manifest-validator";
|
||||
|
||||
describe("manifest-validator", () => {
|
||||
let testDir: string;
|
||||
let manifestPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create a temporary test directory
|
||||
testDir = join(tmpdir(), `manifest-test-${Date.now()}`);
|
||||
await mkdir(testDir, { recursive: true });
|
||||
manifestPath = join(testDir, "standalone.manifest.json");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up test directory
|
||||
await rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("validateStandaloneManifest", () => {
|
||||
it("should fail when manifest file does not exist", async () => {
|
||||
const result = await validateStandaloneManifest(join(testDir, "nonexistent.json"));
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0]).toContain("Manifest file does not exist");
|
||||
});
|
||||
|
||||
it("should fail when manifest has invalid JSON", async () => {
|
||||
await writeFile(manifestPath, "{ invalid json");
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0]).toContain("Failed to parse manifest JSON");
|
||||
});
|
||||
|
||||
it("should pass for valid manifest with existing files", async () => {
|
||||
// Create the referenced files
|
||||
await writeFile(join(testDir, "app.js"), "console.log('app');");
|
||||
await writeFile(join(testDir, "app.css"), "body { color: red; }");
|
||||
|
||||
// Create valid manifest
|
||||
const manifest: StandaloneManifest = {
|
||||
"app.js": {
|
||||
file: "app.js",
|
||||
src: "app.js",
|
||||
isEntry: true,
|
||||
},
|
||||
"app.css": {
|
||||
file: "app.css",
|
||||
src: "app.css",
|
||||
},
|
||||
ts: Date.now(),
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
expect(result.warnings).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail when referenced files are missing", async () => {
|
||||
const manifest: StandaloneManifest = {
|
||||
"app.js": {
|
||||
file: "app.js",
|
||||
src: "app.js",
|
||||
},
|
||||
"app.css": {
|
||||
file: "app.css",
|
||||
src: "app.css",
|
||||
},
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toHaveLength(2);
|
||||
expect(result.errors).toContain("Missing file referenced in manifest: app.js");
|
||||
expect(result.errors).toContain("Missing file referenced in manifest: app.css");
|
||||
});
|
||||
|
||||
it("should fail when CSS files in array are missing", async () => {
|
||||
await writeFile(join(testDir, "app.js"), "console.log('app');");
|
||||
|
||||
const manifest: StandaloneManifest = {
|
||||
"app.js": {
|
||||
file: "app.js",
|
||||
css: ["style1.css", "style2.css"],
|
||||
},
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toHaveLength(2);
|
||||
expect(result.errors).toContain("Missing CSS file referenced in manifest: style1.css");
|
||||
expect(result.errors).toContain("Missing CSS file referenced in manifest: style2.css");
|
||||
});
|
||||
|
||||
it("should fail when asset files are missing", async () => {
|
||||
await writeFile(join(testDir, "app.js"), "console.log('app');");
|
||||
|
||||
const manifest: StandaloneManifest = {
|
||||
"app.js": {
|
||||
file: "app.js",
|
||||
assets: ["image.png", "font.woff2"],
|
||||
},
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toHaveLength(2);
|
||||
expect(result.errors).toContain("Missing asset file referenced in manifest: image.png");
|
||||
expect(result.errors).toContain("Missing asset file referenced in manifest: font.woff2");
|
||||
});
|
||||
|
||||
it("should warn for missing imports but not fail", async () => {
|
||||
await writeFile(join(testDir, "app.js"), "console.log('app');");
|
||||
|
||||
const manifest: StandaloneManifest = {
|
||||
"app.js": {
|
||||
file: "app.js",
|
||||
imports: ["virtual-module"],
|
||||
},
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
expect(result.warnings).toHaveLength(1);
|
||||
expect(result.warnings[0]).toContain("Missing import file referenced in manifest: virtual-module");
|
||||
});
|
||||
|
||||
it("should skip timestamp field", async () => {
|
||||
await writeFile(join(testDir, "app.js"), "console.log('app');");
|
||||
|
||||
const manifest = {
|
||||
"app.js": {
|
||||
file: "app.js",
|
||||
},
|
||||
ts: 1234567890,
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should warn for non-entry fields", async () => {
|
||||
await writeFile(join(testDir, "app.js"), "console.log('app');");
|
||||
|
||||
const manifest = {
|
||||
"app.js": {
|
||||
file: "app.js",
|
||||
},
|
||||
"invalid": "not an entry",
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.warnings).toHaveLength(1);
|
||||
expect(result.warnings[0]).toContain("Skipping non-entry field: invalid");
|
||||
});
|
||||
|
||||
it("should fail when no JavaScript entry exists", async () => {
|
||||
await writeFile(join(testDir, "app.css"), "body { color: red; }");
|
||||
|
||||
const manifest: StandaloneManifest = {
|
||||
"app.css": {
|
||||
file: "app.css",
|
||||
},
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.errors).toHaveLength(1);
|
||||
expect(result.errors[0]).toContain("Manifest must contain at least one JavaScript entry file");
|
||||
});
|
||||
|
||||
it("should not check duplicate files multiple times", async () => {
|
||||
await writeFile(join(testDir, "app.js"), "console.log('app');");
|
||||
await writeFile(join(testDir, "shared.css"), "body { color: red; }");
|
||||
|
||||
const manifest: StandaloneManifest = {
|
||||
"entry1": {
|
||||
file: "app.js",
|
||||
css: ["shared.css"],
|
||||
},
|
||||
"entry2": {
|
||||
file: "app.js",
|
||||
css: ["shared.css"],
|
||||
},
|
||||
};
|
||||
|
||||
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
const result = await validateStandaloneManifest(manifestPath);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStandaloneManifestPath", () => {
|
||||
it("should find manifest in standalone subdirectory (preferred)", async () => {
|
||||
const standaloneDir = join(testDir, "standalone");
|
||||
await mkdir(standaloneDir, { recursive: true });
|
||||
const standaloneManifestPath = join(standaloneDir, "standalone.manifest.json");
|
||||
await writeFile(standaloneManifestPath, "{}");
|
||||
|
||||
const path = getStandaloneManifestPath(testDir);
|
||||
|
||||
expect(path).toBe(standaloneManifestPath);
|
||||
});
|
||||
|
||||
it("should find manifest in root directory", async () => {
|
||||
await writeFile(manifestPath, "{}");
|
||||
|
||||
const path = getStandaloneManifestPath(testDir);
|
||||
|
||||
expect(path).toBe(manifestPath);
|
||||
});
|
||||
|
||||
it("should find manifest in nuxt subdirectory for backwards compatibility", async () => {
|
||||
const nuxtDir = join(testDir, "nuxt");
|
||||
await mkdir(nuxtDir, { recursive: true });
|
||||
const nuxtManifestPath = join(nuxtDir, "standalone.manifest.json");
|
||||
await writeFile(nuxtManifestPath, "{}");
|
||||
|
||||
const path = getStandaloneManifestPath(testDir);
|
||||
|
||||
expect(path).toBe(nuxtManifestPath);
|
||||
});
|
||||
|
||||
it("should prefer standalone subdirectory over root and nuxt", async () => {
|
||||
// Create manifest in all locations
|
||||
const standaloneDir = join(testDir, "standalone");
|
||||
await mkdir(standaloneDir, { recursive: true });
|
||||
const standaloneManifestPath = join(standaloneDir, "standalone.manifest.json");
|
||||
await writeFile(standaloneManifestPath, "{}");
|
||||
|
||||
await writeFile(manifestPath, "{}");
|
||||
|
||||
const nuxtDir = join(testDir, "nuxt");
|
||||
await mkdir(nuxtDir, { recursive: true });
|
||||
await writeFile(join(nuxtDir, "standalone.manifest.json"), "{}");
|
||||
|
||||
const path = getStandaloneManifestPath(testDir);
|
||||
|
||||
expect(path).toBe(standaloneManifestPath);
|
||||
});
|
||||
|
||||
it("should return null when no manifest exists", async () => {
|
||||
const path = getStandaloneManifestPath(testDir);
|
||||
|
||||
expect(path).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
173
plugin/builder/utils/manifest-validator.ts
Normal file
173
plugin/builder/utils/manifest-validator.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { existsSync } from "fs";
|
||||
import { readFile } from "fs/promises";
|
||||
import { join, dirname } from "path";
|
||||
|
||||
export interface ManifestEntry {
|
||||
file: string;
|
||||
src?: string;
|
||||
css?: string[];
|
||||
assets?: string[];
|
||||
imports?: string[];
|
||||
dynamicImports?: string[];
|
||||
isDynamicEntry?: boolean;
|
||||
isEntry?: boolean;
|
||||
}
|
||||
|
||||
export interface StandaloneManifest {
|
||||
[key: string]: ManifestEntry | number;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
manifest?: StandaloneManifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a standalone.manifest.json file and checks that all referenced files exist
|
||||
* @param manifestPath - Path to the manifest file
|
||||
* @returns Validation result with errors and warnings
|
||||
*/
|
||||
export async function validateStandaloneManifest(manifestPath: string): Promise<ValidationResult> {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Check if manifest file exists
|
||||
if (!existsSync(manifestPath)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errors: [`Manifest file does not exist: ${manifestPath}`],
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
let manifest: StandaloneManifest;
|
||||
|
||||
try {
|
||||
const content = await readFile(manifestPath, "utf-8");
|
||||
manifest = JSON.parse(content);
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
errors: [`Failed to parse manifest JSON: ${error.message}`],
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the directory containing the manifest
|
||||
// Files should be relative to the manifest location
|
||||
const manifestDir = dirname(manifestPath);
|
||||
|
||||
// Track which files were checked to avoid duplicates
|
||||
const checkedFiles = new Set<string>();
|
||||
|
||||
// Validate each entry in the manifest
|
||||
for (const [key, value] of Object.entries(manifest)) {
|
||||
// Skip the timestamp field
|
||||
if (key === "ts" && typeof value === "number") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if not a manifest entry
|
||||
if (typeof value !== "object" || !value || !("file" in value)) {
|
||||
warnings.push(`Skipping non-entry field: ${key}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = value as ManifestEntry;
|
||||
|
||||
// Check main file
|
||||
if (entry.file) {
|
||||
const filePath = join(manifestDir, entry.file);
|
||||
if (!checkedFiles.has(filePath)) {
|
||||
checkedFiles.add(filePath);
|
||||
if (!existsSync(filePath)) {
|
||||
errors.push(`Missing file referenced in manifest: ${entry.file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check CSS files
|
||||
if (entry.css && Array.isArray(entry.css)) {
|
||||
for (const cssFile of entry.css) {
|
||||
const cssPath = join(manifestDir, cssFile);
|
||||
if (!checkedFiles.has(cssPath)) {
|
||||
checkedFiles.add(cssPath);
|
||||
if (!existsSync(cssPath)) {
|
||||
errors.push(`Missing CSS file referenced in manifest: ${cssFile}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check asset files
|
||||
if (entry.assets && Array.isArray(entry.assets)) {
|
||||
for (const assetFile of entry.assets) {
|
||||
const assetPath = join(manifestDir, assetFile);
|
||||
if (!checkedFiles.has(assetPath)) {
|
||||
checkedFiles.add(assetPath);
|
||||
if (!existsSync(assetPath)) {
|
||||
errors.push(`Missing asset file referenced in manifest: ${assetFile}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check imports
|
||||
if (entry.imports && Array.isArray(entry.imports)) {
|
||||
for (const importFile of entry.imports) {
|
||||
const importPath = join(manifestDir, importFile);
|
||||
if (!checkedFiles.has(importPath)) {
|
||||
checkedFiles.add(importPath);
|
||||
if (!existsSync(importPath)) {
|
||||
warnings.push(`Missing import file referenced in manifest: ${importFile} (this may be okay if it's a virtual import)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for required entries
|
||||
const hasJsEntry = Object.values(manifest).some(
|
||||
(entry) => typeof entry === "object" && entry?.file?.endsWith(".js")
|
||||
);
|
||||
|
||||
if (!hasJsEntry) {
|
||||
errors.push("Manifest must contain at least one JavaScript entry file");
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
manifest,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the standalone manifest file in a directory
|
||||
* @param dir - Directory to search in
|
||||
* @returns Path to the manifest file or null if not found
|
||||
*/
|
||||
export function getStandaloneManifestPath(dir: string): string | null {
|
||||
// Check standalone subdirectory first (preferred location)
|
||||
const standaloneManifest = join(dir, "standalone", "standalone.manifest.json");
|
||||
if (existsSync(standaloneManifest)) {
|
||||
return standaloneManifest;
|
||||
}
|
||||
|
||||
// Check root directory for backwards compatibility
|
||||
const rootManifest = join(dir, "standalone.manifest.json");
|
||||
if (existsSync(rootManifest)) {
|
||||
return rootManifest;
|
||||
}
|
||||
|
||||
// Check nuxt subdirectory for backwards compatibility
|
||||
const nuxtManifest = join(dir, "nuxt", "standalone.manifest.json");
|
||||
if (existsSync(nuxtManifest)) {
|
||||
return nuxtManifest;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- ./source:/app/source
|
||||
- ./scripts:/app/scripts
|
||||
- ../unraid-ui/dist-wc:/app/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/uui
|
||||
- ../web/.nuxt/nuxt-custom-elements/dist/unraid-components:/app/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt
|
||||
- ../web/dist:/app/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone
|
||||
- ../api/deploy/release/:/app/source/dynamix.unraid.net/usr/local/unraid-api # Use the release dir instead of pack to allow watcher to not try to build with node_modules
|
||||
stdin_open: true # equivalent to -i
|
||||
tty: true # equivalent to -t
|
||||
|
||||
@@ -27,10 +27,10 @@ CONTAINER_NAME="plugin-builder"
|
||||
|
||||
# Create the directory if it doesn't exist
|
||||
# This is to prevent errors when mounting volumes in docker compose
|
||||
NUXT_COMPONENTS_DIR="../web/.nuxt/nuxt-custom-elements/dist/unraid-components"
|
||||
if [ ! -d "$NUXT_COMPONENTS_DIR" ]; then
|
||||
echo "Creating directory $NUXT_COMPONENTS_DIR for Docker volume mount..."
|
||||
mkdir -p "$NUXT_COMPONENTS_DIR"
|
||||
WEB_DIST_DIR="../web/dist"
|
||||
if [ ! -d "$WEB_DIST_DIR" ]; then
|
||||
echo "Creating directory $WEB_DIST_DIR for Docker volume mount..."
|
||||
mkdir -p "$WEB_DIST_DIR"
|
||||
fi
|
||||
|
||||
# Stop any running plugin-builder container first
|
||||
|
||||
@@ -25,3 +25,9 @@ backup_file_if_exists usr/local/unraid-api/.env
|
||||
cp usr/local/unraid-api/.env.production usr/local/unraid-api/.env
|
||||
|
||||
# auto-generated actions from makepkg:
|
||||
( cd usr/local/bin ; rm -rf corepack )
|
||||
( cd usr/local/bin ; ln -sf ../lib/node_modules/corepack/dist/corepack.js corepack )
|
||||
( cd usr/local/bin ; rm -rf npm )
|
||||
( cd usr/local/bin ; ln -sf ../lib/node_modules/npm/bin/npm-cli.js npm )
|
||||
( cd usr/local/bin ; rm -rf npx )
|
||||
( cd usr/local/bin ; ln -sf ../lib/node_modules/npm/bin/npx-cli.js npx )
|
||||
|
||||
Reference in New Issue
Block a user