From 4574230ab03a0685134e0f32c6363549e363f161 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Wed, 26 Nov 2025 22:31:24 -0500 Subject: [PATCH] Revert "feat(API): add text content API (#174)" This reverts commit 37984603c40b8a5d45e8fd08836f7af5c74c02a9. --- .gitignore | 4 +- cli/__tests__/createCollectionContent.test.ts | 240 ++--------- cli/createCollectionContent.ts | 47 +-- cli/getConfig.ts | 1 - jest.config.ts | 2 - src/__mocks__/astro-content.ts | 6 - src/pages/api/[version].ts | 52 --- src/pages/api/[version]/[section].ts | 52 --- src/pages/api/[version]/[section]/[page].ts | 113 ----- .../api/[version]/[section]/[page]/[tab].ts | 71 ---- src/pages/api/__tests__/[version].test.ts | 115 ----- .../api/__tests__/[version]/[section].test.ts | 137 ------ .../[version]/[section]/[page].test.ts | 130 ------ .../[version]/[section]/[page]/[tab].test.ts | 174 -------- src/pages/api/__tests__/testHelpers.ts | 140 ------- src/pages/api/__tests__/versions.test.ts | 47 --- src/pages/api/index.ts | 172 -------- src/pages/api/openapi.json.ts | 392 ------------------ src/pages/api/versions.ts | 17 - src/utils/__tests__/apiHelpers.test.ts | 193 --------- src/utils/apiHelpers.ts | 32 -- test.setup.ts | 51 --- 22 files changed, 29 insertions(+), 2159 deletions(-) delete mode 100644 src/__mocks__/astro-content.ts delete mode 100644 src/pages/api/[version].ts delete mode 100644 src/pages/api/[version]/[section].ts delete mode 100644 src/pages/api/[version]/[section]/[page].ts delete mode 100644 src/pages/api/[version]/[section]/[page]/[tab].ts delete mode 100644 src/pages/api/__tests__/[version].test.ts delete mode 100644 src/pages/api/__tests__/[version]/[section].test.ts delete mode 100644 src/pages/api/__tests__/[version]/[section]/[page].test.ts delete mode 100644 src/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts delete mode 100644 src/pages/api/__tests__/testHelpers.ts delete mode 100644 src/pages/api/__tests__/versions.test.ts delete mode 100644 src/pages/api/index.ts delete mode 100644 src/pages/api/openapi.json.ts delete mode 100644 src/pages/api/versions.ts delete mode 100644 src/utils/__tests__/apiHelpers.test.ts delete mode 100644 src/utils/apiHelpers.ts diff --git a/.gitignore b/.gitignore index 20427db..ebe0a8c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,4 @@ pnpm-debug.log* .eslintcache ## Ignore content.ts -src/content.ts - -coverage/ \ No newline at end of file +src/content.ts \ No newline at end of file diff --git a/cli/__tests__/createCollectionContent.test.ts b/cli/__tests__/createCollectionContent.test.ts index 9c9b5ab..4482c85 100644 --- a/cli/__tests__/createCollectionContent.test.ts +++ b/cli/__tests__/createCollectionContent.test.ts @@ -1,7 +1,7 @@ import { createCollectionContent } from '../createCollectionContent' import { getConfig } from '../getConfig' import { writeFile } from 'fs/promises' -import { existsSync, readFileSync } from 'fs' +import { existsSync } from 'fs' jest.mock('../getConfig') jest.mock('fs/promises') @@ -53,11 +53,10 @@ it('should call writeFile with the expected file location and content without th const mockContent = [ { name: 'test', base: 'src/docs', pattern: '**/*.md' } ] - ;(getConfig as jest.Mock).mockResolvedValue({ + ;(getConfig as jest.Mock).mockResolvedValue({ content: mockContent, repoRoot: '.' }) - ;(existsSync as jest.Mock).mockReturnValue(false) // No package.json const mockConsoleError = jest.fn() jest.spyOn(console, 'error').mockImplementation(mockConsoleError) @@ -65,7 +64,7 @@ it('should call writeFile with the expected file location and content without th await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) const expectedContent = [ - { name: 'test', base: '/config/dir/src/docs', pattern: '**/*.md', version: null } + { name: 'test', base: '/config/dir/src/docs', pattern: '**/*.md' } ] expect(writeFile).toHaveBeenCalledWith( @@ -79,11 +78,10 @@ it('should log error if writeFile throws an error', async () => { const mockContent = [ { name: 'test', base: 'src/docs', pattern: '**/*.md' } ] - ;(getConfig as jest.Mock).mockResolvedValue({ + ;(getConfig as jest.Mock).mockResolvedValue({ content: mockContent, repoRoot: '.' }) - ;(existsSync as jest.Mock).mockReturnValue(false) // No package.json const mockConsoleError = jest.fn() jest.spyOn(console, 'error').mockImplementation(mockConsoleError) @@ -104,12 +102,11 @@ it('should log all verbose messages when run in verbose mode', async () => { { name: 'docs', base: 'src/docs', pattern: '**/*.md' }, { name: 'components', packageName: '@patternfly/react-core', pattern: '**/*.md' } ] - ;(getConfig as jest.Mock).mockResolvedValue({ + ;(getConfig as jest.Mock).mockResolvedValue({ content: mockContent, repoRoot: '../' }) ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ version: '6.2.2' })) ;(writeFile as jest.Mock).mockResolvedValue(undefined) const mockConsoleLog = jest.fn() @@ -121,19 +118,15 @@ it('should log all verbose messages when run in verbose mode', async () => { expect(mockConsoleLog).toHaveBeenCalledWith('configuration content entry: ', mockContent, '\n') expect(mockConsoleLog).toHaveBeenCalledWith('Creating content file', '/foo/src/content.ts', '\n') expect(mockConsoleLog).toHaveBeenCalledWith('repoRootDir: ', '/config', '\n') - + // For the base entry expect(mockConsoleLog).toHaveBeenCalledWith('relative path: ', 'src/docs') expect(mockConsoleLog).toHaveBeenCalledWith('absolute path: ', '/config/dir/src/docs', '\n') - + // For the packageName entry expect(mockConsoleLog).toHaveBeenCalledWith('looking for package in ', '/config/dir/node_modules', '\n') expect(mockConsoleLog).toHaveBeenCalledWith('found package at ', '/config/dir/node_modules/@patternfly/react-core', '\n') - - // Version extraction logs - expect(mockConsoleLog).toHaveBeenCalledWith('Extracted version v6 from /config/dir/src/docs/package.json\n') - expect(mockConsoleLog).toHaveBeenCalledWith('Extracted version v6 from /config/dir/node_modules/@patternfly/react-core/package.json\n') - + // Final log expect(mockConsoleLog).toHaveBeenCalledWith('Content file created') }) @@ -184,11 +177,10 @@ it('should not log to the console when not run in verbose mode', async () => { const mockContent = [ { name: 'test', base: 'src/docs', pattern: '**/*.md' } ] - ;(getConfig as jest.Mock).mockResolvedValue({ + ;(getConfig as jest.Mock).mockResolvedValue({ content: mockContent, repoRoot: '.' }) - ;(existsSync as jest.Mock).mockReturnValue(false) ;(writeFile as jest.Mock).mockResolvedValue(undefined) const mockConsoleLog = jest.fn() @@ -203,12 +195,11 @@ it('should handle content with packageName by finding package in node_modules', const mockContent = [ { name: 'test', packageName: '@patternfly/react-core', pattern: '**/*.md' } ] - ;(getConfig as jest.Mock).mockResolvedValue({ + ;(getConfig as jest.Mock).mockResolvedValue({ content: mockContent, repoRoot: '.' }) ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ version: '6.2.2' })) ;(writeFile as jest.Mock).mockResolvedValue(undefined) const mockConsoleError = jest.fn() @@ -217,11 +208,10 @@ it('should handle content with packageName by finding package in node_modules', await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) const expectedContent = [ - { + { base: '/config/dir/node_modules/@patternfly/react-core', - version: 'v6', - name: 'test', - packageName: '@patternfly/react-core', + name: 'test', + packageName: '@patternfly/react-core', pattern: '**/*.md' } ] @@ -237,15 +227,13 @@ it('should handle content with packageName when package is not found locally but const mockContent = [ { name: 'test', packageName: '@patternfly/react-core', pattern: '**/*.md' } ] - ;(getConfig as jest.Mock).mockResolvedValue({ + ;(getConfig as jest.Mock).mockResolvedValue({ content: mockContent, repoRoot: '../../' }) ;(existsSync as jest.Mock) .mockReturnValueOnce(false) // not found in /config/dir/node_modules - .mockReturnValueOnce(true) // found in /config/node_modules/package.json - .mockReturnValueOnce(true) // package.json exists for version extraction - ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ version: '5.1.0' })) + .mockReturnValueOnce(true) // found in /config/node_modules ;(writeFile as jest.Mock).mockResolvedValue(undefined) const mockConsoleError = jest.fn() @@ -254,11 +242,10 @@ it('should handle content with packageName when package is not found locally but await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) const expectedContent = [ - { + { base: '/config/node_modules/@patternfly/react-core', - version: 'v5', - name: 'test', - packageName: '@patternfly/react-core', + name: 'test', + packageName: '@patternfly/react-core', pattern: '**/*.md' } ] @@ -274,7 +261,7 @@ it('should handle content with packageName when package is not found anywhere', const mockContent = [ { name: 'test', packageName: '@patternfly/react-core', pattern: '**/*.md' } ] - ;(getConfig as jest.Mock).mockResolvedValue({ + ;(getConfig as jest.Mock).mockResolvedValue({ content: mockContent, repoRoot: '../' }) @@ -287,11 +274,10 @@ it('should handle content with packageName when package is not found anywhere', await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) const expectedContent = [ - { + { base: null, - version: null, - name: 'test', - packageName: '@patternfly/react-core', + name: 'test', + packageName: '@patternfly/react-core', pattern: '**/*.md' } ] @@ -308,12 +294,11 @@ it('should handle mixed content with both base and packageName entries', async ( { name: 'docs', base: 'src/docs', pattern: '**/*.md' }, { name: 'components', packageName: '@patternfly/react-core', pattern: '**/*.md' } ] - ;(getConfig as jest.Mock).mockResolvedValue({ + ;(getConfig as jest.Mock).mockResolvedValue({ content: mockContent, repoRoot: '../' }) ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ version: '6.2.2' })) ;(writeFile as jest.Mock).mockResolvedValue(undefined) const mockConsoleError = jest.fn() @@ -322,12 +307,11 @@ it('should handle mixed content with both base and packageName entries', async ( await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) const expectedContent = [ - { name: 'docs', base: '/config/dir/src/docs', pattern: '**/*.md', version: 'v6' }, - { + { name: 'docs', base: '/config/dir/src/docs', pattern: '**/*.md' }, + { base: '/config/dir/node_modules/@patternfly/react-core', - version: 'v6', - name: 'components', - packageName: '@patternfly/react-core', + name: 'components', + packageName: '@patternfly/react-core', pattern: '**/*.md' } ] @@ -338,173 +322,3 @@ it('should handle mixed content with both base and packageName entries', async ( ) expect(mockConsoleError).not.toHaveBeenCalled() }) - -describe('getPackageVersion function', () => { - it('should extract major version from valid package.json with version 6.2.2', async () => { - const mockContent = [ - { name: 'test', base: 'src/docs', pattern: '**/*.md' } - ] - ;(getConfig as jest.Mock).mockResolvedValue({ - content: mockContent, - repoRoot: '.' - }) - ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ version: '6.2.2' })) - ;(writeFile as jest.Mock).mockResolvedValue(undefined) - - await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) - - const expectedContent = [ - { name: 'test', base: '/config/dir/src/docs', pattern: '**/*.md', version: 'v6' } - ] - - expect(writeFile).toHaveBeenCalledWith( - '/foo/src/content.ts', - `export const content = ${JSON.stringify(expectedContent)}`, - ) - }) - - it('should extract major version from valid package.json with version 5.1.0', async () => { - const mockContent = [ - { name: 'test', base: 'src/docs', pattern: '**/*.md' } - ] - ;(getConfig as jest.Mock).mockResolvedValue({ - content: mockContent, - repoRoot: '.' - }) - ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ version: '5.1.0' })) - ;(writeFile as jest.Mock).mockResolvedValue(undefined) - - await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) - - const expectedContent = [ - { name: 'test', base: '/config/dir/src/docs', pattern: '**/*.md', version: 'v5' } - ] - - expect(writeFile).toHaveBeenCalledWith( - '/foo/src/content.ts', - `export const content = ${JSON.stringify(expectedContent)}`, - ) - }) - - it('should return null version when package.json does not exist', async () => { - const mockContent = [ - { name: 'test', base: 'src/docs', pattern: '**/*.md' } - ] - ;(getConfig as jest.Mock).mockResolvedValue({ - content: mockContent, - repoRoot: '.' - }) - ;(existsSync as jest.Mock).mockReturnValue(false) - ;(writeFile as jest.Mock).mockResolvedValue(undefined) - - await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) - - const expectedContent = [ - { name: 'test', base: '/config/dir/src/docs', pattern: '**/*.md', version: null } - ] - - expect(writeFile).toHaveBeenCalledWith( - '/foo/src/content.ts', - `export const content = ${JSON.stringify(expectedContent)}`, - ) - }) - - it('should return null version when package.json has no version field', async () => { - const mockContent = [ - { name: 'test', base: 'src/docs', pattern: '**/*.md' } - ] - ;(getConfig as jest.Mock).mockResolvedValue({ - content: mockContent, - repoRoot: '.' - }) - ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ name: 'test-package' })) - ;(writeFile as jest.Mock).mockResolvedValue(undefined) - - await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) - - const expectedContent = [ - { name: 'test', base: '/config/dir/src/docs', pattern: '**/*.md', version: null } - ] - - expect(writeFile).toHaveBeenCalledWith( - '/foo/src/content.ts', - `export const content = ${JSON.stringify(expectedContent)}`, - ) - }) - - it('should handle malformed package.json gracefully', async () => { - const mockContent = [ - { name: 'test', base: 'src/docs', pattern: '**/*.md' } - ] - ;(getConfig as jest.Mock).mockResolvedValue({ - content: mockContent, - repoRoot: '.' - }) - ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readFileSync as jest.Mock).mockReturnValue('{ invalid json }') - ;(writeFile as jest.Mock).mockResolvedValue(undefined) - - const mockConsoleError = jest.fn() - jest.spyOn(console, 'error').mockImplementation(mockConsoleError) - - await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) - - const expectedContent = [ - { name: 'test', base: '/config/dir/src/docs', pattern: '**/*.md', version: null } - ] - - expect(writeFile).toHaveBeenCalledWith( - '/foo/src/content.ts', - `export const content = ${JSON.stringify(expectedContent)}`, - ) - }) - - it('should handle version with pre-release tags (e.g., 6.0.0-beta.1)', async () => { - const mockContent = [ - { name: 'test', base: 'src/docs', pattern: '**/*.md' } - ] - ;(getConfig as jest.Mock).mockResolvedValue({ - content: mockContent, - repoRoot: '.' - }) - ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ version: '6.0.0-beta.1' })) - ;(writeFile as jest.Mock).mockResolvedValue(undefined) - - await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', false) - - const expectedContent = [ - { name: 'test', base: '/config/dir/src/docs', pattern: '**/*.md', version: 'v6' } - ] - - expect(writeFile).toHaveBeenCalledWith( - '/foo/src/content.ts', - `export const content = ${JSON.stringify(expectedContent)}`, - ) - }) - - it('should log version extraction in verbose mode', async () => { - const mockContent = [ - { name: 'test', base: 'src/docs', pattern: '**/*.md' } - ] - ;(getConfig as jest.Mock).mockResolvedValue({ - content: mockContent, - repoRoot: '.' - }) - ;(existsSync as jest.Mock).mockReturnValue(true) - ;(readFileSync as jest.Mock).mockReturnValue(JSON.stringify({ version: '6.2.2' })) - ;(writeFile as jest.Mock).mockResolvedValue(undefined) - - const mockConsoleLog = jest.fn() - jest.spyOn(console, 'log').mockImplementation(mockConsoleLog) - - await createCollectionContent('/foo/', '/config/dir/pf-docs.config.mjs', true) - - expect(mockConsoleLog).toHaveBeenCalledWith( - 'Extracted version v6 from /config/dir/src/docs/package.json\n' - ) - }) -}) diff --git a/cli/createCollectionContent.ts b/cli/createCollectionContent.ts index 6ce6828..b5eb2e4 100644 --- a/cli/createCollectionContent.ts +++ b/cli/createCollectionContent.ts @@ -2,7 +2,7 @@ import { writeFile } from 'fs/promises' import { dirname, join, resolve } from 'path' import { getConfig } from './getConfig.js' -import { existsSync, readFileSync } from 'fs' +import { existsSync } from 'fs' /** * Looks for the specified package in the node_modules directory in the configDir, then recursively @@ -42,44 +42,6 @@ const findPackage = ( return findPackage(parentDir, packageName, repoRootDir, verbose) } -/** - * Extracts the major version from a package.json file - */ -const getPackageVersion = (packagePath: string | null, verbose?: boolean) => { - if (!packagePath) { - return null - } - - const packageJsonPath = join(packagePath, 'package.json') - - if (!existsSync(packageJsonPath)) { - return null - } - - try { - const packageJsonContent = readFileSync(packageJsonPath, 'utf-8') - const packageJson = JSON.parse(packageJsonContent) - const version = packageJson.version - - if (version) { - // Extract major version (e.g., "6.2.2" -> "v6") - const majorVersion = `v${version.split('.')[0]}` - if (verbose) { - console.log( - `Extracted version ${majorVersion} from ${packageJsonPath}\n`, - ) - } - return majorVersion - } - } catch (error) { - if (verbose) { - console.error(`Error reading package.json at ${packageJsonPath}:`, error) - } - } - - return null -} - export async function createCollectionContent( astroRoot: string, configFileLocation: string, @@ -124,12 +86,9 @@ export async function createCollectionContent( verboseModeLog('relative path: ', contentEntry.base) verboseModeLog('absolute path: ', absoluteBase, '\n') - const version = getPackageVersion(absoluteBase, verbose) - return { ...contentEntry, base: absoluteBase, - version, } } @@ -144,7 +103,6 @@ export async function createCollectionContent( return { ...contentEntry, base: null, - version: null, } } @@ -155,11 +113,8 @@ export async function createCollectionContent( verbose, ) - const version = getPackageVersion(packagePath, verbose) - return { base: packagePath, - version, ...contentEntry, } }) diff --git a/cli/getConfig.ts b/cli/getConfig.ts index 7028245..25ffe6a 100644 --- a/cli/getConfig.ts +++ b/cli/getConfig.ts @@ -4,7 +4,6 @@ export interface CollectionDefinition { packageName?: string pattern: string name: string - version?: string } export interface PropsGlobs { diff --git a/jest.config.ts b/jest.config.ts index 4c5ea87..b4cdbe0 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -13,10 +13,8 @@ const config: Config = { '^.+\\.m?jsx?$': 'babel-jest', }, testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$', - testPathIgnorePatterns: ['/node_modules/', 'testHelpers\\.ts$'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], moduleNameMapper: { - '^astro:content$': '/src/__mocks__/astro-content.ts', '\\.(css|less)$': '/src/__mocks__/styleMock.ts', '(.+)\\.js': '$1', }, diff --git a/src/__mocks__/astro-content.ts b/src/__mocks__/astro-content.ts deleted file mode 100644 index 485a1ae..0000000 --- a/src/__mocks__/astro-content.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Default mock for Astro's content collections module - * Individual tests override this with jest.mock() for specific behavior - */ - -export const getCollection = jest.fn(() => Promise.resolve([])) diff --git a/src/pages/api/[version].ts b/src/pages/api/[version].ts deleted file mode 100644 index ce1c1fd..0000000 --- a/src/pages/api/[version].ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { APIRoute } from 'astro' -import type { CollectionEntry, CollectionKey } from 'astro:content' -import { getCollection } from 'astro:content' -import { content } from '../../content' -import { createJsonResponse } from '../../utils/apiHelpers' - -export const prerender = false - -type ContentEntry = CollectionEntry< - 'core-docs' | 'quickstarts-docs' | 'react-component-docs' -> - -export const GET: APIRoute = async ({ params }) => { - const { version } = params - - if (!version) { - return createJsonResponse( - { error: 'Version parameter is required' }, - 400, - ) - } - - // Build version map: collection name -> version - const versionMap = new Map() - content.forEach((entry) => { - if (entry.version) { - versionMap.set(entry.name, entry.version) - } - }) - - // Get collections that match the version - const collectionsToFetch = content - .filter((entry) => entry.version === version) - .map((entry) => entry.name as CollectionKey) - - if (collectionsToFetch.length === 0) { - return createJsonResponse({ error: `Version '${version}' not found` }, 404) - } - - const collections = await Promise.all( - collectionsToFetch.map(async (name) => await getCollection(name)), - ) - - const sections = new Set() - collections.flat().forEach((entry: ContentEntry) => { - if (entry.data.section) { - sections.add(entry.data.section) - } - }) - - return createJsonResponse(Array.from(sections).sort()) -} diff --git a/src/pages/api/[version]/[section].ts b/src/pages/api/[version]/[section].ts deleted file mode 100644 index f934a78..0000000 --- a/src/pages/api/[version]/[section].ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { APIRoute } from 'astro' -import type { CollectionEntry, CollectionKey } from 'astro:content' -import { getCollection } from 'astro:content' -import { content } from '../../../content' -import { kebabCase } from '../../../utils' -import { createJsonResponse } from '../../../utils/apiHelpers' - -export const prerender = false - -type ContentEntry = CollectionEntry< - 'core-docs' | 'quickstarts-docs' | 'react-component-docs' -> - -export const GET: APIRoute = async ({ params }) => { - const { version, section } = params - - if (!version || !section) { - return createJsonResponse( - { error: 'Version and section parameters are required' }, - 400, - ) - } - - // Get collections that match the version - const collectionsToFetch = content - .filter((entry) => entry.version === version) - .map((entry) => entry.name as CollectionKey) - - if (collectionsToFetch.length === 0) { - return createJsonResponse({ error: `Version '${version}' not found` }, 404) - } - - const collections = await Promise.all( - collectionsToFetch.map(async (name) => await getCollection(name)), - ) - - const pages = new Set() - collections.flat().forEach((entry: ContentEntry) => { - if (entry.data.section === section) { - pages.add(kebabCase(entry.data.id)) - } - }) - - if (pages.size === 0) { - return createJsonResponse( - { error: `Section '${section}' not found for version '${version}'` }, - 404, - ) - } - - return createJsonResponse(Array.from(pages).sort()) -} diff --git a/src/pages/api/[version]/[section]/[page].ts b/src/pages/api/[version]/[section]/[page].ts deleted file mode 100644 index c5dda71..0000000 --- a/src/pages/api/[version]/[section]/[page].ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { APIRoute } from 'astro' -import type { CollectionEntry, CollectionKey } from 'astro:content' -import { getCollection } from 'astro:content' -import { content } from '../../../../content' -import { - kebabCase, - getDefaultTab, - addDemosOrDeprecated, -} from '../../../../utils' -import { createJsonResponse } from '../../../../utils/apiHelpers' - -export const prerender = false - -type ContentEntry = CollectionEntry< - 'core-docs' | 'quickstarts-docs' | 'react-component-docs' -> - -export const GET: APIRoute = async ({ params }) => { - const { version, section, page } = params - - if (!version || !section || !page) { - return createJsonResponse( - { error: 'Version, section, and page parameters are required' }, - 400, - ) - } - - // Get collections that match the version - const collectionsToFetch = content - .filter((entry) => entry.version === version) - .map((entry) => entry.name as CollectionKey) - - if (collectionsToFetch.length === 0) { - return createJsonResponse({ error: `Version '${version}' not found` }, 404) - } - - const collections = await Promise.all( - collectionsToFetch.map(async (name) => await getCollection(name)), - ) - - // Build tabs dictionary similar to the main page logic - const tabsDictionary: Record = collections - .flat() - .reduce((acc: Record, entry: ContentEntry) => { - const { tab: specifiedTab, source } = entry.data - const hasTab = !!specifiedTab || !!source - let tab = specifiedTab - - if (!hasTab) { - tab = getDefaultTab(entry.filePath) - } else { - tab = addDemosOrDeprecated(specifiedTab, entry.id) - } - - const tabEntry = acc[entry.data.id] - - if (tabEntry === undefined) { - acc[entry.data.id] = [tab] - } else if (!tabEntry.includes(tab)) { - acc[entry.data.id] = [...tabEntry, tab] - } - - return acc - }, {}) - - // Sort tabs - const defaultOrder = 50 - const sourceOrder: Record = { - react: 1, - 'react-next': 1.1, - 'react-demos': 2, - 'react-deprecated': 2.1, - html: 3, - 'html-demos': 4, - 'design-guidelines': 99, - accessibility: 100, - 'upgrade-guide': 101, - 'release-notes': 102, - } - - const sortSources = (s1: string, s2: string) => { - const s1Index = sourceOrder[s1] || defaultOrder - const s2Index = sourceOrder[s2] || defaultOrder - if (s1Index === defaultOrder && s2Index === defaultOrder) { - return s1.localeCompare(s2) - } - return s1Index > s2Index ? 1 : -1 - } - - Object.values(tabsDictionary).forEach((tabs: string[]) => { - tabs.sort(sortSources) - }) - - // Find matching page - const flatEntries = collections.flat() - const matchingEntry = flatEntries.find( - (entry: ContentEntry) => - entry.data.section === section && kebabCase(entry.data.id) === page, - ) - - if (!matchingEntry) { - return createJsonResponse( - { - error: `Page '${page}' not found in section '${section}' for version '${version}'`, - }, - 404, - ) - } - - const tabs = tabsDictionary[matchingEntry.data.id] || [] - - return createJsonResponse(tabs) -} diff --git a/src/pages/api/[version]/[section]/[page]/[tab].ts b/src/pages/api/[version]/[section]/[page]/[tab].ts deleted file mode 100644 index f3bbc7e..0000000 --- a/src/pages/api/[version]/[section]/[page]/[tab].ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { APIRoute } from 'astro' -import type { CollectionEntry, CollectionKey } from 'astro:content' -import { getCollection } from 'astro:content' -import { content } from '../../../../../content' -import { kebabCase, getDefaultTab, addDemosOrDeprecated } from '../../../../../utils' -import { createJsonResponse, createTextResponse } from '../../../../../utils/apiHelpers' - -export const prerender = false - -type ContentEntry = CollectionEntry< - 'core-docs' | 'quickstarts-docs' | 'react-component-docs' -> - -export const GET: APIRoute = async ({ params }) => { - const { version, section, page, tab } = params - - if (!version || !section || !page || !tab) { - return createJsonResponse( - { error: 'Version, section, page, and tab parameters are required' }, - 400, - ) - } - - // Get collections that match the version - const collectionsToFetch = content - .filter((entry) => entry.version === version) - .map((entry) => entry.name as CollectionKey) - - if (collectionsToFetch.length === 0) { - return createJsonResponse({ error: `Version '${version}' not found` }, 404) - } - - const collections = await Promise.all( - collectionsToFetch.map(async (name) => await getCollection(name)), - ) - - const flatEntries = collections - .flat() - .map(({ data, filePath, ...rest }) => ({ - filePath, - ...rest, - data: { - ...data, - tab: data.tab || data.source || getDefaultTab(filePath), - }, - })) - - // Find the matching entry - const matchingEntry = flatEntries.find((entry: ContentEntry) => { - const entryTab = addDemosOrDeprecated(entry.data.tab, entry.id) - return ( - entry.data.section === section && - kebabCase(entry.data.id) === page && - entryTab === tab - ) - }) - - if (!matchingEntry) { - return createJsonResponse( - { - error: `Tab '${tab}' not found for page '${page}' in section '${section}' for version '${version}'`, - }, - 404, - ) - } - - // Get the raw body content (markdown/mdx text) - const textContent = matchingEntry.body || '' - - return createTextResponse(textContent) -} diff --git a/src/pages/api/__tests__/[version].test.ts b/src/pages/api/__tests__/[version].test.ts deleted file mode 100644 index 0e993be..0000000 --- a/src/pages/api/__tests__/[version].test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { GET } from '../[version]' - -/** - * Mock content collections with multiple versions (v5, v6) - * to test version filtering across different collection types - */ -jest.mock('../../../content', () => { - const { mockContentCollections } = jest.requireActual('./testHelpers') - return { content: mockContentCollections.v6Extended } -}) - -/** - * Mock Astro's getCollection to return different sections - * based on collection name, simulating real content structure - */ -jest.mock('astro:content', () => { - const { createGetCollectionMock } = jest.requireActual('./testHelpers') - return { - getCollection: createGetCollectionMock({ - 'react-component-docs': [ - { data: { section: 'components', id: 'alert' } }, - { data: { section: 'components', id: 'button' } }, - { data: { section: 'layouts', id: 'grid' } }, - ], - 'core-docs': [ - { data: { section: 'components', id: 'badge' } }, - { data: { section: 'utilities', id: 'spacing' } }, - ], - 'quickstarts-docs': [ - { data: { section: 'getting-started', id: 'intro' } }, - ], - }), - } -}) - -beforeEach(() => { - jest.clearAllMocks() -}) - -it('returns all sections for a valid version', async () => { - const response = await GET({ - params: { version: 'v6' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(200) - expect(response.headers.get('Content-Type')).toBe('application/json; charset=utf-8') - expect(Array.isArray(body)).toBe(true) - expect(body).toContain('components') - expect(body).toContain('layouts') - expect(body).toContain('utilities') -}) - -it('returns only sections for the requested version', async () => { - const response = await GET({ - params: { version: 'v5' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(200) - expect(body).toContain('getting-started') -}) - -it('sorts sections alphabetically', async () => { - const response = await GET({ - params: { version: 'v6' }, - } as any) - const body = await response.json() - - const sorted = [...body].sort() - expect(body).toEqual(sorted) -}) - -it('deduplicates sections from multiple collections', async () => { - const response = await GET({ - params: { version: 'v6' }, - } as any) - const body = await response.json() - - const unique = [...new Set(body)] - expect(body).toEqual(unique) -}) - -it('returns 404 error for nonexistent version', async () => { - const response = await GET({ - params: { version: 'v99' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') - expect(body.error).toContain('v99') - expect(body.error).toContain('not found') -}) - -it('returns 400 error when version parameter is missing', async () => { - const response = await GET({ - params: {}, - } as any) - const body = await response.json() - - expect(response.status).toBe(400) - expect(body).toHaveProperty('error') - expect(body.error).toContain('Version parameter is required') -}) - -it('excludes content entries that have no section field', async () => { - const response = await GET({ - params: { version: 'v6' }, - } as any) - const body = await response.json() - - // Should only include sections from entries that have data.section - expect(body.length).toBeGreaterThan(0) -}) diff --git a/src/pages/api/__tests__/[version]/[section].test.ts b/src/pages/api/__tests__/[version]/[section].test.ts deleted file mode 100644 index 7f23648..0000000 --- a/src/pages/api/__tests__/[version]/[section].test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { GET } from '../../[version]/[section]' - -/** - * Mock content collections with pages across different sections - * to test section filtering and page aggregation - */ -jest.mock('../../../../content', () => { - const { mockContentCollections } = jest.requireActual('../testHelpers') - return { content: mockContentCollections.v6Extended } -}) - -/** - * Mock getCollection to return pages with different sections - * simulating component, layout, and utility pages - */ -jest.mock('astro:content', () => { - const { createGetCollectionMock } = jest.requireActual('../testHelpers') - return { - getCollection: createGetCollectionMock({ - 'react-component-docs': [ - { data: { section: 'components', id: 'Alert' } }, - { data: { section: 'components', id: 'Button' } }, - { data: { section: 'layouts', id: 'Grid' } }, - ], - 'core-docs': [ - { data: { section: 'components', id: 'Badge' } }, - { data: { section: 'utilities', id: 'Spacing' } }, - ], - 'quickstarts-docs': [ - { data: { section: 'getting-started', id: 'Intro' } }, - ], - }), - } -}) - -/** - * Mock kebabCase utility to convert page IDs to URL-friendly format - */ -jest.mock('../../../../utils', () => { - const { mockUtils } = jest.requireActual('../testHelpers') - return { kebabCase: mockUtils.kebabCase } -}) - -beforeEach(() => { - jest.clearAllMocks() -}) - -it('returns all pages within a section', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(200) - expect(response.headers.get('Content-Type')).toBe('application/json; charset=utf-8') - expect(Array.isArray(body)).toBe(true) - expect(body).toContain('alert') - expect(body).toContain('button') - expect(body).toContain('badge') -}) - -it('formats page IDs as kebab-case', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components' }, - } as any) - const body = await response.json() - - // All page IDs should be kebab-cased - body.forEach((pageId: string) => { - expect(pageId).toBe(pageId.toLowerCase()) - expect(pageId).not.toContain(' ') - }) -}) - -it('sorts pages alphabetically', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components' }, - } as any) - const body = await response.json() - - const sorted = [...body].sort() - expect(body).toEqual(sorted) -}) - -it('deduplicates page IDs from multiple collections', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components' }, - } as any) - const body = await response.json() - - const unique = [...new Set(body)] - expect(body).toEqual(unique) -}) - -it('returns 404 error for nonexistent version', async () => { - const response = await GET({ - params: { version: 'v99', section: 'components' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') - expect(body.error).toContain('v99') -}) - -it('returns 404 error for nonexistent section', async () => { - const response = await GET({ - params: { version: 'v6', section: 'nonexistent' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') - expect(body.error).toContain('nonexistent') -}) - -it('returns 400 error when required parameters are missing', async () => { - const response = await GET({ - params: { version: 'v6' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(400) - expect(body).toHaveProperty('error') - expect(body.error).toContain('required') -}) - -it('filters pages to only the specified section', async () => { - const response = await GET({ - params: { version: 'v6', section: 'layouts' }, - } as any) - const body = await response.json() - - expect(body).toContain('grid') - expect(body).not.toContain('alert') - expect(body).not.toContain('button') -}) diff --git a/src/pages/api/__tests__/[version]/[section]/[page].test.ts b/src/pages/api/__tests__/[version]/[section]/[page].test.ts deleted file mode 100644 index d16c47c..0000000 --- a/src/pages/api/__tests__/[version]/[section]/[page].test.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { GET } from '../../../[version]/[section]/[page]' - -/** - * Mock content collections with pages that have multiple tabs - * to test tab aggregation and sorting from different collections - */ -jest.mock('../../../../../content', () => { - const { mockContentCollections } = jest.requireActual('../../testHelpers') - return { content: mockContentCollections.v6 } -}) - -/** - * Mock getCollection to return entries with various tab configurations - * simulating React, HTML, and design guideline tabs - */ -jest.mock('astro:content', () => { - const { - mockCollectionEntries, - createGetCollectionMock, - } = jest.requireActual('../../testHelpers') - return { - getCollection: createGetCollectionMock({ - 'react-component-docs': mockCollectionEntries['react-component-docs'], - 'core-docs': mockCollectionEntries['core-docs'], - }), - } -}) - -/** - * Mock utilities for tab name transformation and sorting - */ -jest.mock('../../../../../utils', () => { - const { mockUtils } = jest.requireActual('../../testHelpers') - return mockUtils -}) - -beforeEach(() => { - jest.clearAllMocks() -}) - -it('returns all tabs available for a page', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components', page: 'alert' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(200) - expect(response.headers.get('Content-Type')).toBe('application/json; charset=utf-8') - expect(Array.isArray(body)).toBe(true) - expect(body.length).toBeGreaterThan(0) -}) - -it('returns tab slugs as strings', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components', page: 'alert' }, - } as any) - const body = await response.json() - - body.forEach((tab: any) => { - expect(typeof tab).toBe('string') - }) -}) - -it('sorts tabs by priority (React before HTML, design guidelines last)', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components', page: 'alert' }, - } as any) - const body = await response.json() - - // Check that tabs are in expected order (react before html, design guidelines near end) - const reactIndex = body.indexOf('react') - const htmlIndex = body.indexOf('html') - - if (reactIndex !== -1 && htmlIndex !== -1) { - expect(reactIndex).toBeLessThan(htmlIndex) - } -}) - -it('returns 404 error for nonexistent version', async () => { - const response = await GET({ - params: { version: 'v99', section: 'components', page: 'alert' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') - expect(body.error).toContain('v99') -}) - -it('returns 404 error for nonexistent section', async () => { - const response = await GET({ - params: { version: 'v6', section: 'invalid', page: 'alert' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') -}) - -it('returns 404 error for nonexistent page', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components', page: 'nonexistent' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') - expect(body.error).toContain('nonexistent') -}) - -it('returns 400 error when required parameters are missing', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components' }, - } as any) - const body = await response.json() - - expect(response.status).toBe(400) - expect(body).toHaveProperty('error') - expect(body.error).toContain('required') -}) - -it('aggregates tabs from multiple collections for the same page', async () => { - const response = await GET({ - params: { version: 'v6', section: 'components', page: 'alert' }, - } as any) - const body = await response.json() - - // Should have tabs from both react-component-docs and core-docs - expect(body.length).toBeGreaterThan(1) -}) diff --git a/src/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts b/src/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts deleted file mode 100644 index b59d33e..0000000 --- a/src/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { GET } from '../../../../[version]/[section]/[page]/[tab]' - -/** - * Mock content collections with entries that have body content - * to test markdown/MDX content retrieval - */ -jest.mock('../../../../../../content', () => { - const { mockContentCollections } = jest.requireActual('../../../testHelpers') - return { content: mockContentCollections.v6 } -}) - -/** - * Mock getCollection to return entries with body (markdown content) - * simulating real documentation pages with content - */ -jest.mock('astro:content', () => { - const { mockEntriesWithBody, createGetCollectionMock } = jest.requireActual( - '../../../testHelpers', - ) - return { - getCollection: createGetCollectionMock({ - 'react-component-docs': mockEntriesWithBody['react-component-docs'], - 'core-docs': mockEntriesWithBody['core-docs'], - }), - } -}) - -/** - * Mock utilities for tab identification and transformation - */ -jest.mock('../../../../../../utils', () => { - const { mockUtils } = jest.requireActual('../../../testHelpers') - return mockUtils -}) - -beforeEach(() => { - jest.clearAllMocks() -}) - -it('returns markdown/MDX content as plain text', async () => { - const response = await GET({ - params: { - version: 'v6', - section: 'components', - page: 'alert', - tab: 'react', - }, - } as any) - const body = await response.text() - - expect(response.status).toBe(200) - expect(response.headers.get('Content-Type')).toBe('text/plain; charset=utf-8') - expect(typeof body).toBe('string') - expect(body).toContain('Alert Component') -}) - -it('returns different content for different tabs', async () => { - const reactResponse = await GET({ - params: { - version: 'v6', - section: 'components', - page: 'alert', - tab: 'react', - }, - } as any) - const reactBody = await reactResponse.text() - - const htmlResponse = await GET({ - params: { - version: 'v6', - section: 'components', - page: 'alert', - tab: 'html', - }, - } as any) - const htmlBody = await htmlResponse.text() - - expect(reactBody).toContain('React Alert') - expect(htmlBody).toContain('HTML') - expect(reactBody).not.toEqual(htmlBody) -}) - -it('returns demo content for demos tabs', async () => { - const response = await GET({ - params: { - version: 'v6', - section: 'components', - page: 'alert', - tab: 'react-demos', - }, - } as any) - const body = await response.text() - - expect(response.status).toBe(200) - expect(body).toContain('demos') -}) - -it('returns 404 error for nonexistent version', async () => { - const response = await GET({ - params: { - version: 'v99', - section: 'components', - page: 'alert', - tab: 'react', - }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') - expect(body.error).toContain('v99') -}) - -it('returns 404 error for nonexistent section', async () => { - const response = await GET({ - params: { - version: 'v6', - section: 'invalid', - page: 'alert', - tab: 'react', - }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') -}) - -it('returns 404 error for nonexistent page', async () => { - const response = await GET({ - params: { - version: 'v6', - section: 'components', - page: 'nonexistent', - tab: 'react', - }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') - expect(body.error).toContain('nonexistent') -}) - -it('returns 404 error for nonexistent tab', async () => { - const response = await GET({ - params: { - version: 'v6', - section: 'components', - page: 'alert', - tab: 'nonexistent', - }, - } as any) - const body = await response.json() - - expect(response.status).toBe(404) - expect(body).toHaveProperty('error') - expect(body.error).toContain('nonexistent') -}) - -it('returns 400 error when required parameters are missing', async () => { - const response = await GET({ - params: { - version: 'v6', - section: 'components', - page: 'alert', - }, - } as any) - const body = await response.json() - - expect(response.status).toBe(400) - expect(body).toHaveProperty('error') - expect(body.error).toContain('required') -}) diff --git a/src/pages/api/__tests__/testHelpers.ts b/src/pages/api/__tests__/testHelpers.ts deleted file mode 100644 index 2a9647d..0000000 --- a/src/pages/api/__tests__/testHelpers.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Shared test helpers and mock data for API endpoint tests - * - * Note: This file is named testHelpers.ts (not testUtils.ts) to avoid - * Jest attempting to run it as a test file. - */ - -/** - * Common mock data for content collections - */ -export const mockContentCollections = { - v6: [ - { name: 'react-component-docs', version: 'v6' }, - { name: 'core-docs', version: 'v6' }, - ], - v6Extended: [ - { name: 'react-component-docs', version: 'v6' }, - { name: 'core-docs', version: 'v6' }, - { name: 'quickstarts-docs', version: 'v5' }, - ], -} - -/** - * Mock collection entries for different test scenarios - */ -export const mockCollectionEntries = { - 'react-component-docs': [ - { - data: { section: 'components', id: 'Alert', tab: 'react' }, - id: 'alert-react', - filePath: '/path/to/alert-react.md', - }, - { - data: { section: 'components', id: 'Alert' }, - id: 'alert-react-demos', - filePath: '/path/to/demos/alert.md', - }, - { - data: { section: 'components', id: 'Button', tab: 'react' }, - id: 'button-react', - filePath: '/path/to/button-react.md', - }, - ], - 'react-component-docs-with-layouts': [ - { data: { section: 'components', id: 'Alert' } }, - { data: { section: 'components', id: 'Button' } }, - { data: { section: 'layouts', id: 'Grid' } }, - ], - 'core-docs': [ - { - data: { section: 'components', id: 'Alert', tab: 'html' }, - id: 'alert-html', - filePath: '/path/to/alert-html.md', - }, - { - data: { - section: 'components', - id: 'Alert', - tabName: 'Design guidelines', - }, - id: 'alert-design', - filePath: '/path/to/alert-design.md', - }, - ], - 'core-docs-with-utilities': [ - { data: { section: 'components', id: 'Badge' } }, - { data: { section: 'utilities', id: 'Spacing' } }, - ], - 'quickstarts-docs': [{ data: { section: 'getting-started', id: 'Intro' } }], -} - -/** - * Mock collection entries with markdown body content - */ -export const mockEntriesWithBody = { - 'react-component-docs': [ - { - data: { section: 'components', id: 'Alert', tab: 'react' }, - id: 'alert-react', - filePath: '/path/to/alert-react.md', - body: '# Alert Component\n\nThis is the React Alert component documentation.', - }, - { - data: { section: 'components', id: 'Alert' }, - id: 'alert-react-demos', - filePath: '/path/to/demos/alert.md', - body: '# Alert Demos\n\nExample demos for the Alert component.', - }, - { - data: { section: 'components', id: 'Button', tab: 'react' }, - id: 'button-react', - filePath: '/path/to/button-react.md', - body: '# Button Component\n\nThis is the React Button component documentation.', - }, - ], - 'core-docs': [ - { - data: { section: 'components', id: 'Alert', tab: 'html' }, - id: 'alert-html', - filePath: '/path/to/alert-html.md', - body: '# Alert HTML\n\nHTML implementation of the Alert component.', - }, - ], -} - -/** - * Factory for creating getCollection mock function - * Use this inside jest.mock() factory functions with require() - */ -export const createGetCollectionMock = (entries: Record) => - jest.fn((name: string) => Promise.resolve(entries[name] || [])) - -/** - * Mock utilities module with common transformations - */ -export const mockUtils = { - kebabCase: (str: string) => str.toLowerCase().replace(/\s+/g, '-'), - getDefaultTab: (filePath: string) => { - if (filePath.includes('demos')) { - return 'react-demos' - } - return 'react' - }, - addDemosOrDeprecated: (tab: string, id: string) => { - if (id?.includes('demos')) { - return 'react-demos' - } - return tab || 'react' - }, -} - -/** - * Mock tab names for display - */ -export const mockTabNames = { - react: 'React', - 'react-demos': 'React demos', - html: 'HTML', - 'design-guidelines': 'Design guidelines', -} diff --git a/src/pages/api/__tests__/versions.test.ts b/src/pages/api/__tests__/versions.test.ts deleted file mode 100644 index 6cf3604..0000000 --- a/src/pages/api/__tests__/versions.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { GET } from '../versions' - -/** - * Mock content with multiple collections at different versions - * to test version filtering and deduplication - */ -jest.mock('../../../content', () => ({ - content: [ - { name: 'test1', version: 'v6' }, - { name: 'test2', version: 'v6' }, - { name: 'test3', version: 'v5' }, - ], -})) - -it('returns unique versions as sorted array', async () => { - const response = await GET({} as any) - const body = await response.json() - - expect(response.status).toBe(200) - expect(response.headers.get('Content-Type')).toBe('application/json; charset=utf-8') - expect(body).toEqual(['v5', 'v6']) -}) - -it('sorts versions alphabetically', async () => { - const response = await GET({} as any) - const body = await response.json() - - expect(body).toEqual(['v5', 'v6']) -}) - -it('excludes content entries that have no version', async () => { - // Re-mock with entries that have null/undefined versions - jest.resetModules() - jest.mock('../../../content', () => ({ - content: [ - { name: 'test1', version: 'v6' }, - { name: 'test2', version: null }, - { name: 'test3', version: undefined }, - ], - })) - - const { GET } = jest.requireActual('../versions') - const response = await GET({} as any) - const body = await response.json() - - expect(body).toEqual(['v6']) -}) diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts deleted file mode 100644 index 247e5fb..0000000 --- a/src/pages/api/index.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { APIRoute } from 'astro' -import { createJsonResponse } from '../../utils/apiHelpers' - -export const prerender = false - -export const GET: APIRoute = async () => - createJsonResponse({ - name: 'PatternFly Documentation API', - description: 'Machine-readable documentation API for LLM agents and MCP servers', - version: '1.0.0', - baseUrl: '/text', - endpoints: [ - { - path: '/text', - method: 'GET', - description: 'Get API schema and documentation', - returns: { - type: 'object', - description: 'API schema with endpoints and usage information', - }, - }, - { - path: '/text/versions', - method: 'GET', - description: 'List available documentation versions', - returns: { - type: 'array', - items: 'string', - example: ['v6'], - }, - }, - { - path: '/text/openapi.json', - method: 'GET', - description: 'Get OpenAPI 3.0 specification', - returns: { - type: 'object', - description: 'Full OpenAPI 3.0 specification for this API', - }, - }, - { - path: '/text/{version}', - method: 'GET', - description: 'List available sections for a specific version', - parameters: [ - { - name: 'version', - in: 'path', - required: true, - type: 'string', - example: 'v6', - }, - ], - returns: { - type: 'array', - items: 'string', - example: ['components', 'layouts', 'utilities'], - }, - }, - { - path: '/text/{version}/{section}', - method: 'GET', - description: 'List available pages within a section', - parameters: [ - { - name: 'version', - in: 'path', - required: true, - type: 'string', - example: 'v6', - }, - { - name: 'section', - in: 'path', - required: true, - type: 'string', - example: 'components', - }, - ], - returns: { - type: 'array', - items: 'string', - description: 'Array of kebab-cased page IDs', - example: ['alert', 'button', 'card'], - }, - }, - { - path: '/text/{version}/{section}/{page}', - method: 'GET', - description: 'List available tabs for a page', - parameters: [ - { - name: 'version', - in: 'path', - required: true, - type: 'string', - example: 'v6', - }, - { - name: 'section', - in: 'path', - required: true, - type: 'string', - example: 'components', - }, - { - name: 'page', - in: 'path', - required: true, - type: 'string', - example: 'alert', - }, - ], - returns: { - type: 'array', - items: 'string', - description: 'Array of tab slugs', - example: ['react', 'react-demos', 'html'], - }, - }, - { - path: '/text/{version}/{section}/{page}/{tab}', - method: 'GET', - description: 'Get raw markdown/MDX content for a specific tab', - parameters: [ - { - name: 'version', - in: 'path', - required: true, - type: 'string', - example: 'v6', - }, - { - name: 'section', - in: 'path', - required: true, - type: 'string', - example: 'components', - }, - { - name: 'page', - in: 'path', - required: true, - type: 'string', - example: 'alert', - }, - { - name: 'tab', - in: 'path', - required: true, - type: 'string', - example: 'react', - }, - ], - returns: { - type: 'string', - contentType: 'text/plain; charset=utf-8', - description: 'Raw markdown/MDX documentation content', - }, - }, - ], - usage: { - description: 'Navigate the API hierarchically to discover and retrieve documentation', - exampleFlow: [ - 'GET /text/versions → ["v6"]', - 'GET /text/v6 → ["components", "layouts", ...]', - 'GET /text/v6/components → ["alert", "button", ...]', - 'GET /text/v6/components/alert → ["react", "html", ...]', - 'GET /text/v6/components/alert/react → (markdown content)', - ], - }, - }) diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts deleted file mode 100644 index ebb8bbf..0000000 --- a/src/pages/api/openapi.json.ts +++ /dev/null @@ -1,392 +0,0 @@ -import type { APIRoute } from 'astro' -import { content } from '../../content' - -export const prerender = false - -export const GET: APIRoute = async () => { - const versions = new Set() - content.forEach((entry) => { - if (entry.version) { - versions.add(entry.version) - } - }) - - const openApiSpec = { - openapi: '3.0.0', - info: { - title: 'PatternFly Documentation API', - description: - 'Machine-readable documentation API for LLM agents and MCP servers. Provides hierarchical access to PatternFly documentation content.', - version: '1.0.0', - contact: { - name: 'PatternFly', - url: 'https://patternfly.org', - }, - }, - servers: [ - { - url: '/api', - description: 'Documentation API base path', - }, - ], - paths: { - '/': { - get: { - summary: 'Get API documentation', - description: - 'Returns self-documenting API schema with complete endpoint descriptions, parameters, and usage examples', - operationId: 'getApiDocs', - responses: { - '200': { - description: 'API documentation schema', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - name: { type: 'string' }, - description: { type: 'string' }, - version: { type: 'string' }, - baseUrl: { type: 'string' }, - endpoints: { - type: 'array', - items: { type: 'object' }, - }, - usage: { type: 'object' }, - }, - }, - }, - }, - }, - }, - }, - }, - '/versions': { - get: { - summary: 'List available versions', - description: 'Returns an array of available documentation versions', - operationId: 'getVersions', - responses: { - '200': { - description: 'List of available versions', - content: { - 'application/json': { - schema: { - type: 'array', - items: { - type: 'string', - }, - example: Array.from(versions).sort(), - }, - }, - }, - }, - }, - }, - }, - '/openapi.json': { - get: { - summary: 'Get OpenAPI specification', - description: 'Returns the complete OpenAPI 3.0 specification for this API', - operationId: 'getOpenApiSpec', - responses: { - '200': { - description: 'OpenAPI 3.0 specification', - content: { - 'application/json': { - schema: { - type: 'object', - description: 'Full OpenAPI 3.0 specification', - }, - }, - }, - }, - }, - }, - }, - '/{version}': { - get: { - summary: 'List sections for a version', - description: - 'Returns an array of available documentation sections for the specified version', - operationId: 'getSections', - parameters: [ - { - name: 'version', - in: 'path', - required: true, - description: 'Documentation version', - schema: { - type: 'string', - enum: Array.from(versions).sort(), - }, - example: 'v6', - }, - ], - responses: { - '200': { - description: 'List of available sections', - content: { - 'application/json': { - schema: { - type: 'array', - items: { - type: 'string', - }, - }, - example: ['components', 'layouts', 'utilities'], - }, - }, - }, - '404': { - description: 'Version not found', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - error: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - }, - '/{version}/{section}': { - get: { - summary: 'List pages in a section', - description: - 'Returns an array of page IDs within the specified section', - operationId: 'getPages', - parameters: [ - { - name: 'version', - in: 'path', - required: true, - description: 'Documentation version', - schema: { - type: 'string', - enum: Array.from(versions).sort(), - }, - example: 'v6', - }, - { - name: 'section', - in: 'path', - required: true, - description: 'Documentation section', - schema: { - type: 'string', - }, - example: 'components', - }, - ], - responses: { - '200': { - description: 'List of page IDs (kebab-cased)', - content: { - 'application/json': { - schema: { - type: 'array', - items: { - type: 'string', - }, - }, - example: ['alert', 'button', 'card'], - }, - }, - }, - '404': { - description: 'Section not found', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - error: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - }, - '/{version}/{section}/{page}': { - get: { - summary: 'List tabs for a page', - description: - 'Returns an array of available tab slugs for the specified page', - operationId: 'getTabs', - parameters: [ - { - name: 'version', - in: 'path', - required: true, - description: 'Documentation version', - schema: { - type: 'string', - enum: Array.from(versions).sort(), - }, - example: 'v6', - }, - { - name: 'section', - in: 'path', - required: true, - description: 'Documentation section', - schema: { - type: 'string', - }, - example: 'components', - }, - { - name: 'page', - in: 'path', - required: true, - description: 'Page ID (kebab-cased)', - schema: { - type: 'string', - }, - example: 'alert', - }, - ], - responses: { - '200': { - description: 'List of available tab slugs', - content: { - 'application/json': { - schema: { - type: 'array', - items: { - type: 'string', - }, - }, - example: ['react', 'react-demos', 'html'], - }, - }, - }, - '404': { - description: 'Page not found', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - error: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - }, - '/{version}/{section}/{page}/{tab}': { - get: { - summary: 'Get tab content', - description: - 'Returns the raw markdown/MDX documentation content for the specified tab', - operationId: 'getContent', - parameters: [ - { - name: 'version', - in: 'path', - required: true, - description: 'Documentation version', - schema: { - type: 'string', - enum: Array.from(versions).sort(), - }, - example: 'v6', - }, - { - name: 'section', - in: 'path', - required: true, - description: 'Documentation section', - schema: { - type: 'string', - }, - example: 'components', - }, - { - name: 'page', - in: 'path', - required: true, - description: 'Page ID (kebab-cased)', - schema: { - type: 'string', - }, - example: 'alert', - }, - { - name: 'tab', - in: 'path', - required: true, - description: 'Tab slug', - schema: { - type: 'string', - }, - example: 'react', - }, - ], - responses: { - '200': { - description: 'Raw markdown/MDX content', - content: { - 'text/plain; charset=utf-8': { - schema: { - type: 'string', - }, - example: - '---\ntitle: Alert\nsection: components\n---\n\n## Overview\n\nAn alert is a notification that provides...', - }, - }, - }, - '404': { - description: 'Tab not found', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - error: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - tags: [ - { - name: 'Documentation', - description: 'PatternFly documentation endpoints', - }, - ], - } - - const body = JSON.stringify(openApiSpec, null, 2) - return new Response(body, { - status: 200, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - 'Cache-Control': 'no-cache', - Date: new Date().toUTCString(), - 'Content-Length': body.length.toString(), - }, - }) -} diff --git a/src/pages/api/versions.ts b/src/pages/api/versions.ts deleted file mode 100644 index 9361419..0000000 --- a/src/pages/api/versions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { APIRoute } from 'astro' -import { content } from '../../content' -import { createJsonResponse } from '../../utils/apiHelpers' - -export const prerender = false - -export const GET: APIRoute = async () => { - const versions = new Set() - - content.forEach((entry) => { - if (entry.version) { - versions.add(entry.version) - } - }) - - return createJsonResponse(Array.from(versions).sort()) -} diff --git a/src/utils/__tests__/apiHelpers.test.ts b/src/utils/__tests__/apiHelpers.test.ts deleted file mode 100644 index 452fd29..0000000 --- a/src/utils/__tests__/apiHelpers.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { createJsonResponse, createTextResponse } from '../apiHelpers' - -describe('createJsonResponse', () => { - it('returns a Response with status 200 by default', () => { - const response = createJsonResponse({ message: 'success' }) - expect(response.status).toBe(200) - }) - - it('returns a Response with custom status code', () => { - const response = createJsonResponse({ error: 'not found' }, 404) - expect(response.status).toBe(404) - }) - - it('sets correct Content-Type header with charset', () => { - const response = createJsonResponse({ test: 'data' }) - expect(response.headers.get('Content-Type')).toBe( - 'application/json; charset=utf-8', - ) - }) - - it('sets Cache-Control header to no-cache', () => { - const response = createJsonResponse({ test: 'data' }) - expect(response.headers.get('Cache-Control')).toBe('no-cache') - }) - - it('sets Date header', () => { - const response = createJsonResponse({ test: 'data' }) - const dateHeader = response.headers.get('Date') - expect(dateHeader).toBeTruthy() - // Verify it's a valid date string - expect(new Date(dateHeader!).toString()).not.toBe('Invalid Date') - }) - - it('sets Content-Length header to match body length', async () => { - const data = { message: 'test', count: 42 } - const response = createJsonResponse(data) - const body = await response.text() - expect(response.headers.get('Content-Length')).toBe(body.length.toString()) - }) - - it('correctly serializes an object', async () => { - const data = { message: 'hello', nested: { value: 123 } } - const response = createJsonResponse(data) - const body = await response.json() - expect(body).toEqual(data) - }) - - it('correctly serializes an array', async () => { - const data = ['one', 'two', 'three'] - const response = createJsonResponse(data) - const body = await response.json() - expect(body).toEqual(data) - }) - - it('correctly serializes a string', async () => { - const data = 'plain string' - const response = createJsonResponse(data) - const body = await response.json() - expect(body).toBe(data) - }) - - it('correctly serializes null', async () => { - const response = createJsonResponse(null) - const body = await response.json() - expect(body).toBeNull() - }) - - it('correctly serializes an empty object', async () => { - const response = createJsonResponse({}) - const body = await response.json() - expect(body).toEqual({}) - }) - - it('correctly serializes an empty array', async () => { - const response = createJsonResponse([]) - const body = await response.json() - expect(body).toEqual([]) - }) - - it('handles complex nested data structures', async () => { - const data = { - users: [ - { id: 1, name: 'Alice', tags: ['admin', 'user'] }, - { id: 2, name: 'Bob', tags: ['user'] }, - ], - meta: { total: 2, page: 1 }, - } - const response = createJsonResponse(data) - const body = await response.json() - expect(body).toEqual(data) - }) - - it('creates error responses with proper status codes', async () => { - const errorData = { error: 'Bad Request', message: 'Invalid input' } - const response = createJsonResponse(errorData, 400) - expect(response.status).toBe(400) - const body = await response.json() - expect(body).toEqual(errorData) - }) -}) - -describe('createTextResponse', () => { - it('returns a Response with status 200 by default', () => { - const response = createTextResponse('Hello, world!') - expect(response.status).toBe(200) - }) - - it('returns a Response with custom status code', () => { - const response = createTextResponse('Not found', 404) - expect(response.status).toBe(404) - }) - - it('sets correct Content-Type header with charset', () => { - const response = createTextResponse('test content') - expect(response.headers.get('Content-Type')).toBe( - 'text/plain; charset=utf-8', - ) - }) - - it('sets Cache-Control header to no-cache', () => { - const response = createTextResponse('test content') - expect(response.headers.get('Cache-Control')).toBe('no-cache') - }) - - it('sets Date header', () => { - const response = createTextResponse('test content') - const dateHeader = response.headers.get('Date') - expect(dateHeader).toBeTruthy() - // Verify it's a valid date string - expect(new Date(dateHeader!).toString()).not.toBe('Invalid Date') - }) - - it('sets Content-Length header to match content length', () => { - const content = 'This is test content' - const response = createTextResponse(content) - expect(response.headers.get('Content-Length')).toBe( - content.length.toString(), - ) - }) - - it('returns correct text content', async () => { - const content = 'Hello, world!' - const response = createTextResponse(content) - const body = await response.text() - expect(body).toBe(content) - }) - - it('handles empty string', async () => { - const response = createTextResponse('') - const body = await response.text() - expect(body).toBe('') - expect(response.headers.get('Content-Length')).toBe('0') - }) - - it('handles multiline text', async () => { - const content = 'Line 1\nLine 2\nLine 3' - const response = createTextResponse(content) - const body = await response.text() - expect(body).toBe(content) - }) - - it('handles unicode characters', async () => { - const content = 'Hello 世界 🌍' - const response = createTextResponse(content) - const body = await response.text() - expect(body).toBe(content) - }) - - it('handles markdown content', async () => { - const markdown = `# Heading - -## Subheading - -- Item 1 -- Item 2 - -\`\`\`javascript -const foo = 'bar'; -\`\`\` -` - const response = createTextResponse(markdown) - const body = await response.text() - expect(body).toBe(markdown) - }) - - it('creates error responses with proper status codes', async () => { - const errorText = 'Internal Server Error' - const response = createTextResponse(errorText, 500) - expect(response.status).toBe(500) - const body = await response.text() - expect(body).toBe(errorText) - }) -}) diff --git a/src/utils/apiHelpers.ts b/src/utils/apiHelpers.ts deleted file mode 100644 index 280e364..0000000 --- a/src/utils/apiHelpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -function getHeaders( - type: 'application/json' | 'text/plain', - contentLength?: number, -): HeadersInit { - return { - 'Content-Type': `${type}; charset=utf-8`, - 'Cache-Control': 'no-cache', - Date: new Date().toUTCString(), - 'Content-Length': contentLength?.toString() || '', - } -} - -export function createJsonResponse( - data: unknown, - status: number = 200, -): Response { - const body = JSON.stringify(data) - return new Response(body, { - status, - headers: getHeaders('application/json', body.length), - }) -} - -export function createTextResponse( - content: string, - status: number = 200, -): Response { - return new Response(content, { - status, - headers: getHeaders('text/plain', content.length), - }) -} diff --git a/test.setup.ts b/test.setup.ts index 7b20f57..52b79e9 100644 --- a/test.setup.ts +++ b/test.setup.ts @@ -1,53 +1,2 @@ // Add custom jest matchers from jest-dom import '@testing-library/jest-dom'; - -// Polyfill Response for API endpoint tests -if (typeof global.Response === 'undefined') { - global.Response = class Response { - body: any; - status: number; - statusText: string; - headers: { - _map: Map; - get: (key: string) => string | null; - }; - - constructor(body?: BodyInit | null, init?: ResponseInit) { - this.body = body; - this.status = init?.status || 200; - this.statusText = init?.statusText || ''; - - const headersMap = new Map(); - this.headers = { - _map: headersMap, - get: (key: string) => headersMap.get(key) || null, - }; - - if (init?.headers) { - if (init.headers instanceof Headers) { - init.headers.forEach((value, key) => { - headersMap.set(key, value); - }); - } else if (Array.isArray(init.headers)) { - init.headers.forEach(([key, value]) => { - headersMap.set(key, value); - }); - } else { - Object.entries(init.headers).forEach(([key, value]) => { - if (typeof value === 'string') { - headersMap.set(key, value); - } - }); - } - } - } - - async json() { - return JSON.parse(this.body as string); - } - - async text() { - return this.body as string; - } - } as any; -}