Skip to content

Commit

Permalink
Merge pull request #4463 from alphagov/express-errors
Browse files Browse the repository at this point in the history
Add error pages to Review app
  • Loading branch information
colinrotherham authored Nov 20, 2023
2 parents 081907f + d5eac38 commit c9d6d99
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 61 deletions.
209 changes: 167 additions & 42 deletions packages/govuk-frontend-review/src/app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,81 @@ export default async () => {
// Configure nunjucks
const env = nunjucks.renderer(app)

// Define parameters

/**
* Handle parameter :componentName
*
* Finds all component fixtures and default example
*/
app.param(
'componentName',

/**
* @param {import('express').Request} req
* @param {import('express').Response<{}, Partial<PreviewLocals>>} res
* @param {import('express').NextFunction} next
* @param {string} componentName
*/
(req, res, next, componentName) => {
const exampleName = 'default'

// Find all fixtures for component
const componentFixtures = componentsFixtures.find(
({ component }) => component === componentName
)

// Find default fixture for component
const componentFixture = componentFixtures?.fixtures.find(
({ name }) => name === exampleName
)

// Add response locals
res.locals.componentName = componentName
res.locals.componentFixtures = componentFixtures
res.locals.componentFixture = componentFixture
res.locals.exampleName = 'default'

next()
}
)

/**
* Handle parameter :exampleName
*
* Finds component fixture for example and updates locals
*/
app.param(
'exampleName',

/**
* @param {import('express').Request} req
* @param {import('express').Response<{}, Partial<PreviewLocals>>} res
* @param {import('express').NextFunction} next
* @param {string} exampleName
*/
(req, res, next, exampleName) => {
const { componentFixtures } = res.locals

// Replace default fixture with named example
const componentFixture = componentFixtures?.fixtures.find(
({ name }) => nunjucks.filters.slugify(name) === exampleName
)

// Update response locals
res.locals.componentFixture = componentFixture
res.locals.exampleName = exampleName

next()
}
)

// Define routes

// Index page - render the component list template
app.get('/', async function (req, res) {
/**
* Review app home page
*/
app.get('/', (req, res) => {
res.render('index', {
componentNames,
componentNamesWithJavaScript,
Expand All @@ -92,43 +163,61 @@ export default async () => {
})
})

// Whenever the route includes a :componentName parameter, read the component fixtures
app.param('componentName', function (req, res, next, componentName) {
res.locals.componentFixtures = componentsFixtures.find(
({ component }) => component === componentName
)
next()
})

// All components redirect
/**
* All components redirect
*/
app.get('/components/all', function (req, res) {
res.redirect('./')
})

// Component examples
app.get('/components/:componentName?', (req, res) => {
const { componentName } = req.params
/**
* Component examples
*/
app.get(
'/components/:componentName?',

/**
* @param {import('express').Request} req
* @param {import('express').Response<{}, Partial<PreviewLocals>>} res
* @param {import('express').NextFunction} next
* @returns {void}
*/
(req, res, next) => {
const { componentName } = res.locals

// Unknown component, continue to page not found
if (componentName && !componentNames.includes(componentName)) {
return next()
}

res.render(componentName ? 'component' : 'components', {
componentsFixtures,
componentName
})
})
res.render(componentName ? 'component' : 'components', {
componentsFixtures,
componentName
})
}
)

// Component example preview
/**
* Component example preview
*/
app.get(
'/components/:componentName/:exampleName?/preview',
function (req, res, next) {
const { componentName, exampleName = 'default' } = req.params

/** @type {ComponentFixtures | undefined} */
const componentFixtures = res.locals.componentFixtures

const fixture = componentFixtures?.fixtures.find(
(fixture) => nunjucks.filters.slugify(fixture.name) === exampleName
)
/**
* @param {import('express').Request} req
* @param {import('express').Response<{}, Partial<PreviewLocals>>} res
* @param {import('express').NextFunction} next
* @returns {void}
*/
(req, res, next) => {
const {
componentName,
componentFixtures: fixtures,
componentFixture: fixture
} = res.locals

if (!fixture) {
// Unknown component or fixture, continue to page not found
if (!componentNames.includes(componentName) || !fixtures || !fixture) {
return next()
}

Expand All @@ -153,34 +242,70 @@ export default async () => {

res.render('component-preview', {
bodyClasses,
componentName,
componentView,
exampleName,
previewLayout: componentFixtures.previewLayout
previewLayout: fixtures.previewLayout
})
}
)

// Example view
app.get('/examples/:exampleName', function (req, res) {
const { exampleName } = req.params
/**
* Example view
*/
app.get(
'/examples/:exampleName',

/**
* @param {import('express').Request} req
* @param {import('express').Response<{}, Partial<PreviewLocals>>} res
* @param {import('express').NextFunction} next
* @returns {void}
*/
(req, res, next) => {
const { exampleName } = res.locals

// Unknown example, continue to page not found
if (!exampleNames.includes(exampleName)) {
return next()
}

res.render(`examples/${exampleName}/index`, {
exampleName,
res.render(`examples/${exampleName}/index`, {
exampleName,

// Render with random number for unique non-visited links
randomPageHash: (Math.random() * 1000000).toFixed()
})
})
// Render with random number for unique non-visited links
randomPageHash: (Math.random() * 1000000).toFixed()
})
}
)

// Full page example views
routes.fullPageExamples(app)

/**
* Page not found handler
*/
app.use((req, res) => {
res.status(404).render('errors/404')
})

/**
* Error handler
*/
app.use((error, req, res, next) => {
console.error(error)
res.status(500).render('errors/500', {
error
})
})

return app
}

/**
* @typedef {import('@govuk-frontend/lib/components').ComponentFixtures} ComponentFixtures
* @typedef {object} PreviewLocals
* @property {import('@govuk-frontend/lib/components').ComponentFixtures} componentFixtures - All Component fixtures
* @property {import('@govuk-frontend/lib/components').ComponentFixture} [componentFixture] - Single component fixture
* @property {string} componentName - Component name
* @property {string} [exampleName] - Example name
*/

/**
Expand Down
1 change: 1 addition & 0 deletions packages/govuk-frontend-review/src/common/lib/files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export async function getFullPageExamples() {
*
* @typedef {object} FullPageExample
* @property {string} name - Example name
* @property {string} path - Example directory name
* @property {string} [scenario] - Description explaining the example
* @property {string} [notes] - Additional notes about the example
*/
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
export { componentNameToMacroName } from '@govuk-frontend/lib/names'
export { highlight } from './highlight.mjs'
export { inspect } from './inspect.mjs'
export { markdown } from './markdown.mjs'
export { slugify } from './slugify.mjs'
export { unslugify } from './unslugify.mjs'
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import util from 'util'

/**
* Format JavaScript objects as strings
*
* @param {unknown} object - JavaScript object to format
* @returns {string} Formatted string
*/
export function inspect(object) {
return util.inspect(object, {
compact: false,
depth: Infinity,
maxArrayLength: Infinity,
maxStringLength: Infinity
})
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { inspect } from 'util'

import prettier from '@prettier/sync'
import { outdent } from 'outdent'

import { componentNameToMacroName } from '../filters/index.mjs'
import { inspect, componentNameToMacroName } from '../filters/index.mjs'

/**
* Component Nunjucks code (formatted)
Expand All @@ -16,12 +14,7 @@ export function getNunjucksCode(componentName, options) {
const macroName = componentNameToMacroName(componentName)

// Allow nested HTML strings to wrap at `\n`
const paramsFormatted = inspect(options.context, {
compact: false,
depth: Infinity,
maxArrayLength: Infinity,
maxStringLength: Infinity
})
const paramsFormatted = inspect(options.context)

// Format Nunjucks safely with double quotes
const macroFormatted = prettier.format(`${macroName}(${paramsFormatted})`, {
Expand Down
15 changes: 13 additions & 2 deletions packages/govuk-frontend-review/src/routes/full-page-examples.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getFullPageExamples } from '../common/lib/files.mjs'
import * as routes from '../views/full-page-examples/index.mjs'

const fullPageExamples = await getFullPageExamples()
const fullPageExampleNames = fullPageExamples.map(({ path }) => path)

/**
* @param {import('express').Application} app
Expand All @@ -22,16 +23,26 @@ export default (app) => {
routes.whatIsYourPostcode(app)
routes.whatWasTheLastCountryYouVisited(app)

/**
* Full page examples index
*/
app.get('/full-page-examples', async (req, res) => {
res.render('full-page-examples/index', {
fullPageExamples
})
})

// Display full page examples index by default if not handled already
app.get('/full-page-examples/:exampleName', (req, res) => {
/**
* Full page example
*/
app.get('/full-page-examples/:exampleName', (req, res, next) => {
const { exampleName } = req.params

// No matching example so continue to page not found
if (!fullPageExampleNames.includes(exampleName)) {
return next()
}

res.render(`full-page-examples/${exampleName}/index`)
})
}
8 changes: 6 additions & 2 deletions packages/govuk-frontend-review/src/views/component.njk
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@

{% block beforeContent %}
{{ govukBreadcrumbs({
"items": [
items: [
{
text: 'GOV.UK Frontend',
text: "GOV.UK Frontend",
href: "/"
},
{
text: "All components",
href: "/components"
},
{
text: componentName | unslugify
}
Expand Down
14 changes: 11 additions & 3 deletions packages/govuk-frontend-review/src/views/components.njk
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends "layouts/full-width-landmarks.njk" %}

{% from "govuk/components/back-link/macro.njk" import govukBackLink %}
{% from "govuk/components/breadcrumbs/macro.njk" import govukBreadcrumbs %}
{% from "macros/showExamples.njk" import showExamples %}

{% block pageTitle %}All components - GOV.UK Frontend{% endblock %}
Expand All @@ -10,8 +10,16 @@
{% endset %}

{% block beforeContent %}
{{ govukBackLink({
href: "/"
{{ govukBreadcrumbs({
items: [
{
text: "GOV.UK Frontend",
href: "/"
},
{
text: "All components"
}
]
}) }}
{% endblock %}

Expand Down
Loading

0 comments on commit c9d6d99

Please sign in to comment.