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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ jobs:
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm run build:cli
- run: npm test
- run: npm run lint
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ jobs:
registry-url: https://registry.npmjs.org/
- name: Build dist
run: npm build
- name: Build CLI
run: npm run build:cli
- name: Release to NPM
run: npx semantic-release
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__tests__
5 changes: 5 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export default defineConfig({
vite: {
ssr: {
noExternal: ["@patternfly/*", "react-dropzone"],
},
server: {
fs: {
allow: ['./']
}
}
}
});
85 changes: 85 additions & 0 deletions cli/__tests__/createCollectionContent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { createCollectionContent } from '../createCollectionContent'
import { getConfig } from '../getConfig'
import { writeFile } from 'fs/promises'

jest.mock('../getConfig')
jest.mock('fs/promises')

// suppress console.log so that it doesn't clutter the test output
jest.spyOn(console, 'log').mockImplementation(() => {})

it('should call getConfig with the passed config file location', async () => {
await createCollectionContent('/foo/', 'bar', false)

expect(getConfig).toHaveBeenCalledWith('bar')
})

it('should not proceed if config is not found', async () => {
;(getConfig as jest.Mock).mockResolvedValue(undefined)

await createCollectionContent('/foo/', 'bar', false)

expect(writeFile).not.toHaveBeenCalled()
})

it('should log error if content is not found in config', async () => {
;(getConfig as jest.Mock).mockResolvedValue({ foo: 'bar' })

const mockConsoleError = jest.fn()
jest.spyOn(console, 'error').mockImplementation(mockConsoleError)

await createCollectionContent('/foo/', 'bar', false)

expect(mockConsoleError).toHaveBeenCalledWith('No content found in config')
expect(writeFile).not.toHaveBeenCalled()
})

it('should call writeFile with the expected file location and content without throwing any errors', async () => {
;(getConfig as jest.Mock).mockResolvedValue({ content: { key: 'value' } })

const mockConsoleError = jest.fn()
jest.spyOn(console, 'error').mockImplementation(mockConsoleError)

await createCollectionContent('/foo/', 'bar', false)

expect(writeFile).toHaveBeenCalledWith(
'/foo/src/content.ts',
`export const content = ${JSON.stringify({ key: 'value' })}`,
)
expect(mockConsoleError).not.toHaveBeenCalled()
})

it('should log error if writeFile throws an error', async () => {
;(getConfig as jest.Mock).mockResolvedValue({ content: { key: 'value' } })

const mockConsoleError = jest.fn()
jest.spyOn(console, 'error').mockImplementation(mockConsoleError)

const error = new Error('error')
;(writeFile as jest.Mock).mockRejectedValue(error)

await createCollectionContent('/foo/', 'bar', false)

expect(mockConsoleError).toHaveBeenCalledWith(
'Error writing content file',
error,
)
})

it('should log that content file was created when run in verbose mode', async () => {
const mockConsoleLog = jest.fn()
jest.spyOn(console, 'log').mockImplementation(mockConsoleLog)

await createCollectionContent('/foo/', 'bar', true)

expect(mockConsoleLog).toHaveBeenCalledWith('Content file created')
})

it('should not log that content file was created when not run in verbose mode', async () => {
const mockConsoleLog = jest.fn()
jest.spyOn(console, 'log').mockImplementation(mockConsoleLog)

await createCollectionContent('/foo/', 'bar', false)

expect(mockConsoleLog).not.toHaveBeenCalled()
})
30 changes: 30 additions & 0 deletions cli/__tests__/getConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getConfig } from '../getConfig'
import { resolve } from 'path'

it('should return the config when pf-docs.config.js exists', async () => {
const config = await getConfig(resolve('./cli/testData/good.config.js'))
expect(config).toEqual({
config: {
content: [
{
base: 'base-path',
packageName: 'package-name',
pattern: 'pattern',
name: 'name',
},
],
},
})
})

it('should return undefined and log error when pf-docs.config.js does not exist', async () => {
const consoleErrorMock = jest.fn()

jest.spyOn(console, 'error').mockImplementation(consoleErrorMock)

const config = await getConfig('foo')
expect(config).toBeUndefined()
expect(consoleErrorMock).toHaveBeenCalledWith(
'pf-docs.config.js not found, have you created it at the root of your package?',
)
})
58 changes: 58 additions & 0 deletions cli/__tests__/setFsRootDir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { readFile, writeFile } from 'fs/promises'
import { setFsRootDir } from '../setFsRootDir'

jest.mock('fs/promises')

// suppress console.log so that it doesn't clutter the test output
jest.spyOn(console, 'log').mockImplementation(() => {})

it('should attempt to read the astro config file', async () => {
;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['/bar/'] } }")

await setFsRootDir('/foo/', '/bar')

expect(readFile).toHaveBeenCalledWith('/foo/astro.config.mjs', 'utf8')
})

it('should not modify the file if the default allow list is not present', async () => {
;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['/bar/'] } }")

await setFsRootDir('/foo/', '/bar')

expect(writeFile).not.toHaveBeenCalled()
})

it('should modify the file if the default allow list is present', async () => {
;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['./'] } }")

await setFsRootDir('/foo/', '/bar')

expect(writeFile).toHaveBeenCalledWith(
'/foo/astro.config.mjs',
"{ fs: { allow: ['/bar/'] } }",
)
})

it('should log an error if writing the file fails', async () => {
;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['./'] } }")
;(writeFile as jest.Mock).mockRejectedValue(new Error('write error'))
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {})

await setFsRootDir('/foo/', '/bar')

expect(consoleErrorSpy).toHaveBeenCalledWith(
`Error setting the server allow list in /foo/`,
expect.any(Error),
)
})

it('should log a success message after attempting to write the file', async () => {
;(readFile as jest.Mock).mockResolvedValue("{ fs: { allow: ['./'] } }")
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})

await setFsRootDir('/foo/', '/bar')

expect(consoleLogSpy).toHaveBeenCalledWith('fs value set created')
})
50 changes: 50 additions & 0 deletions cli/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env node
import { Command } from 'commander'
import { build, dev, preview, sync } from 'astro'
import { createCollectionContent } from './createCollectionContent.js'
import { setFsRootDir } from './setFsRootDir.js'

const astroRoot = import.meta
.resolve('patternfly-doc-core')
.replace('dist/cli/cli.js', '')
.replace('file://', '')

const program = new Command()
program.name('pf-doc-core')

program.option('--verbose', 'verbose mode', false)

program.command('init').action(async () => {
await setFsRootDir(astroRoot, process.cwd())
})

program.command('start').action(async () => {
dev({ mode: 'development', root: astroRoot })
})

program.command('build').action(async () => {
build({ root: astroRoot })
})

program.command('serve').action(async () => {
preview({ root: astroRoot })
})

program.command('sync').action(async () => {
sync({ root: astroRoot })
})

program.parse(process.argv)

const { verbose } = program.opts()

if (verbose) {
// eslint-disable-next-line no-console
console.log('Verbose mode enabled')
}

createCollectionContent(
astroRoot,
`${process.cwd()}/pf-docs.config.js`,
verbose,
)
31 changes: 31 additions & 0 deletions cli/createCollectionContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* eslint-disable no-console */
import { writeFile } from 'fs/promises'
import { getConfig } from './getConfig.js'

export async function createCollectionContent(rootDir: string, configFile: string, verbose: boolean) {
const config = await getConfig(configFile)
if (!config) {
return
}

const { content } = config
if (!content) {
console.error('No content found in config')
return
}

const contentFile = rootDir + 'src/content.ts'

try {
await writeFile(
contentFile,
`export const content = ${JSON.stringify(content)}`,
)
} catch (e: any) {
console.error('Error writing content file', e)
} finally {
if (verbose) {
console.log('Content file created')
}
}
}
27 changes: 27 additions & 0 deletions cli/getConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable no-console */
export interface CollectionDefinition {
base?: string
packageName?: string
pattern: string
name: string
}

export interface DocsConfig {
content: CollectionDefinition[]
}

export async function getConfig(fileLocation: string): Promise<DocsConfig | undefined> {
try {
const { config } = await import(fileLocation)
return config as DocsConfig
} catch (e: any) {
if (['ERR_MODULE_NOT_FOUND', 'MODULE_NOT_FOUND'].includes(e.code)) {
console.error(
'pf-docs.config.js not found, have you created it at the root of your package?',
)
return
}
console.error(e)
return
}
}
29 changes: 29 additions & 0 deletions cli/setFsRootDir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable no-console */
import { readFile, writeFile } from 'fs/promises'

export async function setFsRootDir(
astroRootDir: string,
consumerRootDir: string,
) {
const astroConfigFile = astroRootDir + 'astro.config.mjs'
const astroConfigFileContent = await readFile(astroConfigFile, 'utf8')

const defaultAllowList = "allow: ['./']"

if (!astroConfigFileContent.includes(defaultAllowList)) {
return
}

const newAstroConfigFileContent = astroConfigFileContent.replace(
defaultAllowList,
`allow: ['${consumerRootDir}/']`,
)

try {
await writeFile(astroConfigFile, newAstroConfigFileContent)
} catch (e: any) {
console.error(`Error setting the server allow list in ${astroRootDir}`, e)
} finally {
console.log('fs value set created')
}
}
12 changes: 12 additions & 0 deletions cli/testData/good.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const config = {
config: {
content: [
{
base: 'base-path',
packageName: 'package-name',
pattern: 'pattern',
name: 'name',
},
],
},
}
9 changes: 9 additions & 0 deletions cli/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"outDir": "../dist/cli",
"skipLibCheck": true,
"module": "NodeNext",
"allowJs": true
},
"exclude": ["__tests__", "testData"],
}
3 changes: 2 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Config } from 'jest'
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src'],
roots: ['<rootDir>/src', '<rootDir>/cli'],
transform: {
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.jest.json' }],
'^.+\\.m?jsx?$': 'babel-jest',
Expand All @@ -16,6 +16,7 @@ const config: Config = {
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'\\.(css|less)$': '<rootDir>/src/__mocks__/styleMock.ts',
'(.+)\\.js': '$1',
},
setupFilesAfterEnv: ['<rootDir>/test.setup.ts'],
transformIgnorePatterns: [
Expand Down
Loading