Skip to content
Open
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
4 changes: 2 additions & 2 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export default defineConfig({
vite: {
ssr: {
noExternal: ["@patternfly/*", "react-dropzone"],
external: ["node:fs", "node:path"]
external: ["node:fs", "node:path", "fs/promises", "path"]
},
server: {
fs: {
allow: ['./']
}
}
},
},
adapter: cloudflare()
});
464 changes: 430 additions & 34 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript-eslint": "^8.15.0",
"wrangler": "^4.20.0"
"wrangler": "^4.20.0",
"@patternfly/react-user-feedback": "^6.0.0",
"@patternfly/react-data-view": "^6.0.0"
},
"config": {
"commitizen": {
Expand Down
2 changes: 2 additions & 0 deletions pf-docs.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const config = {
pattern: '*/patternfly-docs/**/*.md',
name: 'quickstarts-docs',
},
{ packageName: "@patternfly/react-user-feedback", pattern: "patternfly-docs/**/*.md", name: "user-feedback-docs" },
{ packageName: "@patternfly/react-data-view", pattern: "patternfly-docs/**/*.md", name: "data-view-docs" },
],
outputDir: './dist',
propsGlobs: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ jest.mock('../../../../../../../utils/apiIndex/fetch', () => ({
tabs: {
'v6::components::alert': ['react', 'html', 'react-demos'],
},
examples: {},
}),
}))

Expand Down
3 changes: 3 additions & 0 deletions src/pages/api/[version]/[section].ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const GET: APIRoute = async ({ params, url }) => {

try {
const index = await fetchApiIndex(url)

// Get pages for this section (includes underscore-separated subsection pages like "forms-checkbox")
const key = createIndexKey(version, section)
const pages = index.pages[key]

Expand All @@ -26,6 +28,7 @@ export const GET: APIRoute = async ({ params, url }) => {
)
}

// Return pages array (already sorted, contains both regular and underscore-separated subsection pages)
return createJsonResponse(pages)
} catch (error) {
const details = error instanceof Error ? error.message : String(error)
Expand Down
6 changes: 4 additions & 2 deletions src/pages/api/[version]/[section]/[page].ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ export const GET: APIRoute = async ({ params, url }) => {

try {
const index = await fetchApiIndex(url)
const key = createIndexKey(version, section, page)
const tabs = index.tabs[key]

// Return tabs for this page (page may be underscore-separated like "forms_checkbox")
const pageKey = createIndexKey(version, section, page)
const tabs = index.tabs[pageKey]

if (!tabs) {
return createJsonResponse(
Expand Down
9 changes: 6 additions & 3 deletions src/pages/api/[version]/[section]/[page]/[tab].ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@ export const GET: APIRoute = async ({ params, redirect, url }) => {
}

// Check if section exists for this version
const sectionKey = createIndexKey(version, section)
if (!index.sections[version]?.includes(section)) {
const sections = index.sections[version] || []

if (!sections.includes(section)) {
return createJsonResponse(
{ error: `Section '${section}' not found for version '${version}'` },
404,
)
}

// Check if page exists for this section
const sectionKey = createIndexKey(version, section)
const pageKey = createIndexKey(version, section, page)

// Check if page exists for this section (page may be underscore-separated like "forms_checkbox")
if (!index.pages[sectionKey]?.includes(page)) {
return createJsonResponse(
{
Expand Down
6 changes: 4 additions & 2 deletions src/pages/api/[version]/[section]/[page]/[tab]/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ export const GET: APIRoute = async ({ params, url }) => {
// Get examples with titles directly from the index
try {
const index = await fetchApiIndex(url)
const tabKey = createIndexKey(version, section, page, tab)
const examples = index.examples[tabKey] || []

// Example key is version::section::page::tab (page may be underscore-separated like "forms_checkbox")
const exampleKey = createIndexKey(version, section, page, tab)

const examples = index.examples[exampleKey] || []
return createJsonResponse(examples)
} catch (error) {
const details = error instanceof Error ? error.message : String(error)
Expand Down
172 changes: 30 additions & 142 deletions src/pages/api/[version]/[section]/[page]/[tab]/examples/[example].ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
/* eslint-disable no-console */
import type { APIRoute, GetStaticPaths } from 'astro'
import type { CollectionEntry, CollectionKey } from 'astro:content'
import { getCollection } from 'astro:content'
import { readFile } from 'fs/promises'
import { resolve } from 'path'
import { content } from '../../../../../../../content'
import { kebabCase, addDemosOrDeprecated } from '../../../../../../../utils'
import { getDefaultTabForApi } from '../../../../../../../utils/packageUtils'
import { createJsonResponse, createTextResponse, createIndexKey } from '../../../../../../../utils/apiHelpers'
import { createJsonResponse, createTextResponse } from '../../../../../../../utils/apiHelpers'
import { generateAndWriteApiIndex } from '../../../../../../../utils/apiIndex/generate'
import { getEnrichedCollections } from '../../../../../../../utils/apiRoutes/collections'
import { findContentEntryFilePath } from '../../../../../../../utils/apiRoutes/contentMatching'
import { extractImports, extractExampleFilePath } from '../../../../../../../utils/apiRoutes/exampleParsing'

export const prerender = true

Expand All @@ -26,144 +24,30 @@ export const getStaticPaths: GetStaticPaths = async () => {
}
}[] = []

// Build paths from index structure
for (const version of index.versions) {
for (const section of index.sections[version] || []) {
const sectionKey = createIndexKey(version, section)
for (const page of index.pages[sectionKey] || []) {
const pageKey = createIndexKey(version, section, page)
for (const tab of index.tabs[pageKey] || []) {
const tabKey = createIndexKey(version, section, page, tab)

// Get all examples for this tab
const examples = index.examples[tabKey] || []
for (const example of examples) {
paths.push({
params: {
version,
section,
page,
tab,
example: example.exampleName,
},
})
}
}
// Build paths from index structure by iterating over examples
// All examples are keyed by version::section::page::tab (page may be underscore-separated like "forms_checkbox")
for (const [exampleKey, examples] of Object.entries(index.examples)) {
const parts = exampleKey.split('::')

if (parts.length === 4) {
const [version, section, page, tab] = parts
for (const example of examples) {
paths.push({
params: {
version,
section,
page,
tab,
example: example.exampleName,
},
})
}
}
}

return paths
}

/**
* Extracts import statements from file content
* Matches import statements with relative paths (starting with ./ or ../)
*
* @param fileContent - The file content to parse
* @returns Array of import statements or null if none found
*/
function getImports(fileContent: string): string[] | null {
// Match import statements with relative paths
// Supports: import X from './path', import X from "../path/file.tsx"
const importRegex = /import\s+.*\s+from\s+['"]\.{1,2}\/[^'"]+['"]/gm
const matches = fileContent.match(importRegex)
return matches
}

/**
* Extracts the file path for a specific example from import statements
* Looks for imports that reference the example name
*
* @param imports - Array of import statements
* @param exampleName - Name of the example to find
* @returns Relative file path without quotes (including query params like ?raw), or null if not found
*/
function getExampleFilePath(imports: string[], exampleName: string): string | null {
const exampleImport = imports.find((imp) => imp.includes(exampleName))
if (!exampleImport) {
console.error('No import path found for example', exampleName)
return null
}
// Extract path from import statement, handling query parameters like ?raw
// Matches: "./path" or "../path" with optional file extensions and query params
const match = exampleImport.match(/['"](\.[^'"]+)['"]/i)
if (!match || !match[1]) {
return null
}
return match[1]
}

/**
* Fetches all content collections for a specific version
* Enriches entries with default tab information if not specified
*
* @param version - The documentation version (e.g., 'v6')
* @returns Promise resolving to array of collection entries with metadata
*/
async function getCollections(version: string) {
const collectionsToFetch = content
.filter((entry) => entry.version === version)
.map((entry) => entry.name as CollectionKey)
const collections = await Promise.all(
collectionsToFetch.map(async (name) => await getCollection(name)),
)
return collections.flat().map(({ data, filePath, ...rest }) => ({
filePath,
...rest,
data: {
...data,
tab: data.tab || data.source || getDefaultTabForApi(filePath),
},
}))
}

/**
* Finds the file path for a content entry matching the given parameters
* Prefers .mdx files over .md files when both exist, since .mdx files
* contain the LiveExample components and example imports
*
* @param collections - Array of collection entries to search
* @param section - The section name (e.g., 'components')
* @param page - The page slug (e.g., 'alert')
* @param tab - The tab name (e.g., 'react')
* @returns Promise resolving to the file path, or null if not found
*/
async function getContentEntryFilePath(
collections: CollectionEntry<'core-docs' | 'quickstarts-docs' | 'react-component-docs'>[],
section: string,
page: string,
tab: string
): Promise<string | null> {
// Find all matching entries
const matchingEntries = collections.filter((entry) => {
const entryTab = addDemosOrDeprecated(entry.data.tab, entry.filePath)
return (
entry.data.section === section &&
kebabCase(entry.data.id) === page &&
entryTab === tab
)
})

if (matchingEntries.length === 0) {
console.error('No content entry found for section', section, 'page', page, 'tab', tab)
return null
}

// Prefer .mdx files over .md files (mdx files have LiveExample components)
const mdxEntry = matchingEntries.find((entry) =>
typeof entry.filePath === 'string' && entry.filePath.endsWith('.mdx')
)
const contentEntry = mdxEntry || matchingEntries[0]

if (typeof contentEntry.filePath !== 'string') {
console.error('No file path found for content entry', contentEntry.id)
return null
}

return contentEntry.filePath
}

/**
* GET handler for retrieving example source code
* Returns the raw source code for a specific example
Expand All @@ -175,14 +59,18 @@ export const GET: APIRoute = async ({ params }) => {
const { version, section, page, tab, example } = params
if (!version || !section || !page || !tab || !example) {
return createJsonResponse(
{ error: 'Version, section, page, tab, and example parameters are required' },
{ error: 'All parameters are required' },
400
)
}

try {
const collections = await getCollections(version)
const contentEntryFilePath = await getContentEntryFilePath(collections, section, page, tab)
const collections = await getEnrichedCollections(version)
const contentEntryFilePath = findContentEntryFilePath(collections, {
section,
page,
tab
})

if (!contentEntryFilePath) {
return createJsonResponse(
Expand All @@ -203,15 +91,15 @@ export const GET: APIRoute = async ({ params }) => {
)
}

const contentEntryImports = getImports(contentEntryFileContent)
const contentEntryImports = extractImports(contentEntryFileContent)
if (!contentEntryImports) {
return createJsonResponse(
{ error: 'No imports found in content entry' },
404
)
}

const relativeExampleFilePath = getExampleFilePath(contentEntryImports, example)
const relativeExampleFilePath = extractExampleFilePath(contentEntryImports, example)
if (!relativeExampleFilePath) {
return createJsonResponse(
{ error: `Example "${example}" not found in imports` },
Expand Down
Loading
Loading