/* * Copyright 2025 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* https://docs.moderne.io/licensing/moderne-source-available-license *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as rpc from "vscode-jsonrpc/node"; import {withMetrics0} from "./metrics"; import {CategoryDescriptor, RecipeMarketplace} from "../../marketplace"; import {RecipeDescriptor} from "../../recipe"; export interface GetMarketplaceResponseRow { readonly descriptor: RecipeDescriptor readonly categoryPaths: CategoryDescriptor[][] /** * The package this recipe was contributed by, recorded at install time. Lets the host attribute * each row to its own bundle instead of force-tagging every row with the one requested bundle. * Undefined for recipes installed from a local path (no package identity). */ readonly packageName?: string } /** * Converts GetMarketplace response rows into a hydrated RecipeMarketplace. * This is used by the RPC client to reconstruct the marketplace structure. */ export async function toMarketplace(rows: GetMarketplaceResponseRow[]): Promise { const marketplace = new RecipeMarketplace(); for (const row of rows) { for (const categoryPath of row.categoryPaths) { await marketplace.root.install(row.descriptor, categoryPath); } } return marketplace; } export class GetMarketplace { static handle(connection: rpc.MessageConnection, marketplace: RecipeMarketplace, recipeOrigin: Map, metricsCsv?: string): void { connection.onRequest( new rpc.RequestType0("GetMarketplace"), withMetrics0( "GetMarketplace", metricsCsv, (context) => async () => { // Group recipes by name, collecting all category paths for each const rowByRecipeId = new Map(); function collectRecipes(category: RecipeMarketplace.Category, categoryPath: CategoryDescriptor[]): void { const currentPath = [...categoryPath, category.descriptor]; // Add all recipes in this category for (const recipe of category.recipes.keys()) { const existing = rowByRecipeId.get(recipe.name); if (existing) { existing.categoryPaths.push(currentPath); } else { rowByRecipeId.set(recipe.name, { descriptor: recipe, categoryPaths: [currentPath], packageName: recipeOrigin.get(recipe.name) }); } } // Recursively process subcategories for (const subcategory of category.categories) { collectRecipes(subcategory, currentPath); } } // Start from each top-level category (skipping the root) for (const category of marketplace.categories()) { collectRecipes(category, []); } context.target = ''; return Array.from(rowByRecipeId.values()); } ) ); } }