mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-17 11:23:27 +00:00
Adding logic to render external plugins with canvases in the canvas gallery (#2323)
* Adding logic to render external plugins with canvases in the canvas gallery * Fix external canvas plugin URL encoding and keyword detection Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: db85480e-d839-4f69-8271-08f8cc845596 * Fail fast on external plugin errors and fix external install links Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: db85480e-d839-4f69-8271-08f8cc845596
This commit is contained in:
@@ -25,12 +25,15 @@ import {
|
||||
parseSkillMetadata,
|
||||
parseYamlFile,
|
||||
} from "./yaml-parser.mjs";
|
||||
import { readExternalPlugins } from "./external-plugin-validation.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
const WEBSITE_DIR = path.join(ROOT_FOLDER, "website");
|
||||
const WEBSITE_DATA_DIR = path.join(WEBSITE_DIR, "public", "data");
|
||||
const WEBSITE_SOURCE_DATA_DIR = path.join(WEBSITE_DIR, "data");
|
||||
const EXTERNAL_CANVAS_KEYWORD = "canvas";
|
||||
const EXTERNAL_CANVAS_PREVIEW_PATH = "assets/preview.png";
|
||||
|
||||
/**
|
||||
* Ensure the output directory exists
|
||||
@@ -97,6 +100,23 @@ function normalizeText(value, fallback = "") {
|
||||
return typeof value === "string" ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
function normalizeRepoRelativePath(value) {
|
||||
const normalized = normalizeText(value);
|
||||
if (!normalized || normalized === "/") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return normalized.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function joinRepoPath(...segments) {
|
||||
return segments
|
||||
.map((segment) => String(segment ?? "").trim())
|
||||
.filter(Boolean)
|
||||
.join("/")
|
||||
.replace(/\/+/g, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize an author value (npm string form or { name, url } object) to
|
||||
* { name, url? } | null. Returns null when no usable name is present.
|
||||
@@ -1055,6 +1075,67 @@ function normalizeExternalScreenshotRole(value, ref) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildExternalRepoImageUrl(repo, locator, assetPath) {
|
||||
if (!repo || !locator || !assetPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encodedLocator = locator
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
const encodedPath = assetPath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
return `https://raw.githubusercontent.com/${repo}/${encodedLocator}/${encodedPath}`;
|
||||
}
|
||||
|
||||
function buildExternalRepoTreeUrl(repo, locator, pluginRoot) {
|
||||
if (!repo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (locator) {
|
||||
const treePath = normalizeRepoRelativePath(pluginRoot);
|
||||
const encodedLocator = locator
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
const encodedTreePath = treePath
|
||||
? treePath
|
||||
.split("/")
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
: null;
|
||||
const suffix = encodedTreePath ? `/${encodedTreePath}` : "";
|
||||
return `https://github.com/${repo}/tree/${encodedLocator}${suffix}`;
|
||||
}
|
||||
|
||||
return `https://github.com/${repo}`;
|
||||
}
|
||||
|
||||
function hasCanvasKeyword(plugin) {
|
||||
return normalizeExternalKeywords(plugin).some(
|
||||
(keyword) => normalizeText(keyword).toLowerCase() === EXTERNAL_CANVAS_KEYWORD
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeExternalKeywords(plugin) {
|
||||
const source = Array.isArray(plugin?.keywords)
|
||||
? plugin.keywords
|
||||
: Array.isArray(plugin?.tags)
|
||||
? plugin.tags
|
||||
: [];
|
||||
|
||||
return [...new Set(
|
||||
source
|
||||
.filter((keyword) => typeof keyword === "string")
|
||||
.map((keyword) => keyword.trim())
|
||||
.filter(Boolean)
|
||||
)].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function normalizeExtensionScreenshotRole(value, relPath, ref) {
|
||||
if (!value) return null;
|
||||
if (typeof value === "string") {
|
||||
@@ -1320,6 +1401,93 @@ function generateCanvasManifest(gitDates, commitSha) {
|
||||
}
|
||||
}
|
||||
|
||||
const seenExtensionIds = new Set(items.map((item) => String(item.id).toLowerCase()));
|
||||
const {
|
||||
plugins: externalPlugins,
|
||||
errors: externalPluginErrors,
|
||||
warnings: externalPluginWarnings,
|
||||
} = readExternalPlugins({ policy: "marketplace" });
|
||||
externalPluginWarnings.forEach((warning) => console.warn(`Warning: ${warning}`));
|
||||
if (externalPluginErrors.length > 0) {
|
||||
externalPluginErrors.forEach((error) => console.error(`Error: ${error}`));
|
||||
throw new Error("External plugin validation failed");
|
||||
}
|
||||
|
||||
for (const ext of externalPlugins) {
|
||||
if (!hasCanvasKeyword(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = normalizeText(ext?.name);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const displayName = formatDisplayName(name);
|
||||
|
||||
const id = normalizeText(ext?.name).toLowerCase().replace(/\s+/g, "-");
|
||||
if (seenExtensionIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const source = ext?.source;
|
||||
if (source?.source !== "github" || !normalizeText(source?.repo)) {
|
||||
console.warn(`Warning: skipping external canvas "${name}" due to missing GitHub source`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const locator = normalizeText(source.sha) || normalizeText(source.ref);
|
||||
if (!locator) {
|
||||
console.warn(`Warning: skipping external canvas "${name}" because source.sha or source.ref is required`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const pluginRoot = normalizeRepoRelativePath(source.path);
|
||||
const previewPath = joinRepoPath(pluginRoot, EXTERNAL_CANVAS_PREVIEW_PATH);
|
||||
const imageUrl = buildExternalRepoImageUrl(source.repo, locator, previewPath);
|
||||
const sourceUrl = buildExternalRepoTreeUrl(source.repo, locator, pluginRoot);
|
||||
const externalSource = normalizeText(source.repo);
|
||||
const keywords = normalizeExternalKeywords(ext);
|
||||
|
||||
items.push({
|
||||
id,
|
||||
canvasId: id,
|
||||
extensionId: id,
|
||||
extensionName: name,
|
||||
pluginName: null,
|
||||
name: displayName,
|
||||
version: normalizeText(ext?.version, "1.0.0"),
|
||||
readmeFile: null,
|
||||
description: normalizeText(ext?.description, "External canvas extension"),
|
||||
path: null,
|
||||
ref: null,
|
||||
lastUpdated: null,
|
||||
screenshots: {
|
||||
icon: imageUrl
|
||||
? {
|
||||
path: imageUrl,
|
||||
type: getImageMimeType(EXTERNAL_CANVAS_PREVIEW_PATH),
|
||||
}
|
||||
: null,
|
||||
gallery: imageUrl
|
||||
? {
|
||||
path: imageUrl,
|
||||
type: getImageMimeType(EXTERNAL_CANVAS_PREVIEW_PATH),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
imageUrl,
|
||||
assetPath: null,
|
||||
installUrl: null,
|
||||
installCommand: null,
|
||||
sourceUrl,
|
||||
externalSource,
|
||||
external: true,
|
||||
author: normalizeAuthor(ext?.author),
|
||||
keywords,
|
||||
});
|
||||
seenExtensionIds.add(id);
|
||||
}
|
||||
|
||||
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const keywordFilters = [...new Set(sortedItems.flatMap((item) => item.keywords || []))]
|
||||
.filter(Boolean)
|
||||
|
||||
Reference in New Issue
Block a user