Skip to content

Commit 14af6cb

Browse files
Merge pull request #185 from patternfly/fix-text-api-monorepo-issues
fix(API): address issues with text API in monorepos
2 parents 1e10053 + 8e9d674 commit 14af6cb

File tree

23 files changed

+430
-112
lines changed

23 files changed

+430
-112
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ src/apiIndex.json
3434
textContent/*.mdx
3535

3636
coverage/
37+
38+
.wrangler/
39+
40+
temp

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 on a clean repository, API endpoints will not be available until you run `npm run build` to generate the API index.
3637
### 🧞 Commands
3738

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

cli/cli.ts

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ try {
3434
.replace('file://', '')
3535
} catch (e: any) {
3636
if (e.code === 'ERR_MODULE_NOT_FOUND') {
37+
console.log('@patternfly/patternfly-doc-core not found, using current directory as astroRoot')
3738
astroRoot = process.cwd()
3839
} else {
3940
console.error('Error resolving astroRoot', e)
@@ -87,29 +88,33 @@ async function transformMDContentToMDX() {
8788
}
8889
}
8990

90-
async function initializeApiIndex() {
91+
async function initializeApiIndex(program: Command) {
92+
const { verbose } = program.opts()
9193
const templateIndexPath = join(astroRoot, 'cli', 'templates', 'apiIndex.json')
92-
const targetIndexPath = join(astroRoot, 'src', 'apiIndex.json')
93-
94+
const targetIndexPath = join(absoluteOutputDir, 'apiIndex.json')
9495
const indexExists = await fileExists(targetIndexPath)
9596

9697
// early return if the file exists from a previous build
9798
if (indexExists) {
98-
console.log('apiIndex.json already exists, skipping initialization')
99+
if (verbose) {
100+
console.log('apiIndex.json already exists, skipping initialization')
101+
}
99102
return
100103
}
101104

102105
try {
103106
await copyFile(templateIndexPath, targetIndexPath)
104-
console.log('Initialized apiIndex.json')
107+
if (verbose) {
108+
console.log('Initialized apiIndex.json')
109+
}
105110
} catch (e: any) {
106111
console.error('Error copying apiIndex.json template:', e)
107112
}
108113
}
109114

110-
async function buildProject(): Promise<DocsConfig | undefined> {
111-
await updateContent(program)
112-
await generateProps(program, true)
115+
async function buildProject(program: Command): Promise<DocsConfig | undefined> {
116+
const { verbose } = program.opts()
117+
113118
if (!config) {
114119
console.error(
115120
'No config found, please run the `setup` command or manually create a pf-docs.config.mjs file',
@@ -123,44 +128,57 @@ async function buildProject(): Promise<DocsConfig | undefined> {
123128
)
124129
return config
125130
}
126-
127-
await initializeApiIndex()
131+
await updateContent(program)
132+
await generateProps(program, true)
133+
await initializeApiIndex(program)
128134
await transformMDContentToMDX()
129135

130-
build({
136+
const docsOutputDir = join(absoluteOutputDir, 'docs')
137+
138+
await build({
131139
root: astroRoot,
132-
outDir: join(absoluteOutputDir, 'docs'),
140+
outDir: docsOutputDir,
133141
})
134142

143+
// copy the apiIndex.json file to the docs directory so it can be served as a static asset
144+
try {
145+
const apiIndexPath = join(absoluteOutputDir, 'apiIndex.json')
146+
const docsApiIndexPath = join(absoluteOutputDir, 'docs', 'apiIndex.json')
147+
await copyFile(apiIndexPath, docsApiIndexPath)
148+
149+
if (verbose) {
150+
console.log('Copied apiIndex.json to docs directory')
151+
}
152+
} catch (error) {
153+
console.error('Failed to copy apiIndex.json to docs directory:', error)
154+
throw error
155+
}
156+
135157
return config
136158
}
137159

138-
async function deploy() {
139-
const { verbose } = program.opts()
160+
async function deploy(program: Command) {
161+
const { verbose, dryRun } = program.opts()
140162

141163
if (verbose) {
142164
console.log('Starting Cloudflare deployment...')
143165
}
144166

167+
if (dryRun) {
168+
console.log('Dry run mode enabled, skipping deployment')
169+
return
170+
}
171+
145172
try {
146-
// First build the project
147-
const config = await buildProject()
148-
if (config) {
149-
if (verbose) {
150-
console.log('Build complete, deploying to Cloudflare...')
151-
}
152-
153-
// Deploy using Wrangler
154-
const { execSync } = await import('child_process')
155-
const outputPath = join(absoluteOutputDir, 'docs')
156-
157-
execSync(`npx wrangler pages deploy ${outputPath}`, {
158-
stdio: 'inherit',
159-
cwd: currentDir,
160-
})
161-
162-
console.log('Successfully deployed to Cloudflare Pages!')
163-
}
173+
// Deploy using Wrangler
174+
const { execSync } = await import('child_process')
175+
176+
execSync(`wrangler pages deploy`, {
177+
stdio: 'inherit',
178+
cwd: currentDir,
179+
})
180+
181+
console.log('Successfully deployed to Cloudflare Pages!')
164182
} catch (error) {
165183
console.error('Deployment failed:', error)
166184
process.exit(1)
@@ -172,6 +190,7 @@ program.name('pf-doc-core')
172190

173191
program.option('--verbose', 'verbose mode', false)
174192
program.option('--props', 'generate props data', false)
193+
program.option('--dry-run', 'dry run mode', false)
175194

176195
program.command('setup').action(async () => {
177196
await Promise.all([
@@ -194,7 +213,7 @@ program.command('init').action(async () => {
194213

195214
program.command('start').action(async () => {
196215
await updateContent(program)
197-
await initializeApiIndex()
216+
await initializeApiIndex(program)
198217

199218
// if a props file hasn't been generated yet, but the consumer has propsData, it will cause a runtime error so to
200219
// prevent that we're just creating a props file regardless of what they say if one doesn't exist yet
@@ -204,7 +223,7 @@ program.command('start').action(async () => {
204223
})
205224

206225
program.command('build').action(async () => {
207-
await buildProject()
226+
await buildProject(program)
208227
})
209228

210229
program.command('generate-props').action(async () => {
@@ -229,7 +248,7 @@ program
229248
})
230249

231250
program.command('deploy').action(async () => {
232-
await deploy()
251+
await deploy(program)
233252
})
234253

235254
program.parse(process.argv)

jest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const config: Config = {
1717
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
1818
moduleNameMapper: {
1919
'\\.(css|less)$': '<rootDir>/src/__mocks__/styleMock.ts',
20-
'(.+)\\.js': '$1',
2120
'^astro:content$': '<rootDir>/src/__mocks__/astro-content.ts',
21+
'(.+)\\.js': '$1',
2222
},
2323
setupFilesAfterEnv: ['<rootDir>/test.setup.ts'],
2424
transformIgnorePatterns: [

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
"build:props": "npm run build:cli && node ./dist/cli/cli.js generate-props",
1919
"preview": "wrangler pages dev",
2020
"astro": "astro",
21-
"deploy": "wrangler pages deploy",
21+
"deploy": "npm run build:cli && node ./dist/cli/cli.js deploy",
2222
"versions:upload": "wrangler versions upload",
2323
"prettier": "prettier --write ./src",
2424
"lint": "eslint . --cache --cache-strategy content",
2525
"test": "jest",
2626
"test:watch": "jest --watch",
2727
"semantic-release": "semantic-release",
28-
"cf-typegen": "wrangler types"
28+
"cf-typegen": "wrangler types",
29+
"clean": "rm -rf dist .astro .wrangler"
2930
},
3031
"main": "dist/cli/cli.js",
3132
"bin": "./dist/cli/cli.js",
@@ -64,7 +65,7 @@
6465
"@patternfly/quickstarts": "^6.0.0",
6566
"@types/react": "^18.3.23",
6667
"@types/react-dom": "^18.3.7",
67-
"astro": "5.15.9",
68+
"astro": "^5.15.9",
6869
"change-case": "5.4.4",
6970
"commander": "^13.1.0",
7071
"glob": "^11.0.3",
Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import { GET } from '../../../../pages/api/[version]'
22

3-
/**
4-
* Mock apiIndex.json with multiple versions (v5, v6)
5-
* to test section retrieval for different versions
6-
*/
7-
jest.mock('../../../../apiIndex.json', () => ({
3+
const mockApiIndex = {
84
versions: ['v5', 'v6'],
95
sections: {
106
v5: ['getting-started'],
117
v6: ['components', 'layouts', 'utilities'],
128
},
139
pages: {},
1410
tabs: {},
15-
}))
11+
}
1612

1713
it('returns all sections for a valid version', async () => {
14+
global.fetch = jest.fn(() =>
15+
Promise.resolve({
16+
ok: true,
17+
json: () => Promise.resolve(mockApiIndex),
18+
} as Response)
19+
)
20+
1821
const response = await GET({
1922
params: { version: 'v6' },
23+
url: new URL('http://localhost:4321/api/v6'),
2024
} as any)
2125
const body = await response.json()
2226

@@ -26,67 +30,131 @@ it('returns all sections for a valid version', async () => {
2630
expect(body).toContain('components')
2731
expect(body).toContain('layouts')
2832
expect(body).toContain('utilities')
33+
34+
jest.restoreAllMocks()
2935
})
3036

3137
it('returns only sections for the requested version', async () => {
38+
global.fetch = jest.fn(() =>
39+
Promise.resolve({
40+
ok: true,
41+
json: () => Promise.resolve(mockApiIndex),
42+
} as Response)
43+
)
44+
3245
const response = await GET({
3346
params: { version: 'v5' },
47+
url: new URL('http://localhost:4321/api/v5'),
3448
} as any)
3549
const body = await response.json()
3650

3751
expect(response.status).toBe(200)
3852
expect(body).toContain('getting-started')
53+
54+
jest.restoreAllMocks()
3955
})
4056

4157
it('sorts sections alphabetically', async () => {
58+
global.fetch = jest.fn(() =>
59+
Promise.resolve({
60+
ok: true,
61+
json: () => Promise.resolve(mockApiIndex),
62+
} as Response)
63+
)
64+
4265
const response = await GET({
4366
params: { version: 'v6' },
67+
url: new URL('http://localhost:4321/api/v6'),
4468
} as any)
4569
const body = await response.json()
4670

4771
const sorted = [...body].sort()
4872
expect(body).toEqual(sorted)
73+
74+
jest.restoreAllMocks()
4975
})
5076

5177
it('deduplicates sections from multiple collections', async () => {
78+
global.fetch = jest.fn(() =>
79+
Promise.resolve({
80+
ok: true,
81+
json: () => Promise.resolve(mockApiIndex),
82+
} as Response)
83+
)
84+
5285
const response = await GET({
5386
params: { version: 'v6' },
87+
url: new URL('http://localhost:4321/api/v6'),
5488
} as any)
5589
const body = await response.json()
5690

5791
const unique = [...new Set(body)]
5892
expect(body).toEqual(unique)
93+
94+
jest.restoreAllMocks()
5995
})
6096

6197
it('returns 404 error for nonexistent version', async () => {
98+
global.fetch = jest.fn(() =>
99+
Promise.resolve({
100+
ok: true,
101+
json: () => Promise.resolve(mockApiIndex),
102+
} as Response)
103+
)
104+
62105
const response = await GET({
63106
params: { version: 'v99' },
107+
url: new URL('http://localhost:4321/api/v99'),
64108
} as any)
65109
const body = await response.json()
66110

67111
expect(response.status).toBe(404)
68112
expect(body).toHaveProperty('error')
69113
expect(body.error).toContain('v99')
70114
expect(body.error).toContain('not found')
115+
116+
jest.restoreAllMocks()
71117
})
72118

73119
it('returns 400 error when version parameter is missing', async () => {
120+
global.fetch = jest.fn(() =>
121+
Promise.resolve({
122+
ok: true,
123+
json: () => Promise.resolve(mockApiIndex),
124+
} as Response)
125+
)
126+
74127
const response = await GET({
75128
params: {},
129+
url: new URL('http://localhost:4321/api/'),
76130
} as any)
77131
const body = await response.json()
78132

79133
expect(response.status).toBe(400)
80134
expect(body).toHaveProperty('error')
81135
expect(body.error).toContain('Version parameter is required')
136+
137+
jest.restoreAllMocks()
82138
})
83139

84-
it('excludes content entries that have no section field', async () => {
140+
it('returns sections array that matches the API index', async () => {
141+
global.fetch = jest.fn(() =>
142+
Promise.resolve({
143+
ok: true,
144+
json: () => Promise.resolve(mockApiIndex),
145+
} as Response)
146+
)
147+
85148
const response = await GET({
86149
params: { version: 'v6' },
150+
url: new URL('http://localhost:4321/api/v6'),
87151
} as any)
88152
const body = await response.json()
89153

90-
// Should only include sections from entries that have data.section
91-
expect(body.length).toBeGreaterThan(0)
154+
// Verify the returned sections exactly match the indexed sections
155+
// The API index generation process filters out entries without section fields
156+
expect(body).toEqual(mockApiIndex.sections.v6)
157+
expect(body).toEqual(['components', 'layouts', 'utilities'])
158+
159+
jest.restoreAllMocks()
92160
})

0 commit comments

Comments
 (0)