Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 26 additions & 18 deletions src/frontend/scripts/write-git-env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,24 @@ function tryExec(cmd) {
}
}

function getExplicitCommit() {
return (
process.env.PUBLIC_GIT_COMMIT_ID ||
process.env.GIT_COMMIT_ID ||
process.env.SOURCE_COMMIT ||
process.env.GITHUB_SHA ||
process.env.VERCEL_GIT_COMMIT_SHA ||
null
);
}

function getCommit() {
/**
* We need to get the commit hash from the public repo's main branch.
* In CI/CD, there may be other intermediary commits from the deploy branch.
* This ensures we always get the correct commit hash.
* Try origin/main first, then fall back to upstream/main for fork workflows.
* Prefer an explicit commit provided by CI, otherwise fall back to the
* currently checked out commit. Avoid remote-ref-dependent resolution so
* local builds remain hermetic and deterministic.
*/
return (
tryExec('git merge-base origin/main HEAD') ??
tryExec('git merge-base upstream/main HEAD') ??
''
);
return getExplicitCommit() ?? tryExec('git rev-parse HEAD') ?? '';
}

function getRepoUrl() {
Expand All @@ -54,25 +60,27 @@ const repo = getRepoUrl().replace(/\/$/, '');
const lines = [`PUBLIC_GIT_COMMIT_ID=${commit}`, `PUBLIC_REPO_URL=${repo}`];

const envPath = join(process.cwd(), '.env.local');
let updated = false;
let content = '';
if (existsSync(envPath)) {
content = readFileSync(envPath, 'utf8');
const fileExists = existsSync(envPath);
const existingContent = fileExists ? readFileSync(envPath, 'utf8') : '';

let content = fileExists ? existingContent : lines.join('\n') + '\n';

if (fileExists) {
lines.forEach((line) => {
const [key] = line.split('=');
const regex = new RegExp(`^${key}=.*$`, 'm');
if (regex.test(content)) {
content = content.replace(regex, line);
updated = true;
} else {
content += (content.endsWith('\n') ? '' : '\n') + line + '\n';
updated = true;
}
});
}

const updated = content !== existingContent;

if (updated) {
writeFileSync(envPath, content);
} else {
writeFileSync(envPath, lines.join('\n') + '\n');
updated = true;
}

if (process.env.CI) {
Expand Down
25 changes: 11 additions & 14 deletions src/frontend/src/components/Include.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
// Include a Markdown/MDX file from the docs collection inline.
// Usage in MDX: <Include relativePath="reference/cli/includes/project-search-logic.description.md" />
// Usage in MDX: <Include relativePath="reference/cli/includes/project-search-logic-description.md" />

import { getEntry, render } from 'astro:content';

interface Props {
/** Path relative to /src/content/docs (may start with or without a leading slash). */
Expand All @@ -9,22 +11,17 @@ interface Props {

const { relativePath } = Astro.props as Props;

// Eagerly import all .md and .mdx under the docs collection so we can render by path.
const mdModules = {
...(import.meta.glob('/src/content/docs/**/*.md', { eager: true }) as Record<string, unknown>),
...(import.meta.glob('/src/content/docs/**/*.mdx', { eager: true }) as Record<string, unknown>),
};

// Normalize the provided path to match the glob keys (absolute from project root).
const normalized = `/src/content/docs/${relativePath.replace(/^\/+/, '')}`;
const mod = mdModules[normalized] as any;
// Resolve the include through the docs collection instead of eagerly globbing
// the entire docs tree into every page that uses <Include>.
Comment on lines +14 to +15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this comment mean? Why is it saying "instead of eagerly globbing the entire docs tree"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code before, was doing this:

 ...(import.meta.glob('/src/content/docs/**/*.md', { eager: true }) as Record<string, unknown>),
  ...(import.meta.glob('/src/content/docs/**/*.mdx', { eager: true }) as Record<string, unknown>),

Which eagerly enumerated ALL matching .md and .mdx files. Which was not good for perf.

const normalizedPath = relativePath.replace(/^\/+/, '');
const entryId = normalizedPath.replace(/\.(md|mdx)$/i, '');
const entry = await getEntry('docs', entryId);

if (!mod) {
throw new Error(`Include.astro: File not found: ${normalized}`);
if (!entry) {
throw new Error(`Include.astro: File not found: ${normalizedPath} (entry id: ${entryId})`);
}

// MD/MDX modules may export either `Content` or a default component.
const ResolvedContent = (mod?.Content ?? mod?.default) as any;
const { Content: ResolvedContent } = await render(entry);
---

{ResolvedContent ? <ResolvedContent /> : null}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function getStaticPaths() {

const { pkg, type, allTypes, kind } = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const apiSidebar = await getApiReferenceSidebar();
const apiSidebar = await getApiReferenceSidebar({ packageName: pkg.package.name });

const filteredMembers = (type.members ?? []).filter((m: any) => m.kind === kind);
const kindLabel = memberKindLabels[kind] ?? kind;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function getStaticPaths() {

const { pkg, type, allTypes } = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const apiSidebar = await getApiReferenceSidebar();
const apiSidebar = await getApiReferenceSidebar({ packageName: pkg.package.name });

/** Flatten doc nodes to a plain-text string (for meta description). */
function flattenToText(nodes: any): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const validTypes = pkg.types.filter((t: any) => t.name); // skip types with empty names
const isOfficial = pkg.package.name.startsWith('Aspire.');
const namespaceGroups = groupTypesByNamespace(validTypes);
const apiSidebar = await getApiReferenceSidebar();
const apiSidebar = await getApiReferenceSidebar({ packageName: pkg.package.name });

const headings = [...namespaceGroups.keys()].map((ns) => ({
depth: 2 as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function getStaticPaths() {

const { pkg, parentType, method } = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const tsSidebar = await getTsApiReferenceSidebar();
const tsSidebar = await getTsApiReferenceSidebar({ packageName: pkg.package.name });
const pkgSlug = tsModuleSlug(pkg.package.name);
const parentSlug = tsSlugify(parentType.name);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export async function getStaticPaths() {

const { pkg, item, itemKind } = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const tsSidebar = await getTsApiReferenceSidebar();
const tsSidebar = await getTsApiReferenceSidebar({ packageName: pkg.package.name });
const pkgSlug = tsModuleSlug(pkg.package.name);

/* ── Handle type: group capabilities into properties & methods ── */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function getStaticPaths() {

const { pkg } = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const tsSidebar = await getTsApiReferenceSidebar();
const tsSidebar = await getTsApiReferenceSidebar({ packageName: pkg.package.name });
const pkgSlug = tsModuleSlug(pkg.package.name);
const isOfficial = pkg.package.name.startsWith('Aspire.');

Expand Down
129 changes: 86 additions & 43 deletions src/frontend/src/utils/api-sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,26 @@
/* passed as the `sidebar` prop to `<StarlightPage>`. */
/* ------------------------------------------------------------------ */

import { slugify, genericArity, typeDisplayName, packageSlug, groupTypesByNamespace, memberKindOrder, memberKindLabels, memberKindSlugs, getPackages } from './packages';
import {
slugify,
genericArity,
typeDisplayName,
packageSlug,
groupTypesByNamespace,
memberKindOrder,
memberKindLabels,
memberKindSlugs,
getPackages,
} from './packages';

/** Member kinds that get their own sidebar sub-item under a type. */
const sidebarMemberKinds = memberKindOrder;
const apiSidebarCache = new Map<string, Promise<any[]>>();
const shouldCacheApiSidebar = import.meta.env.PROD;

interface ApiSidebarOptions {
packageName?: string;
}

/**
* Determine whether a type is a simple marker interface (no members at all).
Expand All @@ -26,18 +42,20 @@ function isMarkerInterface(type: any): boolean {
* Returns an array of { label, link } entries for each member-kind group
* that has at least one member (e.g. Constructors, Properties, Methods).
*/
function buildTypeSidebarItems(
packageName: string,
type: any,
): { label: string; link: string }[] {
function buildTypeSidebarItems(packageName: string, type: any): { label: string; link: string }[] {
const base = `/reference/api/csharp/${packageSlug(packageName)}/${slugify(type.name, genericArity(type))}`;
const items: { label: string; link: string }[] = [
{ label: 'Overview', link: `${base}/` },
];
const items: { label: string; link: string }[] = [{ label: 'Overview', link: `${base}/` }];

const members: any[] = type.members ?? [];
const memberKindsPresent = new Set<string>();
for (const member of members) {
if (member.kind) {
memberKindsPresent.add(member.kind);
}
}

for (const kind of sidebarMemberKinds) {
if (members.some((m: any) => m.kind === kind)) {
if (memberKindsPresent.has(kind)) {
items.push({
label: memberKindLabels[kind] ?? kind,
link: `${base}/${memberKindSlugs[kind] ?? kind}/`,
Expand All @@ -49,46 +67,71 @@ function buildTypeSidebarItems(
}

/**
* Generate a sidebar configuration array for API reference pages.
* Each package becomes a collapsible group. Within each package,
* types are grouped by namespace.
* Build the sidebar group for a single C# package.
*/
export async function getApiReferenceSidebar() {
function buildPackageSidebarEntry(pkg: any, collapsed: boolean = true) {
const validTypes = pkg.types.filter((t: any) => t.name);
const nsGroups = groupTypesByNamespace(validTypes);

// If only one namespace, skip the namespace nesting level
const hasMultipleNamespaces = nsGroups.size > 1;

const typeItems = hasMultipleNamespaces
? [...nsGroups.entries()].map(([ns, types]) => ({
label: ns,
collapsed: true,
items: types.map((t: any) => buildTypeSidebarEntry(pkg.package.name, t)),
}))
: [...nsGroups.values()].flat().map((t: any) => buildTypeSidebarEntry(pkg.package.name, t));

return {
label: pkg.package.name,
collapsed,
items: [
{ label: 'Overview', link: `/reference/api/csharp/${packageSlug(pkg.package.name)}/` },
...typeItems,
],
};
}

async function buildApiReferenceSidebar(options: ApiSidebarOptions = {}) {
const packages = await getPackages();
const sidebarRoot = { label: 'Search APIs', link: '/reference/api/csharp/' };

if (options.packageName) {
const currentPackage = packages.find(
(pkg) => pkg.data.package.name === options.packageName
)?.data;
return currentPackage
? [sidebarRoot, buildPackageSidebarEntry(currentPackage, false)]
: [sidebarRoot];
}

const sorted = packages
.map((p) => p.data)
.sort((a, b) => a.package.name.localeCompare(b.package.name));

return [
{ label: 'Search APIs', link: '/reference/api/csharp/' },
...sorted.map((pkg) => {
const validTypes = pkg.types.filter((t: any) => t.name);
const nsGroups = groupTypesByNamespace(validTypes);

// If only one namespace, skip the namespace nesting level
const hasMultipleNamespaces = nsGroups.size > 1;

const typeItems = hasMultipleNamespaces
? [...nsGroups.entries()].map(([ns, types]) => ({
label: ns,
collapsed: true,
items: types.map((t: any) => buildTypeSidebarEntry(pkg.package.name, t)),
}))
: [...nsGroups.values()]
.flat()
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.map((t: any) => buildTypeSidebarEntry(pkg.package.name, t));

return {
label: pkg.package.name,
collapsed: true,
items: [
{ label: 'Overview', link: `/reference/api/csharp/${packageSlug(pkg.package.name)}/` },
...typeItems,
],
};
}),
];
return [sidebarRoot, ...sorted.map((pkg) => buildPackageSidebarEntry(pkg))];
}

/**
* Generate a sidebar configuration array for API reference pages.
* Results are memoized so repeated page renders do not rebuild the same tree.
*/
export function getApiReferenceSidebar(options: ApiSidebarOptions = {}) {
if (!shouldCacheApiSidebar) {
return buildApiReferenceSidebar(options);
}

const cacheKey = options.packageName ? `package:${options.packageName}` : 'all';
const cachedSidebar = apiSidebarCache.get(cacheKey);
if (cachedSidebar) {
return cachedSidebar;
}

const sidebar = buildApiReferenceSidebar(options);
apiSidebarCache.set(cacheKey, sidebar);
return sidebar;
}

/** Build a single sidebar entry for a type (flat link or nested group). */
Expand Down
Loading