// Copyright Epic Games, Inc. All Rights Reserved. "use strict"; import { ZenPage } from "./page.js" import { Fetcher } from "../util/fetcher.js" import { Table, Toolbar, PropTable } from "../util/widgets.js" //////////////////////////////////////////////////////////////////////////////// export class Page extends ZenPage { main() { this.set_title("cook artifacts"); const project = this.get_param("project"); const oplog = this.get_param("oplog"); const opkey = this.get_param("opkey"); const artifact_hash = this.get_param("hash"); // Fetch the artifact content as JSON this._artifact = new Fetcher() .resource("prj", project, "oplog", oplog, artifact_hash + ".json") .json(); // Optionally fetch entry info for display context if (opkey) { this._entry = new Fetcher() .resource("prj", project, "oplog", oplog, "entries") .param("opkey", opkey) .cbo(); } this._build_page(); } // Map CookDependency enum values to display names _get_dependency_type_name(type_value) { const type_names = { 0: "None", 1: "File", 2: "Function", 3: "TransitiveBuild", 4: "Package", 5: "ConsoleVariable", 6: "Config", 7: "SettingsObject", 8: "NativeClass", 9: "AssetRegistryQuery", 10: "RedirectionTarget" }; return type_names[type_value] || `Unknown (${type_value})`; } // Check if Data content should be expandable _should_make_expandable(data_string) { if (!data_string || data_string.length < 40) return false; // Check if it's JSON array or object if (!data_string.startsWith('[') && !data_string.startsWith('{')) return false; // Check if formatting would add newlines try { const parsed = JSON.parse(data_string); const formatted = JSON.stringify(parsed, null, 2); return formatted.includes('\n'); } catch (e) { return false; } } // Get first line of content for collapsed state _get_first_line(data_string) { if (!data_string) return ""; const newline_index = data_string.indexOf('\n'); if (newline_index === -1) { // No newline, truncate if too long return data_string.length > 80 ? data_string.substring(0, 77) + "..." : data_string; } return data_string.substring(0, newline_index) + "..."; } // Format JSON with indentation _format_json(data_string) { try { const parsed = JSON.parse(data_string); return JSON.stringify(parsed, null, 2); } catch (e) { return data_string; } } // Toggle expand/collapse state _toggle_data_cell(cell) { const is_expanded = cell.attr("expanded") !== null; const full_data = cell.attr("data-full"); // Find the text wrapper span const text_wrapper = cell.first_child().next_sibling(); if (is_expanded) { // Collapse: show first line only const first_line = this._get_first_line(full_data); text_wrapper.text(first_line); cell.attr("expanded", null); } else { // Expand: show formatted JSON const formatted = this._format_json(full_data); text_wrapper.text(formatted); cell.attr("expanded", ""); } } // Format dependency data based on its structure _format_dependency(dep_array) { const type = dep_array[0]; const formatted = {}; // Common patterns based on the example data: // Type 2 (Function): [type, name, array, hash] // Type 4 (Package): [type, path, hash] // Type 5 (ConsoleVariable): [type, bool, array, hash] // Type 8 (NativeClass): [type, path, hash] // Type 9 (AssetRegistryQuery): [type, bool, object, hash] // Type 10 (RedirectionTarget): [type, path, hash] if (dep_array.length > 1) { // Most types have a name/path as second element if (typeof dep_array[1] === "string") { formatted.Name = dep_array[1]; } else if (typeof dep_array[1] === "boolean") { formatted.Value = dep_array[1].toString(); } } if (dep_array.length > 2) { // Third element varies if (Array.isArray(dep_array[2])) { formatted.Data = JSON.stringify(dep_array[2]); } else if (typeof dep_array[2] === "object") { formatted.Data = JSON.stringify(dep_array[2]); } else if (typeof dep_array[2] === "string") { formatted.Hash = dep_array[2]; } } if (dep_array.length > 3) { // Fourth element is usually the hash if (typeof dep_array[3] === "string") { formatted.Hash = dep_array[3]; } } return formatted; } async _build_page() { const project = this.get_param("project"); const oplog = this.get_param("oplog"); const opkey = this.get_param("opkey"); const artifact_hash = this.get_param("hash"); // Build page title let title = "Cook Artifacts"; if (this._entry) { try { const entry = await this._entry; const entry_obj = entry.as_object().find("entry").as_object(); const key = entry_obj.find("key").as_value(); title = `Cook Artifacts`; } catch (e) { console.error("Failed to fetch entry:", e); } } const section = this.add_section(title); // Fetch and parse artifact let artifact; try { artifact = await this._artifact; } catch (e) { section.text(`Failed to load artifact: ${e.message}`); return; } // Display artifact info const info_section = section.add_section("Artifact Info"); const info_table = info_section.add_widget(Table, ["Property", "Value"], Table.Flag_PackRight); if (artifact.Version !== undefined) info_table.add_row("Version", artifact.Version.toString()); if (artifact.HasSaveResults !== undefined) info_table.add_row("HasSaveResults", artifact.HasSaveResults.toString()); if (artifact.PackageSavedHash !== undefined) info_table.add_row("PackageSavedHash", artifact.PackageSavedHash); // Process SaveBuildDependencies if (artifact.SaveBuildDependencies && artifact.SaveBuildDependencies.Dependencies) { this._build_dependency_section( section, "Save Build Dependencies", artifact.SaveBuildDependencies.Dependencies, artifact.SaveBuildDependencies.StoredKey ); } // Process LoadBuildDependencies if (artifact.LoadBuildDependencies && artifact.LoadBuildDependencies.Dependencies) { this._build_dependency_section( section, "Load Build Dependencies", artifact.LoadBuildDependencies.Dependencies, artifact.LoadBuildDependencies.StoredKey ); } // Process RuntimeDependencies if (artifact.RuntimeDependencies && artifact.RuntimeDependencies.length > 0) { const runtime_section = section.add_section("Runtime Dependencies"); const runtime_table = runtime_section.add_widget(Table, ["Path"], Table.Flag_PackRight); for (const dep of artifact.RuntimeDependencies) { const row = runtime_table.add_row(dep); // Make Path clickable to navigate to entry if (this._should_link_dependency(dep)) { row.get_cell(0).text(dep).on_click((opkey) => { window.location = `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey.toLowerCase()}`; }, dep); } } } } _should_link_dependency(name) { // Exclude dependencies starting with /Script/ (code-defined entries) - case insensitive if (name && name.toLowerCase().startsWith("/script/")) return false; return true; } _build_dependency_section(parent_section, title, dependencies, stored_key) { const section = parent_section.add_section(title); // Add stored key info if (stored_key) { const key_toolbar = section.add_widget(Toolbar); key_toolbar.left().add(`Key: ${stored_key}`); } // Group dependencies by type const dependencies_by_type = {}; for (const dep_array of dependencies) { if (!Array.isArray(dep_array) || dep_array.length === 0) continue; const type = dep_array[0]; if (!dependencies_by_type[type]) dependencies_by_type[type] = []; dependencies_by_type[type].push(this._format_dependency(dep_array)); } // Sort types numerically const sorted_types = Object.keys(dependencies_by_type).map(Number).sort((a, b) => a - b); for (const type_value of sorted_types) { const type_name = this._get_dependency_type_name(type_value); const deps = dependencies_by_type[type_value]; const type_section = section.add_section(type_name); // Determine columns based on available fields const all_fields = new Set(); for (const dep of deps) { for (const field in dep) all_fields.add(field); } let columns = Array.from(all_fields); // Remove Hash column for RedirectionTarget as it's not useful if (type_value === 10) { columns = columns.filter(col => col !== "Hash"); } if (columns.length === 0) { type_section.text("No data fields"); continue; } // Create table with dynamic columns const table = type_section.add_widget(Table, columns, Table.Flag_PackRight); // Check if this type should have clickable Name links const should_link = (type_value === 3 || type_value === 4 || type_value === 10); const name_col_index = columns.indexOf("Name"); for (const dep of deps) { const row_values = columns.map(col => dep[col] || ""); const row = table.add_row(...row_values); // Make Name field clickable for Package, TransitiveBuild, and RedirectionTarget if (should_link && name_col_index >= 0 && dep.Name && this._should_link_dependency(dep.Name)) { const project = this.get_param("project"); const oplog = this.get_param("oplog"); row.get_cell(name_col_index).text(dep.Name).on_click((opkey) => { window.location = `?page=entry&project=${project}&oplog=${oplog}&opkey=${opkey.toLowerCase()}`; }, dep.Name); } // Make Data field expandable/collapsible if needed const data_col_index = columns.indexOf("Data"); if (data_col_index >= 0 && dep.Data) { const data_cell = row.get_cell(data_col_index); if (this._should_make_expandable(dep.Data)) { // Store full data in attribute data_cell.attr("data-full", dep.Data); // Clear the cell and rebuild with icon + text data_cell.inner().innerHTML = ""; // Create expand/collapse icon const icon = data_cell.tag("span").classify("zen_expand_icon").text("+"); icon.on_click(() => { this._toggle_data_cell(data_cell); // Update icon text const is_expanded = data_cell.attr("expanded") !== null; icon.text(is_expanded ? "-" : "+"); }); // Add text content wrapper const text_wrapper = data_cell.tag("span").classify("zen_data_text"); const first_line = this._get_first_line(dep.Data); text_wrapper.text(first_line); // Store reference to text wrapper for updates data_cell.attr("data-text-wrapper", "true"); } } } } } }