chore: publish from staged

This commit is contained in:
github-actions[bot]
2026-06-17 05:28:37 +00:00
parent 97205f47ef
commit 2512d7d728
29 changed files with 923 additions and 89 deletions
+452 -69
View File
@@ -674,11 +674,33 @@ function generatePluginsData(gitDates) {
/**
* Generate canvas extensions metadata
*/
function getExtensionAssetInfo(extensionDir, relPath, ref) {
function getImageMimeType(filePath) {
const extension = path.extname(filePath).toLowerCase();
const mimeByExtension = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".gif": "image/gif",
};
return mimeByExtension[extension] || "application/octet-stream";
}
function resolveImageUrl(value, ref) {
const normalized = normalizeText(value);
if (!normalized) return null;
if (/^https?:\/\//i.test(normalized)) {
return normalized;
}
const repoPath = normalized.replace(/\\/g, "/").replace(/^\/+/, "");
return buildRepoImageUrl(repoPath, ref);
}
function getImageAssetFiles(extensionDir) {
const assetDir = path.join(extensionDir, "assets");
if (!fs.existsSync(assetDir)) {
return null;
return [];
}
const imageExtensions = new Set([
@@ -689,7 +711,35 @@ function getExtensionAssetInfo(extensionDir, relPath, ref) {
".gif",
]);
const preferredNames = [
return fs
.readdirSync(assetDir)
.filter((file) => imageExtensions.has(path.extname(file).toLowerCase()))
.sort((a, b) => a.localeCompare(b));
}
function pickAssetFile(files, preferredNames) {
const preferredLookup = new Set(preferredNames.map((name) => name.toLowerCase()));
for (const file of files) {
if (preferredLookup.has(file.toLowerCase())) {
return file;
}
}
return files[0] || null;
}
function getExtensionAssetInfo(extensionDir, relPath, ref) {
const files = getImageAssetFiles(extensionDir);
if (files.length === 0) {
return null;
}
const iconAsset = pickAssetFile(files, [
"icon.png",
"icon.jpg",
"icon.jpeg",
"icon.webp",
"icon.gif",
"preview.png",
"preview.jpg",
"preview.jpeg",
@@ -705,34 +755,52 @@ function getExtensionAssetInfo(extensionDir, relPath, ref) {
"image.jpeg",
"image.webp",
"image.gif",
];
]);
const galleryAsset = pickAssetFile(files, [
"gallery.png",
"gallery.jpg",
"gallery.jpeg",
"gallery.webp",
"gallery.gif",
"preview.png",
"preview.jpg",
"preview.jpeg",
"preview.webp",
"preview.gif",
"screenshot.png",
"screenshot.jpg",
"screenshot.jpeg",
"screenshot.webp",
"screenshot.gif",
"image.png",
"image.jpg",
"image.jpeg",
"image.webp",
"image.gif",
]);
for (const candidate of preferredNames) {
const candidatePath = path.join(assetDir, candidate);
if (fs.existsSync(candidatePath)) {
const assetPath = `${relPath}/assets/${candidate}`;
return {
assetPath,
imageUrl: buildRepoImageUrl(assetPath, ref),
};
}
}
const files = fs
.readdirSync(assetDir)
.filter((file) => imageExtensions.has(path.extname(file).toLowerCase()))
.sort((a, b) => a.localeCompare(b));
if (files.length === 0) {
return null;
}
const assetFile = files[0];
const assetPath = `${relPath}/assets/${assetFile}`;
const iconFile = iconAsset || galleryAsset;
const galleryFile = galleryAsset || iconAsset;
const iconPath = iconFile ? `${relPath}/assets/${iconFile}` : null;
const galleryPath = galleryFile ? `${relPath}/assets/${galleryFile}` : null;
return {
assetPath,
imageUrl: buildRepoImageUrl(assetPath, ref),
screenshots: {
icon: iconPath
? {
path: iconPath,
type: getImageMimeType(iconPath),
}
: null,
gallery: galleryPath
? {
path: galleryPath,
type: getImageMimeType(galleryPath),
}
: null,
},
assetPath: iconPath,
imageUrl: iconPath ? buildRepoImageUrl(iconPath, ref) : null,
};
}
@@ -744,11 +812,174 @@ function buildRepoImageUrl(assetPath, ref) {
return `https://raw.githubusercontent.com/github/awesome-copilot/${ref}/${encodedAssetPath}`;
}
function generateExtensionsData(gitDates, commitSha) {
const extensions = [];
function extractCanvasMetadataFromSource(source) {
const constants = new Map();
const constantPattern =
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|`([^`$]*)`)\s*;/g;
let constantMatch = constantPattern.exec(source);
while (constantMatch) {
const key = constantMatch[1];
const value = constantMatch[2] ?? constantMatch[3] ?? constantMatch[4] ?? "";
constants.set(key, value.replace(/\\n/g, "\n").trim());
constantMatch = constantPattern.exec(source);
}
function resolveExpression(expr) {
const trimmed = normalizeText(expr);
if (!trimmed) return null;
if (
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))
) {
return trimmed
.slice(1, -1)
.replace(/\\n/g, "\n")
.replace(/\\"/g, '"')
.replace(/\\'/g, "'");
}
if (trimmed.startsWith("`") && trimmed.endsWith("`") && !trimmed.includes("${")) {
return trimmed.slice(1, -1);
}
return constants.get(trimmed) || null;
}
function findMatchingBrace(startIndex) {
let depth = 0;
let inSingle = false;
let inDouble = false;
let inTemplate = false;
let escaped = false;
for (let i = startIndex; i < source.length; i++) {
const char = source[i];
if (escaped) {
escaped = false;
continue;
}
if (char === "\\") {
escaped = true;
continue;
}
if (!inDouble && !inTemplate && char === "'" && !inSingle) {
inSingle = true;
continue;
}
if (inSingle && char === "'") {
inSingle = false;
continue;
}
if (!inSingle && !inTemplate && char === '"' && !inDouble) {
inDouble = true;
continue;
}
if (inDouble && char === '"') {
inDouble = false;
continue;
}
if (!inSingle && !inDouble && char === "`" && !inTemplate) {
inTemplate = true;
continue;
}
if (inTemplate && char === "`") {
inTemplate = false;
continue;
}
if (inSingle || inDouble || inTemplate) {
continue;
}
if (char === "{") depth++;
if (char === "}") {
depth--;
if (depth === 0) return i;
}
}
return -1;
}
function readProp(head, key) {
const pattern = new RegExp(`\\b${key}\\s*:\\s*([^,\\n]+)`);
const match = pattern.exec(head);
return resolveExpression(match?.[1]);
}
const canvases = [];
let cursor = 0;
while (cursor < source.length) {
const createCanvasIndex = source.indexOf("createCanvas(", cursor);
if (createCanvasIndex === -1) {
break;
}
const objectStart = source.indexOf("{", createCanvasIndex);
if (objectStart === -1) {
break;
}
const objectEnd = findMatchingBrace(objectStart);
if (objectEnd === -1) {
break;
}
const objectContent = source.slice(objectStart + 1, objectEnd);
const header = objectContent.slice(0, 1400);
const id = readProp(header, "id");
const displayName = readProp(header, "displayName");
const description = readProp(header, "description");
if (id || displayName || description) {
canvases.push({
id: id || null,
displayName: displayName || null,
description: description || null,
});
}
cursor = objectEnd + 1;
}
return canvases;
}
function getExtensionCanvasFiles(extensionDir) {
const queue = [extensionDir];
const files = [];
while (queue.length > 0) {
const currentDir = queue.shift();
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const absolutePath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
queue.push(absolutePath);
} else if (entry.isFile() && entry.name.endsWith(".mjs")) {
files.push(absolutePath);
}
}
}
return files.sort((a, b) => a.localeCompare(b));
}
function normalizeExternalScreenshotRole(value, ref) {
if (!value) return null;
if (typeof value === "string") {
const type = getImageMimeType(value);
return {
path: value.replace(/\\/g, "/"),
type,
imageUrl: resolveImageUrl(value, ref),
};
}
const pathValue = normalizeText(value.path);
const urlValue = normalizeText(value.url);
if (!pathValue && !urlValue) return null;
const imagePath = pathValue ? pathValue.replace(/\\/g, "/") : null;
const type = normalizeText(value.type) || getImageMimeType(imagePath || urlValue);
const imageUrl = resolveImageUrl(urlValue || imagePath, ref);
return {
path: imagePath,
type,
imageUrl,
};
}
function generateCanvasManifest(gitDates, commitSha) {
const items = [];
if (!fs.existsSync(EXTENSIONS_DIR)) {
return { items: [] };
return { items: [], filters: { keywords: [] } };
}
const extensionDirs = fs
@@ -761,32 +992,61 @@ function generateExtensionsData(gitDates, commitSha) {
"extension.mjs"
);
return fs.existsSync(extensionEntryPoint);
});
})
.sort((a, b) => a.name.localeCompare(b.name));
for (const dir of extensionDirs) {
const relPath = `extensions/${dir.name}`;
const assetInfo = getExtensionAssetInfo(
path.join(EXTENSIONS_DIR, dir.name),
relPath,
commitSha
);
const extensionDir = path.join(EXTENSIONS_DIR, dir.name);
const packageJsonPath = path.join(extensionDir, "package.json");
const packageJson = fs.existsSync(packageJsonPath)
? JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
: {};
const keywords = Array.isArray(packageJson.keywords)
? [...new Set(packageJson.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
: [];
const extensionDescription = normalizeText(packageJson.description, "Canvas extension");
const extensionName = normalizeText(packageJson.name, dir.name);
const extensionVersion = normalizeText(packageJson.version, "1.0.0");
const screenshots = getExtensionAssetInfo(extensionDir, relPath, commitSha);
const canvasFiles = getExtensionCanvasFiles(extensionDir);
const canvases = [];
for (const canvasFile of canvasFiles) {
const source = fs.readFileSync(canvasFile, "utf-8");
canvases.push(...extractCanvasMetadataFromSource(source));
}
const canvasEntries = canvases.length > 0
? canvases
: [{ id: dir.name, displayName: formatDisplayName(dir.name), description: extensionDescription }];
const installUrl = `https://github.com/github/awesome-copilot/tree/${commitSha}/${relPath.replace(
/\\/g,
"/"
)}`;
extensions.push({
id: dir.name,
name: formatDisplayName(dir.name),
description: "Canvas extension",
path: relPath,
ref: commitSha,
lastUpdated: getDirectoryLastUpdated(gitDates, relPath),
imageUrl: assetInfo?.imageUrl || null,
assetPath: assetInfo?.assetPath || null,
installUrl: `https://github.com/github/awesome-copilot/tree/${commitSha}/${relPath.replace(
/\\/g,
"/"
)}`,
sourceUrl: null,
external: false,
});
for (const canvas of canvasEntries) {
const canvasId = normalizeText(canvas.id, dir.name);
const canvasName = normalizeText(canvas.displayName, formatDisplayName(canvasId));
const canvasDescription = normalizeText(extensionDescription, canvas.description);
items.push({
id: canvasId,
canvasId,
extensionId: dir.name,
extensionName,
name: canvasName,
version: extensionVersion,
description: canvasDescription,
path: relPath,
ref: commitSha,
lastUpdated: getDirectoryLastUpdated(gitDates, relPath),
screenshots: screenshots?.screenshots || { icon: null, gallery: null },
imageUrl: screenshots?.imageUrl || null,
assetPath: screenshots?.assetPath || null,
installUrl,
sourceUrl: null,
external: false,
keywords,
});
}
}
const externalJsonPath = path.join(EXTENSIONS_DIR, "external.json");
@@ -805,27 +1065,58 @@ function generateExtensionsData(gitDates, commitSha) {
}
const id = normalizeText(ext?.id || name.toLowerCase().replace(/\s+/g, "-"));
let imageUrl = normalizeText(ext?.imageUrl);
let assetPath = null;
const imagePath = normalizeText(ext?.imagePath);
if (!imageUrl && imagePath) {
const repoAssetPath = imagePath.replace(/\\/g, "/");
imageUrl = buildRepoImageUrl(repoAssetPath, commitSha);
assetPath = repoAssetPath;
}
const keywords = Array.isArray(ext?.keywords)
? [...new Set(ext.keywords.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
: Array.isArray(ext?.tags)
? [...new Set(ext.tags.filter((keyword) => typeof keyword === "string").map((keyword) => keyword.trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b))
: [];
const iconScreenshot =
normalizeExternalScreenshotRole(ext?.screenshots?.icon, commitSha) ||
normalizeExternalScreenshotRole(ext?.iconPath, commitSha) ||
normalizeExternalScreenshotRole(ext?.imagePath, commitSha) ||
normalizeExternalScreenshotRole(ext?.iconUrl, commitSha) ||
normalizeExternalScreenshotRole(ext?.imageUrl, commitSha);
const galleryScreenshot =
normalizeExternalScreenshotRole(ext?.screenshots?.gallery, commitSha) ||
normalizeExternalScreenshotRole(ext?.galleryPath, commitSha) ||
normalizeExternalScreenshotRole(ext?.galleryUrl, commitSha) ||
iconScreenshot;
const screenshots = {
icon: iconScreenshot
? {
path: iconScreenshot.path,
type: iconScreenshot.type,
}
: null,
gallery: galleryScreenshot
? {
path: galleryScreenshot.path,
type: galleryScreenshot.type,
}
: null,
};
const imageUrl = iconScreenshot?.imageUrl || null;
const assetPath = iconScreenshot?.path || null;
const canvasId = normalizeText(ext?.canvasId, id);
extensions.push({
items.push({
id,
canvasId,
extensionId: id,
extensionName: name,
name,
version: normalizeText(ext?.version, "1.0.0"),
description: normalizeText(ext?.description, "External canvas extension"),
path: null,
ref: null,
lastUpdated: null,
imageUrl: imageUrl || null,
screenshots,
imageUrl,
assetPath,
installUrl,
sourceUrl: sourceUrl || null,
external: true,
keywords,
});
}
}
@@ -834,11 +1125,98 @@ function generateExtensionsData(gitDates, commitSha) {
}
}
const sortedExtensions = extensions.sort((a, b) =>
a.name.localeCompare(b.name)
);
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
const keywordFilters = [...new Set(sortedItems.flatMap((item) => item.keywords || []))]
.filter(Boolean)
.sort((a, b) => a.localeCompare(b));
return { items: sortedExtensions };
return {
items: sortedItems,
filters: {
keywords: keywordFilters,
},
};
}
function generateExtensionsData(canvasManifestData) {
if (!canvasManifestData || !Array.isArray(canvasManifestData.items)) {
return { items: [], filters: { keywords: [] } };
}
const items = canvasManifestData.items.map((item) => ({
...item,
keywords: Array.isArray(item.keywords) ? item.keywords : [],
screenshots: item.screenshots || { icon: null, gallery: null },
}));
const filters = {
keywords: [...new Set(items.flatMap((item) => item.keywords))]
.filter(Boolean)
.sort((a, b) => a.localeCompare(b)),
};
return { items, filters };
}
function writePerExtensionCanvasManifests(canvasManifestData) {
const manifests = new Map();
function toExtensionRelativePath(assetPath, extensionId) {
const normalizedPath = normalizeText(assetPath).replace(/\\/g, "/");
if (!normalizedPath) return null;
const prefix = `extensions/${extensionId}/`;
return normalizedPath.startsWith(prefix)
? normalizedPath.slice(prefix.length)
: normalizedPath;
}
function toRelativeScreenshots(screenshots, extensionId) {
if (!screenshots) return { icon: null, gallery: null };
const toRelativeEntry = (entry) =>
entry
? {
...entry,
path: toExtensionRelativePath(entry.path, extensionId),
}
: null;
return {
icon: toRelativeEntry(screenshots.icon),
gallery: toRelativeEntry(screenshots.gallery),
};
}
for (const item of canvasManifestData.items || []) {
if (!item || item.external || !item.extensionId || !item.path) {
continue;
}
// We assume one canvas per extension folder.
if (manifests.has(item.extensionId)) {
continue;
}
manifests.set(item.extensionId, {
id: item.canvasId || item.id,
name: item.name,
description: item.description || "Canvas extension",
version: item.version || "1.0.0",
keywords: Array.isArray(item.keywords)
? [...new Set(item.keywords)].sort((a, b) => a.localeCompare(b))
: [],
screenshots: toRelativeScreenshots(
item.screenshots || { icon: null, gallery: null },
item.extensionId
),
});
}
for (const [extensionId, manifest] of manifests.entries()) {
const canvasManifestPath = path.join(
EXTENSIONS_DIR,
extensionId,
"canvas.json"
);
fs.writeFileSync(canvasManifestPath, JSON.stringify(manifest, null, 2));
}
}
/**
@@ -1181,9 +1559,12 @@ async function main() {
`✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)`
);
const extensionsData = generateExtensionsData(gitDates, commitSha);
const canvasManifestData = generateCanvasManifest(gitDates, commitSha);
const extensionsData = generateExtensionsData(canvasManifestData);
const extensions = extensionsData.items;
console.log(`✓ Generated ${extensions.length} extensions`);
console.log(
`✓ Generated ${extensions.length} extensions (${extensionsData.filters.keywords.length} keywords)`
);
const toolsData = generateToolsData();
const tools = toolsData.items;
@@ -1248,6 +1629,8 @@ async function main() {
JSON.stringify(extensionsData, null, 2)
);
writePerExtensionCanvasManifests(canvasManifestData);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "tools.json"),
JSON.stringify(toolsData, null, 2)