diff --git a/druid/editor_scripts/core/asset_store.lua b/druid/editor_scripts/core/asset_store.lua index be492c0..cb4c5e0 100644 --- a/druid/editor_scripts/core/asset_store.lua +++ b/druid/editor_scripts/core/asset_store.lua @@ -42,12 +42,13 @@ end ---Handle widget installation ---@param item table - Widget item to install ---@param install_folder string - Installation folder +---@param all_items table - List of all widgets for dependency resolution ---@param on_success function - Success callback ---@param on_error function - Error callback -local function handle_install(item, install_folder, on_success, on_error) +local function handle_install(item, install_folder, all_items, on_success, on_error) print("Installing widget:", item.id) - local success, message = installer.install_widget(item, install_folder) + local success, message = installer.install_widget(item, install_folder, all_items) if success then print("Installation successful:", message) @@ -115,7 +116,7 @@ function M.open_asset_store(store_url) -- Installation handlers local function on_install(item) - handle_install(item, install_folder, + handle_install(item, install_folder, all_items, function(message) set_install_status("Success: " .. message) end, diff --git a/druid/editor_scripts/core/installer.lua b/druid/editor_scripts/core/installer.lua index 9e6ea05..e339331 100644 --- a/druid/editor_scripts/core/installer.lua +++ b/druid/editor_scripts/core/installer.lua @@ -42,19 +42,115 @@ local function download_file_zip_json(url) end +---Find widget by dependency string (format: "author:widget_id@version" or "author@widget_id" or "widget_id") +---@param dep_string string - Dependency string +---@param all_items table - List of all available widgets +---@return table|nil - Found widget item or nil +local function find_widget_by_dependency(dep_string, all_items) + if not dep_string or not all_items then + return nil + end + + local author, widget_id + + -- Try format: "author:widget_id@version" (e.g., "Insality:mini_graph@1") + author, widget_id = dep_string:match("^([^:]+):([^@]+)@") + if not author then + -- Try format: "author@widget_id" (e.g., "insality@mini_graph") + author, widget_id = dep_string:match("^([^@]+)@(.+)$") + if not author then + -- No author specified, search by widget_id only + widget_id = dep_string + end + end + + for _, item in ipairs(all_items) do + if item.id == widget_id then + -- If author was specified, check it matches (case-insensitive) + if not author or string.lower(item.author or "") == string.lower(author) then + return item + end + end + end + + return nil +end + + +---Install widget dependencies recursively +---@param item druid.core.item_info - Widget item +---@param all_items table - List of all available widgets +---@param install_folder string - Installation folder +---@param installing_set table - Set of widget IDs currently being installed (to prevent cycles) +---@return boolean, string|nil - Success status and message +local function install_dependencies(item, all_items, install_folder, installing_set) + if not item.depends or #item.depends == 0 then + return true, nil + end + + installing_set = installing_set or {} + + for _, dep_string in ipairs(item.depends) do + local dep_item = find_widget_by_dependency(dep_string, all_items) + if not dep_item then + print("Warning: Dependency not found:", dep_string) + -- Continue with other dependencies + else + -- Check if already installed + if M.is_widget_installed(dep_item, install_folder) then + print("Dependency already installed:", dep_item.id) + else + -- Check for circular dependencies + if installing_set[dep_item.id] then + print("Warning: Circular dependency detected:", dep_item.id) + -- Continue with other dependencies + else + print("Installing dependency:", dep_item.id) + local success, err = M.install_widget(dep_item, install_folder, all_items, installing_set) + if not success then + return false, "Failed to install dependency " .. dep_item.id .. ": " .. (err or "unknown error") + end + end + end + end + end + + return true, nil +end + + ---Install a widget from a zip URL ---@param item druid.core.item_info - Widget item data containing zip_url and id ---@param install_folder string - Target folder to install to +---@param all_items table|nil - Optional list of all widgets for dependency resolution +---@param installing_set table|nil - Optional set of widget IDs currently being installed (to prevent cycles) ---@return boolean, string - Success status and message -function M.install_widget(item, install_folder) +function M.install_widget(item, install_folder, all_items, installing_set) if not item.json_zip_url or not item.id then - return false, "Invalid widget data: missing zip_url or id" + return false, "Invalid widget data: missing json_zip_url or id" + end + + -- Install dependencies first if all_items is provided + if all_items then + installing_set = installing_set or {} + if installing_set[item.id] then + return false, "Circular dependency detected: " .. item.id + end + installing_set[item.id] = true + local dep_success, dep_err = install_dependencies(item, all_items, install_folder, installing_set) + if not dep_success then + installing_set[item.id] = nil + return false, dep_err or "Failed to install dependencies" + end end -- Download the zip file local zip_data, filename, content_list = download_file_zip_json(item.json_zip_url) if not zip_data or not filename then - return false, "Failed to download widget: " .. filename + if installing_set then + installing_set[item.id] = nil + end + return false, "Failed to download widget: " .. (filename or "unknown error") end if content_list then @@ -67,6 +163,9 @@ function M.install_widget(item, install_folder) local zip_file_path = "." .. install_folder .. "/" .. filename local zip_file = io.open(zip_file_path, "wb") if not zip_file then + if installing_set then + installing_set[item.id] = nil + end print("Directory does not exist: " .. install_folder) print("Please create the directory manually and try again.") return false, "Directory does not exist: " .. install_folder @@ -97,6 +196,10 @@ function M.install_widget(item, install_folder) print("Warning: No file list available, skipping path replacement") end + if installing_set then + installing_set[item.id] = nil + end + return true, "Widget installed successfully" end diff --git a/druid/editor_scripts/core/path_replacer.lua b/druid/editor_scripts/core/path_replacer.lua index 53e5947..1d0add6 100644 --- a/druid/editor_scripts/core/path_replacer.lua +++ b/druid/editor_scripts/core/path_replacer.lua @@ -6,29 +6,15 @@ local system = require("druid.editor_scripts.defold_parser.system.parser_interna local M = {} ----Detect the original path structure from item data ----Builds widget/author/widget_id path ----@param author string|nil - Author name ----@param widget_id string - Widget ID ----@return string|nil - Original path prefix (e.g., "widget/Insality/fps_panel") or nil -local function detect_original_path_from_item(author, widget_id) - if not author or not widget_id then - return nil - end - - local original_path = "widget/" .. author .. "/" .. widget_id - print("Detected original path from item:", original_path) - return original_path -end ---Replace paths in file content ---@param content string - File content ----@param original_path string - Original path to replace (e.g., "widget/Insality/fps_panel") ----@param target_path string - Target path (e.g., "widget/fps_panel") +---@param author string - Author name (e.g., "Insality") +---@param install_folder string - Installation folder (e.g., "widget") ---@return string - Modified content -local function replace_paths_in_content(content, original_path, target_path) - if not content or not original_path or not target_path then +local function replace_paths_in_content(content, author, install_folder) + if not content or not author then return content end @@ -37,15 +23,28 @@ local function replace_paths_in_content(content, original_path, target_path) return str:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") end - -- Replace paths with forward slashes: widget/Insality/fps_panel -> widget/fps_panel - local escaped_original = escape_pattern(original_path) - content = content:gsub(escaped_original, target_path) + -- Remove leading / from install_folder if present + local clean_install_folder = install_folder + if clean_install_folder:sub(1, 1) == "/" then + clean_install_folder = clean_install_folder:sub(2) + end - -- Replace require statements with dots: widget.Insality.fps_panel -> widget.fps_panel - local original_dots = original_path:gsub("/", ".") - local target_dots = target_path:gsub("/", ".") - local escaped_original_dots = escape_pattern(original_dots) - content = content:gsub(escaped_original_dots, target_dots) + -- Replace all paths with author: widget/Insality/* -> widget/* + local author_path_pattern = escape_pattern(clean_install_folder .. "/" .. author .. "/") + local target_path_prefix = clean_install_folder .. "/" + content = content:gsub(author_path_pattern, target_path_prefix) + + -- Replace all require statements with dots: widget.Insality.* -> widget.* + local author_dots_pattern = escape_pattern(clean_install_folder .. "." .. author .. ".") + local target_dots_prefix = clean_install_folder .. "." + content = content:gsub(author_dots_pattern, target_dots_prefix) + + -- Also replace paths that start with author directly: Insality/widget -> widget + -- But only if they're in require statements or paths + local author_start_pattern = escape_pattern(author .. "/") + content = content:gsub(author_start_pattern, "") + local author_start_dots_pattern = escape_pattern(author .. ".") + content = content:gsub(author_start_dots_pattern, "") return content end @@ -61,31 +60,12 @@ end function M.process_widget_paths(folder_path, install_folder, widget_id, author, file_list) print("Processing widget paths in:", folder_path) - -- Detect original path structure from item data - local original_path = detect_original_path_from_item(author, widget_id) - - if not original_path then - print("Warning: Could not detect original path structure (missing author or widget_id), skipping path replacement") + if not author then + print("Warning: Missing author, skipping path replacement") return true, nil end - -- Construct target path - -- Remove leading / from install_folder if present - local clean_install_folder = install_folder - if clean_install_folder:sub(1, 1) == "/" then - clean_install_folder = clean_install_folder:sub(2) - end - - local target_path = clean_install_folder - if widget_id then - if target_path ~= "" then - target_path = target_path .. "/" .. widget_id - else - target_path = widget_id - end - end - - print("Replacing paths from:", original_path, "to:", target_path) + print("Replacing all paths with author:", author, "in install folder:", install_folder) -- Get absolute project path local absolute_project_path = editor.external_file_attributes(".").path @@ -120,8 +100,8 @@ function M.process_widget_paths(folder_path, install_folder, widget_id, author, if not content then print("Warning: Could not read file:", file_path, err) else - -- Replace paths - local modified_content = replace_paths_in_content(content, original_path, target_path) + -- Replace all paths with author + local modified_content = replace_paths_in_content(content, author, install_folder) if modified_content ~= content then -- Write modified content back local success, write_err = system.write_file(absolute_file_path, modified_content) @@ -141,4 +121,3 @@ end return M -