Skip to content

Commit

Permalink
Merge pull request #3727 from alphagov/component-libs
Browse files Browse the repository at this point in the history
  • Loading branch information
colinrotherham authored Jun 1, 2023
2 parents 2756911 + f76fdbc commit 88c4edd
Show file tree
Hide file tree
Showing 14 changed files with 147 additions and 111 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/govuk-frontend-review/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"cookie-parser": "^1.4.6",
"express": "^4.18.2",
"express-validator": "^7.0.1",
"front-matter": "^4.0.2",
"govuk-frontend": "*",
"govuk-frontend-config": "*",
"govuk-frontend-lib": "*",
Expand Down
12 changes: 5 additions & 7 deletions packages/govuk-frontend-review/src/app.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { join } from 'path'

import express from 'express'
import { paths } from 'govuk-frontend-config'
import { getDirectories, getComponentsData, getFullPageExamples } from 'govuk-frontend-lib/files'
import { componentNameToMacroName, packageNameToPath } from 'govuk-frontend-lib/names'
import { getComponentsData, getComponentNames } from 'govuk-frontend-lib/files'
import { componentNameToMacroName } from 'govuk-frontend-lib/names'
import { outdent } from 'outdent'

import { getExampleNames, getFullPageExamples } from './common/lib/files.mjs'
import * as middleware from './common/middleware/index.mjs'
import * as nunjucks from './common/nunjucks/index.mjs'
import * as routes from './routes/index.mjs'
Expand All @@ -16,8 +14,8 @@ export default async () => {
// Cache mapped components and examples
const [componentsData, componentNames, exampleNames, fullPageExamples] = await Promise.all([
getComponentsData(),
getDirectories(packageNameToPath('govuk-frontend', 'src/govuk/components')),
getDirectories(join(paths.app, 'src/views/examples')),
getComponentNames(),
getExampleNames(),
getFullPageExamples()
])

Expand Down
8 changes: 3 additions & 5 deletions packages/govuk-frontend-review/src/app.test.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { join } from 'path'

import { load } from 'cheerio'
import { paths, ports } from 'govuk-frontend-config'
import { getDirectories } from 'govuk-frontend-lib/files'
import { ports } from 'govuk-frontend-config'
import { getComponentNames } from 'govuk-frontend-lib/files'

const expectedPages = [
'/',
Expand Down Expand Up @@ -46,7 +44,7 @@ describe(`http://localhost:${ports.app}`, () => {
const response = await fetchPath('/')
const $ = load(await response.text())

const componentNames = await getDirectories(join(paths.package, 'src/govuk/components'))
const componentNames = await getComponentNames()
const componentsList = $('li a[href^="/components/"]').get()

// Since we have an 'all' component link that renders the default example of all
Expand Down
52 changes: 52 additions & 0 deletions packages/govuk-frontend-review/src/common/lib/files.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { readFile } from 'fs/promises'
import { join } from 'path'

import fm from 'front-matter'
import { paths } from 'govuk-frontend-config'
import { getDirectories } from 'govuk-frontend-lib/files'

/**
* Get example names
*
* @returns {Promise<string[]>} Component names
*/
export function getExampleNames () {
return getDirectories(join(paths.app, 'src/views/examples'))
}

/**
* Load all full page examples' front matter
*
* @returns {Promise<FullPageExample[]>} Full page examples
*/
export async function getFullPageExamples () {
const directories = await getDirectories(join(paths.app, 'src/views/full-page-examples'))

// Add metadata (front matter) to each example
const examples = await Promise.all(directories.map(async (exampleName) => {
const templatePath = join(paths.app, 'src/views/full-page-examples', exampleName, 'index.njk')
const { attributes } = fm(await readFile(templatePath, 'utf8'))

return {
name: exampleName,
path: exampleName,
...attributes
}
}))

const collator = new Intl.Collator('en', {
sensitivity: 'base'
})

return examples.sort(({ name: a }, { name: b }) =>
collator.compare(a, b))
}

/**
* Full page example from front matter
*
* @typedef {object} FullPageExample
* @property {string} name - Example name
* @property {string} [scenario] - Description explaining the example
* @property {string} [notes] - Additional notes about the example
*/
40 changes: 40 additions & 0 deletions packages/govuk-frontend-review/src/common/lib/files.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getFullPageExamples } from './files.mjs'

describe('getFullPageExamples', () => {
it('contains name of each example', async () => {
const examples = await getFullPageExamples()

examples.forEach((example) => expect(example).toEqual(
expect.objectContaining({
name: expect.any(String),
path: expect.any(String)
})
))
})

it('contains scenario front matter, for some examples', async () => {
const examples = await getFullPageExamples()

// At least 1x example with { name, path, scenario }
expect(examples).toEqual(expect.arrayContaining([
expect.objectContaining({
name: expect.any(String),
path: expect.any(String),
scenario: expect.any(String)
})
]))
})

it('contains notes front matter, for some examples', async () => {
const examples = await getFullPageExamples()

// At least 1x example with { name, path, notes }
expect(examples).toEqual(expect.arrayContaining([
expect.objectContaining({
name: expect.any(String),
path: expect.any(String),
notes: expect.any(String)
})
]))
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getFullPageExamples } from 'govuk-frontend-lib/files'

import { getFullPageExamples } from '../common/lib/files.mjs'
import * as routes from '../views/full-page-examples/index.mjs'

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { filterPath, getDirectories, getListing } from 'govuk-frontend-lib/files'
import { filterPath, getComponentNames, getListing } from 'govuk-frontend-lib/files'
import { componentNameToMacroName, packageNameToPath } from 'govuk-frontend-lib/names'
import slash from 'slash'

Expand All @@ -9,7 +9,7 @@ import slash from 'slash'
*/
export default async () => {
const componentMacros = await getListing(packageNameToPath('govuk-frontend', 'src'), '**/components/**/macro.njk')
const componentNames = await getDirectories(packageNameToPath('govuk-frontend', 'src/govuk/components'))
const componentNames = await getComponentNames()

// Build array of macros
const nunjucksMacros = componentNames
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { join } = require('path')

const { paths } = require('govuk-frontend-config')
const { nunjucksEnv, renderHTML } = require('govuk-frontend-helpers/nunjucks')
const { getDirectories, getComponentsData } = require('govuk-frontend-lib/files')
const { getComponentsData, getComponentNames } = require('govuk-frontend-lib/files')
const { HtmlValidate } = require('html-validate')
// We can't use the render function from jest-helpers, because we need control
// over the nunjucks environment.
Expand All @@ -21,7 +21,7 @@ describe('Components', () => {
nunjucksEnvDefault = nunjucksEnv

// Components list
componentNames = await getDirectories(join(paths.package, 'src/govuk/components'))
componentNames = await getComponentNames()
})

describe('Nunjucks environment', () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/govuk-frontend/tasks/build/package.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { join } from 'path'

import { paths, pkg } from 'govuk-frontend-config'
import { compileSassFile } from 'govuk-frontend-helpers/tests'
import { filterPath, getDirectories, getListing, mapPathTo } from 'govuk-frontend-lib/files'
import { filterPath, getComponentNames, getListing, mapPathTo } from 'govuk-frontend-lib/files'
import { componentNameToClassName, componentPathToModuleName } from 'govuk-frontend-lib/names'

describe('packages/govuk-frontend/dist/', () => {
Expand All @@ -27,7 +27,7 @@ describe('packages/govuk-frontend/dist/', () => {
componentsFilesDistESM = await getListing(join(paths.package, 'dist/govuk-esm/components'))

// Components list
componentNames = await getDirectories(join(paths.package, 'src/govuk/components'))
componentNames = await getComponentNames()
})

it('should contain the expected files', async () => {
Expand Down Expand Up @@ -175,8 +175,8 @@ describe('packages/govuk-frontend/dist/', () => {

beforeAll(async () => {
// Components list (with JavaScript only)
componentNamesWithJavaScript = componentNames
.filter((componentName) => componentsFilesSource.includes(join(componentName, `${componentName}.mjs`)))
componentNamesWithJavaScript = await getComponentNames((componentName, componentFiles) =>
componentFiles.every(filterPath([`**/${componentName}.mjs`])))
})

it('should have component JavaScript file with correct module name', () => {
Expand Down
75 changes: 32 additions & 43 deletions shared/lib/files.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
const { readFile } = require('fs/promises')
const { join, parse, relative } = require('path')

const fm = require('front-matter')
const { glob } = require('glob')
const { paths } = require('govuk-frontend-config')
const yaml = require('js-yaml')
const { minimatch } = require('minimatch')

Expand Down Expand Up @@ -105,10 +103,39 @@ const getComponentData = async (componentName) => {
* @returns {Promise<(ComponentData & { name: string })[]>} Components' data
*/
const getComponentsData = async () => {
const componentNames = await getDirectories(packageNameToPath('govuk-frontend', 'src/govuk/components'))
const componentNames = await getComponentNames()
return Promise.all(componentNames.map(getComponentData))
}

/**
* Get component files
*
* @param {string} [componentName] - Component name
* @returns {Promise<string[]>} Component files
*/
const getComponentFiles = (componentName = '') =>
getListing(packageNameToPath('govuk-frontend', join('src/govuk/components', componentName)))

/**
* Get component names (with optional filter)
*
* @param {(componentName: string, componentFiles: string[]) => boolean} [filter] - Component names array filter
* @returns {Promise<string[]>} Component names
*/
const getComponentNames = async (filter) => {
const componentNames = await getDirectories(packageNameToPath('govuk-frontend', 'src/govuk/components'))

if (filter) {
const componentFiles = await getComponentFiles()

// Apply component names filter
return componentNames.filter((componentName) =>
filter(componentName, componentFiles))
}

return componentNames
}

/**
* Get examples from a component's metadata file
*
Expand All @@ -128,43 +155,14 @@ async function getExamples (componentName) {
return examples
}

/**
* Load all full page examples' front matter
*
* @returns {Promise<FullPageExample[]>} Full page examples
*/
const getFullPageExamples = async () => {
const directories = await getDirectories(join(paths.app, 'src/views/full-page-examples'))

// Add metadata (front matter) to each example
const examples = await Promise.all(directories.map(async (exampleName) => {
const templatePath = join(paths.app, 'src/views/full-page-examples', exampleName, 'index.njk')

// @ts-expect-error "This expression is not callable" due to incorrect types
const { attributes } = fm(await readFile(templatePath, 'utf8'))

return {
name: exampleName,
path: exampleName,
...attributes
}
}))

const collator = new Intl.Collator('en', {
sensitivity: 'base'
})

return examples.sort(({ name: a }, { name: b }) =>
collator.compare(a, b))
}

module.exports = {
filterPath,
getComponentData,
getComponentsData,
getComponentFiles,
getComponentNames,
getDirectories,
getExamples,
getFullPageExamples,
getListing,
getYaml,
mapPathTo
Expand Down Expand Up @@ -200,12 +198,3 @@ module.exports = {
* @property {object} data - Example data
* @property {boolean} [hidden] - Example hidden from review app
*/

/**
* Full page example from front matter
*
* @typedef {object} FullPageExample
* @property {string} name - Example name
* @property {string} [scenario] - Description explaining the example
* @property {string} [notes] - Additional notes about the example
*/
41 changes: 1 addition & 40 deletions shared/lib/files.unit.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { getComponentData, getFullPageExamples } = require('./files.js')
const { getComponentData } = require('./files.js')

describe('getComponentData', () => {
it('rejects if unable to load component data', async () => {
Expand Down Expand Up @@ -45,42 +45,3 @@ describe('getComponentData', () => {
))
})
})

describe('getFullPageExamples', () => {
it('contains name of each example', async () => {
const examples = await getFullPageExamples()

examples.forEach((example) => expect(example).toEqual(
expect.objectContaining({
name: expect.any(String),
path: expect.any(String)
})
))
})

it('contains scenario front matter, for some examples', async () => {
const examples = await getFullPageExamples()

// At least 1x example with { name, path, scenario }
expect(examples).toEqual(expect.arrayContaining([
expect.objectContaining({
name: expect.any(String),
path: expect.any(String),
scenario: expect.any(String)
})
]))
})

it('contains notes front matter, for some examples', async () => {
const examples = await getFullPageExamples()

// At least 1x example with { name, path, notes }
expect(examples).toEqual(expect.arrayContaining([
expect.objectContaining({
name: expect.any(String),
path: expect.any(String),
notes: expect.any(String)
})
]))
})
})
1 change: 0 additions & 1 deletion shared/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
},
"license": "MIT",
"dependencies": {
"front-matter": "^4.0.2",
"glob": "^10.2.6",
"govuk-frontend-config": "*",
"js-yaml": "^4.1.0",
Expand Down
Loading

0 comments on commit 88c4edd

Please sign in to comment.