Skip to content

Commit 9fd87fb

Browse files
fix(API): address issues with text API in monorepos
1 parent 1e10053 commit 9fd87fb

File tree

8 files changed

+114
-12
lines changed

8 files changed

+114
-12
lines changed

README.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Any static assets, like images, can be placed in the `public/` directory.
3333

3434
To define the markdown schema this project uses a typescript based schema known as [Zod](https://zod.dev). Details of how this is integratred into Astro can be found in Astros documentation on [content creation using Zod](https://docs.astro.build/en/guides/content-collections/#defining-datatypes-with-zod).
3535

36+
Note: When running in dev mode locally, API endpoints are not available on a clean repo until either a build has been done or a tab route has been hit.
3637
### 🧞 Commands
3738

3839
All commands are run from the root of the project, from a terminal:

src/__tests__/pages/api/__tests__/[version]/[section]/[page]/[tab].test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jest.mock('../../../../../../../utils', () => ({
8686
.replace(/[\s_]+/g, '-')
8787
.toLowerCase()
8888
}),
89-
getDefaultTab: jest.fn((filePath?: string) => {
89+
getDefaultTabForApi: jest.fn((filePath?: string) => {
9090
if (!filePath) {
9191
return 'react'
9292
}

src/pages/api/[version]/[section]/[page]/[tab].ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { getCollection } from 'astro:content'
55
import { content } from '../../../../../content'
66
import {
77
kebabCase,
8-
getDefaultTab,
98
addDemosOrDeprecated,
9+
getDefaultTabForApi,
1010
} from '../../../../../utils'
1111
import { generateAndWriteApiIndex } from '../../../../../utils/apiIndex/generate'
1212
import { getApiIndex } from '../../../../../utils/apiIndex/get'
@@ -44,7 +44,15 @@ export const getStaticPaths: GetStaticPaths = async () => {
4444
}
4545
}
4646

47-
return paths
47+
// This shouldn't happen since we have a fallback tab value, but if it somehow does we need to alert the user
48+
paths.forEach((path) => {
49+
if (!path.params.tab) {
50+
console.warn(`[API Warning] Tab not found for path: ${path.params.version}/${path.params.section}/${path.params.page}`)
51+
}
52+
})
53+
54+
// Again, this shouldn't happen since we have a fallback tab value, but if it somehow does and we don't filter out tabless paths it will crash the build
55+
return paths.filter((path) => !!path.params.tab)
4856
}
4957

5058
export const GET: APIRoute = async ({ params }) => {
@@ -109,7 +117,7 @@ export const GET: APIRoute = async ({ params }) => {
109117
...rest,
110118
data: {
111119
...data,
112-
tab: data.tab || data.source || getDefaultTab(filePath),
120+
tab: data.tab || data.source || getDefaultTabForApi(filePath),
113121
},
114122
}))
115123

src/utils/__tests__/packageUtils.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getPackageName, getTabBase, getDefaultTab, addDemosOrDeprecated } from '../packageUtils'
1+
import { getPackageName, getTabBase, getDefaultTab, getDefaultTabForApi, addDemosOrDeprecated } from '../packageUtils'
22

33
describe('getPackageName', () => {
44
it('returns empty string for empty input', () => {
@@ -192,3 +192,70 @@ describe('getDefaultTab', () => {
192192
expect(getDefaultTab(filePath)).toBe('')
193193
})
194194
})
195+
196+
describe('getDefaultTabForApi', () => {
197+
it('returns base tab for regular patternfly package path', () => {
198+
const filePath = '/path/to/node_modules/@patternfly/patternfly/dist/index.js'
199+
expect(getDefaultTabForApi(filePath)).toBe('html')
200+
})
201+
202+
it('returns base tab for regular react-core package path', () => {
203+
const filePath = '/path/to/node_modules/@patternfly/react-core/dist/index.js'
204+
expect(getDefaultTabForApi(filePath)).toBe('react')
205+
})
206+
207+
it('returns demos tab for demos path with patternfly package', () => {
208+
const filePath = '/path/to/node_modules/@patternfly/patternfly/demos/Button.js'
209+
expect(getDefaultTabForApi(filePath)).toBe('html-demos')
210+
})
211+
212+
it('returns demos tab for demos path with react-core package', () => {
213+
const filePath = '/path/to/node_modules/@patternfly/react-core/demos/Button.js'
214+
expect(getDefaultTabForApi(filePath)).toBe('react-demos')
215+
})
216+
217+
it('returns deprecated tab for deprecated path with patternfly package', () => {
218+
const filePath = '/path/to/node_modules/@patternfly/patternfly/deprecated/OldButton.js'
219+
expect(getDefaultTabForApi(filePath)).toBe('html-deprecated')
220+
})
221+
222+
it('returns deprecated tab for deprecated path with react-core package', () => {
223+
const filePath = '/path/to/node_modules/@patternfly/react-core/deprecated/OldButton.js'
224+
expect(getDefaultTabForApi(filePath)).toBe('react-deprecated')
225+
})
226+
227+
it('adds both demos and deprecated when both are in path', () => {
228+
const filePath = '/path/to/node_modules/@patternfly/react-core/demos/deprecated/Button.js'
229+
expect(getDefaultTabForApi(filePath)).toBe('react-demos-deprecated')
230+
})
231+
232+
it('returns "text" fallback for unknown package', () => {
233+
const filePath = '/path/to/node_modules/unknown-package/dist/index.js'
234+
expect(getDefaultTabForApi(filePath)).toBe('text')
235+
})
236+
237+
it('returns "text" fallback for path without node_modules', () => {
238+
const filePath = '/path/to/some/file.js'
239+
expect(getDefaultTabForApi(filePath)).toBe('text')
240+
})
241+
242+
it('returns "text" fallback for empty input', () => {
243+
expect(getDefaultTabForApi('')).toBe('text')
244+
})
245+
246+
it('returns "text" fallback for null/undefined input', () => {
247+
expect(getDefaultTabForApi(null as any)).toBe('text')
248+
expect(getDefaultTabForApi(undefined)).toBe('text')
249+
expect(getDefaultTabForApi()).toBe('text')
250+
})
251+
252+
it('returns "text" fallback for unknown package with demos path', () => {
253+
const filePath = '/path/to/node_modules/unknown-package/demos/Button.js'
254+
expect(getDefaultTabForApi(filePath)).toBe('text')
255+
})
256+
257+
it('returns "text" fallback for unknown package with deprecated path', () => {
258+
const filePath = '/path/to/node_modules/unknown-package/deprecated/Button.js'
259+
expect(getDefaultTabForApi(filePath)).toBe('text')
260+
})
261+
})

src/utils/apiIndex/generate.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { writeFile } from 'fs/promises'
44
import { getCollection } from 'astro:content'
55
import type { CollectionKey } from 'astro:content'
66
import { content } from '../../content'
7-
import { kebabCase, getDefaultTab, addDemosOrDeprecated } from '../index'
7+
import { kebabCase, addDemosOrDeprecated } from '../index'
8+
import { getDefaultTabForApi } from '../packageUtils'
9+
import { getOutputDir } from '../getOutputDir'
810

911
const SOURCE_ORDER: Record<string, number> = {
1012
react: 1,
@@ -107,7 +109,7 @@ export async function generateApiIndex(): Promise<ApiIndex> {
107109

108110
// Collect tab
109111
const entryTab =
110-
entry.data.tab || entry.data.source || getDefaultTab(entry.filePath)
112+
entry.data.tab || entry.data.source || getDefaultTabForApi(entry.filePath)
111113
const tab = addDemosOrDeprecated(entryTab, entry.id)
112114
if (!pageTabs[pageKey]) {
113115
pageTabs[pageKey] = new Set()
@@ -137,7 +139,8 @@ export async function generateApiIndex(): Promise<ApiIndex> {
137139
* @param index - The API index structure to write
138140
*/
139141
export async function writeApiIndex(index: ApiIndex): Promise<void> {
140-
const indexPath = join(process.cwd(), 'src', 'apiIndex.json')
142+
const outputDir = await getOutputDir()
143+
const indexPath = join(outputDir, 'apiIndex.json')
141144

142145
try {
143146
await writeFile(indexPath, JSON.stringify(index, null, 2))

src/utils/apiIndex/get.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { join } from 'path'
22
import { readFile } from 'fs/promises'
33
import type { ApiIndex } from './generate'
4+
import { getOutputDir } from '../getOutputDir'
45

56
/**
67
* Reads and parses the API index file
@@ -10,7 +11,8 @@ import type { ApiIndex } from './generate'
1011
* @throws Error if index file is not found, contains invalid JSON, or has invalid structure
1112
*/
1213
export async function getApiIndex(): Promise<ApiIndex> {
13-
const indexPath = join(process.cwd(), 'src', 'apiIndex.json')
14+
const outputDir = await getOutputDir()
15+
const indexPath = join(outputDir, 'apiIndex.json')
1416

1517
try {
1618
const content = await readFile(indexPath, 'utf-8')

src/utils/getOutputDir.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { join } from 'path'
2+
import { getConfig } from '../../cli/getConfig'
3+
4+
export async function getOutputDir(): Promise<string> {
5+
const config = await getConfig(join(process.cwd(), 'pf-docs.config.mjs'))
6+
if (!config) {
7+
throw new Error(
8+
'No config found, please run the `setup` command or manually create a pf-docs.config.mjs file',
9+
)
10+
}
11+
12+
if (!config.outputDir) {
13+
throw new Error(
14+
'No outputDir found in config file, an output directory must be defined in your config file e.g. "dist"',
15+
)
16+
}
17+
18+
return config.outputDir
19+
}

src/utils/packageUtils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ export const getDefaultTab = (filePath?: string): string => {
6161
const packageName = getPackageName(filePath)
6262
const tabBase = getTabBase(packageName)
6363

64-
const tab = addDemosOrDeprecated(tabBase, filePath)
65-
66-
return tab
64+
return addDemosOrDeprecated(tabBase, filePath)
6765
}
66+
67+
// This function is specifically for API routes where we need a fallback tab name
68+
// to ensure content is always accessible even when the default tab logic doesn't apply
69+
export const getDefaultTabForApi = (filePath?: string): string => getDefaultTab(filePath) || 'text'

0 commit comments

Comments
 (0)