chore: publish from main

This commit is contained in:
github-actions[bot]
2026-07-10 03:24:21 +00:00
parent 9a8e446f02
commit 51b6fa0253
56 changed files with 4496 additions and 992 deletions
+125
View File
@@ -0,0 +1,125 @@
import fs from "node:fs";
import path from "node:path";
import { marked } from "marked";
import matter from "gray-matter";
import { enhanceMarkdownA11y } from "./markdown-a11y";
import { sanitizeHtml } from "./sanitize-html";
/**
* Build-time helpers shared by the resource detail pages
* (src/pages/agent/[id].astro, src/pages/instruction/[id].astro, ...).
*
* This module is intentionally free of any DOM/browser dependencies so it can
* run in Astro frontmatter at build time (unlike src/scripts/utils.ts, which is
* client-side).
*/
const RAW_BASE =
"https://raw.githubusercontent.com/github/awesome-copilot/main";
const GITHUB_BASE = "https://github.com/github/awesome-copilot/blob/main";
// Per resource-type VS Code install configuration.
const INSTALL_CONFIG = {
agent: {
command: "chat-agent",
baseUrl: "https://aka.ms/awesome-copilot/install/agent",
},
instruction: {
command: "chat-instructions",
baseUrl: "https://aka.ms/awesome-copilot/install/instructions",
},
} as const satisfies Record<string, { command: string; baseUrl: string }>;
export type ResourceType = keyof typeof INSTALL_CONFIG;
export interface DetailPageInput {
path: string;
lastUpdated?: string | null;
}
export interface DetailPageData {
vscodeUrl: string;
insidersUrl: string;
githubUrl: string;
markdownHtml: string;
frontmatterText: string;
rawMarkdown: string;
lastUpdated: string | null;
}
// Repo root. Astro runs (both `dev` and `build`) execute with the website/
// directory as the working directory, so the repo root is its parent. This is
// resolved from the working directory rather than `import.meta.url` because the
// latter is unreliable once this module is bundled during a production build.
const repoRoot = path.resolve(process.cwd(), "..");
function buildInstallUrl(
filePath: string,
type: ResourceType,
insiders: boolean
): string {
const { command, baseUrl } = INSTALL_CONFIG[type];
const rawUrl = `${RAW_BASE}/${filePath}`;
const editor = insiders ? "vscode-insiders" : "vscode";
const innerUrl = `${editor}:${command}/install?url=${encodeURIComponent(rawUrl)}`;
return `${baseUrl}?url=${encodeURIComponent(innerUrl)}`;
}
export function formatLastUpdated(
lastUpdated?: string | null
): string | null {
return lastUpdated
? new Date(lastUpdated).toLocaleDateString(undefined, {
year: "numeric",
month: "long",
day: "numeric",
})
: null;
}
/**
* Read a resource's markdown file at build time and return its rendered HTML,
* trimmed frontmatter block, and the raw file contents.
*/
export function readResourceMarkdown(filePath: string): {
markdownHtml: string;
frontmatterText: string;
rawMarkdown: string;
} {
let markdownHtml = "";
let frontmatterText = "";
let rawMarkdown = "";
try {
const raw = fs.readFileSync(path.join(repoRoot, filePath), "utf-8");
rawMarkdown = raw;
const parsed = matter(raw);
frontmatterText = parsed.matter?.trim() ?? "";
markdownHtml = enhanceMarkdownA11y(
sanitizeHtml(marked.parse(parsed.content, { async: false }) as string)
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.warn(
`[detail-page] Failed to read or parse markdown for "${filePath}": ${message}`
);
markdownHtml = "";
}
return { markdownHtml, frontmatterText, rawMarkdown };
}
/**
* Build every piece of shared data a resource detail page needs: install URLs,
* the GitHub source URL, rendered/raw markdown, and the formatted update date.
*/
export function loadDetailPage(
item: DetailPageInput,
type: ResourceType
): DetailPageData {
return {
vscodeUrl: buildInstallUrl(item.path, type, false),
insidersUrl: buildInstallUrl(item.path, type, true),
githubUrl: `${GITHUB_BASE}/${item.path}`,
...readResourceMarkdown(item.path),
lastUpdated: formatLastUpdated(item.lastUpdated),
};
}
+86
View File
@@ -0,0 +1,86 @@
/**
* Shared helpers for building safe GitHub links to external (third-party)
* plugin/extension sources.
*
* This module is intentionally dependency-free (no DOM or node imports) so it
* can run both at build time (in Astro frontmatter, e.g. plugin/[id].astro and
* extension/[id].astro) and on the client (plugins-render.ts, modal.ts).
*/
export interface ExternalSource {
source?: string;
repo?: string;
path?: string;
ref?: string;
sha?: string;
}
const GITHUB_REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
// Tags, branch names, or a 40-char commit SHA. Deliberately excludes
// whitespace, "..", and characters that could break out of the path segment.
const GITHUB_REF_RE = /^(?!\/)(?!.*\/\/)(?!.*\.\.)(?!.*\/$)[A-Za-z0-9._/-]+$/;
/**
* Allow only http(s) URLs; return "#" for anything else (mirrors the
* client-side sanitizeUrl in scripts/utils.ts). Prevents javascript:/data:
* schemes from reaching an href/src attribute.
*/
export function sanitizeHttpUrl(url: string | null | undefined): string {
if (!url) return "#";
try {
const parsed = new URL(url);
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
return url;
}
} catch {
// Invalid URL
}
return "#";
}
function encodeRepoPath(rawPath: string): string {
return rawPath
.replace(/\\/g, "/")
.split("/")
.filter((segment) => segment !== "")
.map((segment) => encodeURIComponent(segment))
.join("/");
}
/**
* Build a GitHub URL for an external plugin/extension source, pinned to the
* most immutable revision available (sha, then ref, then the default branch).
* Path segments are URL-encoded and a leading "/" (or a bare "/" path) is
* treated as "no path".
*
* When the source is not a valid GitHub repo reference, falls back to the first
* safe http(s) URL in `fallbackUrls`, or "#" if none is provided.
*/
export function externalRepoUrl(
source: ExternalSource | null | undefined,
fallbackUrls: Array<string | null | undefined> = []
): string {
if (
source?.source === "github" &&
typeof source.repo === "string" &&
GITHUB_REPO_RE.test(source.repo)
) {
const base = `https://github.com/${source.repo}`;
const candidateRef = source.sha || source.ref;
const ref =
candidateRef && GITHUB_REF_RE.test(candidateRef) ? candidateRef : "";
const path =
source.path && source.path !== "/" ? encodeRepoPath(source.path) : "";
if (path) {
return `${base}/tree/${ref || "main"}/${path}`;
}
return ref ? `${base}/tree/${ref}` : base;
}
for (const url of fallbackUrls) {
const safe = sanitizeHttpUrl(url);
if (safe !== "#") return safe;
}
return "#";
}
+108
View File
@@ -0,0 +1,108 @@
/**
* Map a file name to a Shiki language id and a coarse "kind" used by the file
* browser to decide how to render it (markdown prose, highlighted code, image,
* or a plain fallback). Shared between the build-time FileBrowser component
* and the client-side file-browser script, so both agree on languages.
*/
export type FileKind = "markdown" | "code" | "image" | "other";
const EXT_LANG: Record<string, string> = {
ts: "typescript",
tsx: "tsx",
js: "javascript",
jsx: "jsx",
mjs: "javascript",
cjs: "javascript",
py: "python",
rb: "ruby",
go: "go",
rs: "rust",
java: "java",
cs: "csharp",
c: "c",
cpp: "cpp",
h: "c",
php: "php",
swift: "swift",
kt: "kotlin",
sh: "bash",
bash: "bash",
zsh: "bash",
ps1: "powershell",
json: "json",
jsonc: "json",
yml: "yaml",
yaml: "yaml",
toml: "toml",
html: "html",
css: "css",
scss: "scss",
xml: "xml",
sql: "sql",
dockerfile: "docker",
md: "markdown",
markdown: "markdown",
txt: "text",
};
const CODE_LANGS = new Set<string>([
"typescript",
"tsx",
"javascript",
"jsx",
"python",
"ruby",
"go",
"rust",
"java",
"csharp",
"c",
"cpp",
"php",
"swift",
"kotlin",
"bash",
"powershell",
"json",
"yaml",
"toml",
"html",
"css",
"scss",
"xml",
"sql",
"docker",
]);
const IMAGE_EXTS = new Set<string>([
"png",
"jpg",
"jpeg",
"gif",
"svg",
"webp",
"avif",
"bmp",
"ico",
]);
export interface FileMeta {
ext: string;
lang: string;
kind: FileKind;
}
export function getFileMeta(name: string): FileMeta {
const base = name.split("/").pop() ?? name;
const ext =
base.toLowerCase() === "dockerfile"
? "dockerfile"
: (base.split(".").pop() ?? "").toLowerCase();
const lang = EXT_LANG[ext] ?? "text";
let kind: FileKind = "other";
if (IMAGE_EXTS.has(ext)) kind = "image";
else if (ext === "md" || ext === "markdown") kind = "markdown";
else if (CODE_LANGS.has(lang)) kind = "code";
return { ext, lang, kind };
}
+35
View File
@@ -0,0 +1,35 @@
/**
* Accessibility post-processing for marked-generated markdown HTML.
*
* axe flags two issues on rendered markdown that we fix here rather than in the
* source content:
* - `scrollable-region-focusable`: <pre> and <table> blocks can overflow on the
* x-axis, so they must be keyboard focusable to let keyboard users scroll them.
* - `label`: GitHub-style task-list checkboxes (`- [ ]` / `- [x]`) render as bare
* disabled <input type="checkbox"> elements with no accessible name.
*
* This module is pure (no DOM or node deps) so it can run both at build time
* (src/lib/detail-page.ts) and on the client (src/scripts/pages/file-browser.ts).
*/
export function enhanceMarkdownA11y(html: string): string {
if (!html) return html;
let out = html;
// Make scrollable code/table blocks keyboard focusable.
out = out.replace(/<pre(?![^>]*\btabindex=)/g, '<pre tabindex="0"');
out = out.replace(/<table(?![^>]*\btabindex=)/g, '<table tabindex="0"');
// Give task-list checkboxes an accessible name based on their checked state.
out = out.replace(
/<input\b([^>]*\btype="checkbox"[^>]*)>/g,
(match, attrs) => {
if (/\baria-label=/.test(attrs)) return match;
const label = /\bchecked\b/.test(attrs)
? "Completed task"
: "Incomplete task";
return `<input${attrs} aria-label="${label}">`;
}
);
return out;
}
+54
View File
@@ -0,0 +1,54 @@
/**
* Isomorphic HTML sanitizer for rendered markdown.
*
* `marked` allows raw HTML to pass through untouched, and the resulting string
* is injected via `set:html` / `innerHTML` on the resource detail pages and in
* the client-side file browser. Even though the markdown we render originates
* from this repository, a compromised or malicious resource file could
* otherwise introduce persistent XSS. Sanitizing the generated HTML gives us
* defense-in-depth on both the server (build time) and the client.
*
* `isomorphic-dompurify` resolves to a jsdom-backed DOMPurify in Node (so it
* works in Astro frontmatter during `astro build`) and to the native
* browser DOMPurify when bundled for the client.
*/
import DOMPurify from "isomorphic-dompurify";
let noopenerHookInstalled = false;
function ensureNoopenerHook(): void {
if (noopenerHookInstalled) return;
noopenerHookInstalled = true;
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
const el = node as unknown as {
tagName?: string;
getAttribute?: (name: string) => string | null;
setAttribute?: (name: string, value: string) => void;
};
if (el?.tagName !== "A") return;
if (el.getAttribute?.("target") !== "_blank") return;
const rel = el.getAttribute?.("rel") ?? "";
const tokens = new Set(rel.split(/\s+/).filter(Boolean));
tokens.add("noopener");
tokens.add("noreferrer");
el.setAttribute?.("rel", Array.from(tokens).join(" "));
});
}
/**
* Sanitize a fragment of HTML produced from trusted-but-untrusted markdown,
* stripping scripts, event handlers, and dangerous URL schemes while keeping
* the formatting tags GitHub-flavored markdown commonly emits.
*/
export function sanitizeHtml(html: string): string {
if (!html) return html;
ensureNoopenerHook();
return DOMPurify.sanitize(html, {
// Keep links that open in a new tab (target/rel) which some resource docs
// author directly as raw HTML.
ADD_ATTR: ["target", "rel"],
});
}