@defra/forms-engine-plugin 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/.public/stylesheets/application.min.css +1 -1
  2. package/.public/stylesheets/application.min.css.map +1 -1
  3. package/.server/client/stylesheets/application.scss +10 -0
  4. package/.server/config/index.js +3 -14
  5. package/.server/config/index.js.map +1 -1
  6. package/.server/server/devserver/dxt-devtool-baselayout.html +71 -0
  7. package/.server/server/forms/register-as-a-unicorn-breeder.json +393 -0
  8. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +251 -0
  9. package/.server/server/index.js +11 -16
  10. package/.server/server/index.js.map +1 -1
  11. package/.server/server/plugins/engine/configureEnginePlugin.js +16 -2
  12. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  13. package/.server/server/plugins/engine/plugin.js +27 -16
  14. package/.server/server/plugins/engine/plugin.js.map +1 -1
  15. package/.server/server/plugins/engine/services/formsService.js +15 -29
  16. package/.server/server/plugins/engine/services/formsService.js.map +1 -1
  17. package/.server/server/plugins/engine/services/localFormsService.js +52 -0
  18. package/.server/server/plugins/engine/services/localFormsService.js.map +1 -0
  19. package/.server/server/plugins/engine/views/confirmation.html +1 -1
  20. package/.server/server/plugins/engine/views/file-upload.html +1 -1
  21. package/.server/server/plugins/engine/views/index.html +1 -1
  22. package/.server/server/plugins/engine/views/item-delete.html +1 -1
  23. package/.server/server/plugins/engine/views/repeat-list-summary.html +1 -1
  24. package/.server/server/plugins/engine/views/summary.html +1 -1
  25. package/.server/server/plugins/errorPages.js +4 -26
  26. package/.server/server/plugins/errorPages.js.map +1 -1
  27. package/.server/server/plugins/nunjucks/context.js +37 -28
  28. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  29. package/.server/server/plugins/nunjucks/context.test.js +23 -28
  30. package/.server/server/plugins/nunjucks/context.test.js.map +1 -1
  31. package/.server/server/plugins/nunjucks/types.js +3 -4
  32. package/.server/server/plugins/nunjucks/types.js.map +1 -1
  33. package/.server/server/routes/index.js +0 -1
  34. package/.server/server/routes/index.js.map +1 -1
  35. package/.server/typings/hapi/index.d.js.map +1 -1
  36. package/package.json +2 -1
  37. package/src/client/stylesheets/application.scss +10 -0
  38. package/src/config/index.ts +4 -17
  39. package/src/server/devserver/dxt-devtool-baselayout.html +71 -0
  40. package/src/server/forms/register-as-a-unicorn-breeder.json +393 -0
  41. package/src/server/forms/register-as-a-unicorn-breeder.yaml +251 -0
  42. package/src/server/index.test.ts +4 -37
  43. package/src/server/index.ts +13 -16
  44. package/src/server/plugins/engine/configureEnginePlugin.ts +19 -1
  45. package/src/server/plugins/engine/plugin.ts +43 -17
  46. package/src/server/plugins/engine/services/formsService.js +17 -35
  47. package/src/server/plugins/engine/services/localFormsService.js +49 -0
  48. package/src/server/plugins/engine/views/confirmation.html +1 -1
  49. package/src/server/plugins/engine/views/file-upload.html +1 -1
  50. package/src/server/plugins/engine/views/index.html +1 -1
  51. package/src/server/plugins/engine/views/item-delete.html +1 -1
  52. package/src/server/plugins/engine/views/repeat-list-summary.html +1 -1
  53. package/src/server/plugins/engine/views/summary.html +1 -1
  54. package/src/server/plugins/errorPages.ts +4 -26
  55. package/src/server/plugins/nunjucks/context.js +41 -31
  56. package/src/server/plugins/nunjucks/context.test.js +24 -27
  57. package/src/server/plugins/nunjucks/types.js +3 -4
  58. package/src/server/routes/index.ts +0 -1
  59. package/src/typings/hapi/index.d.ts +3 -9
  60. package/.server/common/cookies.js +0 -55
  61. package/.server/common/cookies.js.map +0 -1
  62. package/.server/common/cookies.test.js +0 -15
  63. package/.server/common/cookies.test.js.map +0 -1
  64. package/.server/common/types.js +0 -6
  65. package/.server/common/types.js.map +0 -1
  66. package/.server/server/forms/README.md +0 -10
  67. package/.server/server/forms/report-a-terrorist.json +0 -270
  68. package/.server/server/forms/runner-components-test.json +0 -365
  69. package/.server/server/forms/test.json +0 -581
  70. package/.server/server/forms/test.yaml +0 -363
  71. package/.server/server/plugins/blankie.js +0 -29
  72. package/.server/server/plugins/blankie.js.map +0 -1
  73. package/.server/server/plugins/engine/services/formsService.test.js +0 -71
  74. package/.server/server/plugins/engine/services/formsService.test.js.map +0 -1
  75. package/.server/server/plugins/engine/views/layout.html +0 -199
  76. package/.server/server/plugins/router.js +0 -169
  77. package/.server/server/plugins/router.js.map +0 -1
  78. package/.server/server/routes/health.js +0 -15
  79. package/.server/server/routes/health.js.map +0 -1
  80. package/.server/server/routes/health.test.js +0 -32
  81. package/.server/server/routes/health.test.js.map +0 -1
  82. package/.server/server/utils/file-form-service.test.js +0 -52
  83. package/.server/server/utils/file-form-service.test.js.map +0 -1
  84. package/.server/server/views/404.html +0 -16
  85. package/.server/server/views/500.html +0 -19
  86. package/.server/server/views/help/accessibility-statement.html +0 -58
  87. package/.server/server/views/help/cookie-preferences.html +0 -57
  88. package/.server/server/views/help/cookies.html +0 -71
  89. package/.server/server/views/help/get-support.html +0 -37
  90. package/.server/server/views/help/privacy-notice.html +0 -68
  91. package/.server/server/views/help/terms-and-conditions.html +0 -83
  92. package/src/common/cookies.js +0 -58
  93. package/src/common/cookies.test.js +0 -23
  94. package/src/common/types.js +0 -5
  95. package/src/server/forms/README.md +0 -10
  96. package/src/server/forms/report-a-terrorist.json +0 -270
  97. package/src/server/forms/runner-components-test.json +0 -365
  98. package/src/server/forms/test.json +0 -581
  99. package/src/server/forms/test.yaml +0 -363
  100. package/src/server/plugins/blankie.test.ts +0 -73
  101. package/src/server/plugins/blankie.ts +0 -48
  102. package/src/server/plugins/engine/services/formsService.test.js +0 -90
  103. package/src/server/plugins/engine/views/layout.html +0 -199
  104. package/src/server/plugins/router.ts +0 -201
  105. package/src/server/routes/health.js +0 -13
  106. package/src/server/routes/health.test.js +0 -35
  107. package/src/server/routes/index.test.ts +0 -125
  108. package/src/server/utils/file-form-service.test.js +0 -79
  109. package/src/server/views/404.html +0 -16
  110. package/src/server/views/500.html +0 -19
  111. package/src/server/views/help/accessibility-statement.html +0 -58
  112. package/src/server/views/help/cookie-preferences.html +0 -57
  113. package/src/server/views/help/cookies.html +0 -71
  114. package/src/server/views/help/get-support.html +0 -37
  115. package/src/server/views/help/privacy-notice.html +0 -68
  116. package/src/server/views/help/terms-and-conditions.html +0 -83
@@ -15,14 +15,13 @@ import { config } from '~/src/config/index.js'
15
15
  import { requestLogger } from '~/src/server/common/helpers/logging/request-logger.js'
16
16
  import { requestTracing } from '~/src/server/common/helpers/logging/request-tracing.js'
17
17
  import { buildRedisClient } from '~/src/server/common/helpers/redis-client.js'
18
- import { configureBlankiePlugin } from '~/src/server/plugins/blankie.js'
19
18
  import { configureCrumbPlugin } from '~/src/server/plugins/crumb.js'
20
- import { configureEnginePlugin } from '~/src/server/plugins/engine/index.js'
19
+ import { configureEnginePlugin } from '~/src/server/plugins/engine/configureEnginePlugin.js'
21
20
  import pluginErrorPages from '~/src/server/plugins/errorPages.js'
22
21
  import { plugin as pluginViews } from '~/src/server/plugins/nunjucks/index.js'
23
22
  import pluginPulse from '~/src/server/plugins/pulse.js'
24
- import pluginRouter from '~/src/server/plugins/router.js'
25
23
  import pluginSession from '~/src/server/plugins/session.js'
24
+ import { publicRoutes } from '~/src/server/routes/index.js'
26
25
  import { prepareSecureContext } from '~/src/server/secure-context.js'
27
26
  import { type RouteConfig } from '~/src/server/types.js'
28
27
 
@@ -82,15 +81,13 @@ export async function createServer(routeConfig?: RouteConfig) {
82
81
  prepareSecureContext(server)
83
82
  }
84
83
 
85
- const pluginEngine = await configureEnginePlugin(routeConfig)
86
84
  const pluginCrumb = configureCrumbPlugin(routeConfig)
87
- const pluginBlankie = configureBlankiePlugin()
85
+ const pluginEngine = await configureEnginePlugin(routeConfig)
88
86
 
89
87
  await server.register(pluginSession)
90
88
  await server.register(pluginPulse)
91
89
  await server.register(inert)
92
90
  await server.register(Scooter)
93
- await server.register(pluginBlankie)
94
91
  await server.register(pluginCrumb)
95
92
 
96
93
  server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
@@ -117,19 +114,19 @@ export async function createServer(routeConfig?: RouteConfig) {
117
114
 
118
115
  await server.register(pluginViews)
119
116
  await server.register(pluginEngine)
120
- await server.register(pluginRouter)
117
+
118
+ await server.register({
119
+ plugin: {
120
+ name: 'router',
121
+ register: (server) => {
122
+ server.route(publicRoutes)
123
+ }
124
+ }
125
+ })
126
+
121
127
  await server.register(pluginErrorPages)
122
128
  await server.register(blipp)
123
129
  await server.register(requestTracing)
124
130
 
125
- server.state('cookieConsent', {
126
- ttl: 365 * 24 * 60 * 60 * 1000, // 1 year in ms
127
- clearInvalid: true,
128
- isHttpOnly: false,
129
- isSecure: config.get('isProduction'),
130
- path: '/',
131
- encoding: 'none' // handle this inside the application so we can share frontend/backend cookie modification
132
- })
133
-
134
131
  return server
135
132
  }
@@ -8,6 +8,10 @@ import {
8
8
  plugin,
9
9
  type PluginOptions
10
10
  } from '~/src/server/plugins/engine/plugin.js'
11
+ import { findPackageRoot } from '~/src/server/plugins/engine/plugin.js'
12
+ import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
13
+ import { formsService } from '~/src/server/plugins/engine/services/localFormsService.js'
14
+ import { devtoolContext } from '~/src/server/plugins/nunjucks/context.js'
11
15
  import { type RouteConfig } from '~/src/server/types.js'
12
16
 
13
17
  export const configureEnginePlugin = async ({
@@ -27,7 +31,21 @@ export const configureEnginePlugin = async ({
27
31
 
28
32
  return {
29
33
  plugin,
30
- options: { model, services, controllers }
34
+ options: {
35
+ model,
36
+ services: services ?? {
37
+ // services for testing, else use the disk loader option for running this service locally
38
+ ...defaultServices,
39
+ formsService: await formsService()
40
+ },
41
+ controllers,
42
+ cacheName: 'session',
43
+ nunjucks: {
44
+ baseLayoutPath: 'dxt-devtool-baselayout.html',
45
+ paths: [join(findPackageRoot(), 'src/server/devserver')] // custom layout to make it really clear this is not the same as the runner
46
+ },
47
+ viewContext: devtoolContext
48
+ }
31
49
  }
32
50
  }
33
51
 
@@ -1,3 +1,7 @@
1
+ import { existsSync } from 'fs'
2
+ import { dirname, join } from 'path'
3
+ import { fileURLToPath } from 'url'
4
+
1
5
  import { hasFormComponents, slugSchema } from '@defra/forms-model'
2
6
  import Boom from '@hapi/boom'
3
7
  import {
@@ -11,6 +15,7 @@ import vision from '@hapi/vision'
11
15
  import { isEqual } from 'date-fns'
12
16
  import Joi from 'joi'
13
17
  import nunjucks, { type Environment } from 'nunjucks'
18
+ import resolvePkg from 'resolve'
14
19
 
15
20
  import { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'
16
21
  import {
@@ -25,7 +30,6 @@ import {
25
30
  redirectPath
26
31
  } from '~/src/server/plugins/engine/helpers.js'
27
32
  import {
28
- PLUGIN_PATH,
29
33
  VIEW_PATH,
30
34
  context,
31
35
  prepareNunjucksEnvironment
@@ -65,6 +69,20 @@ import * as httpService from '~/src/server/services/httpService.js'
65
69
  import { CacheService } from '~/src/server/services/index.js'
66
70
  import { type Services } from '~/src/server/types.js'
67
71
 
72
+ export function findPackageRoot() {
73
+ const currentFileName = fileURLToPath(import.meta.url)
74
+ const currentDirectoryName = dirname(currentFileName)
75
+
76
+ let dir = currentDirectoryName
77
+ while (dir !== '/') {
78
+ if (existsSync(join(dir, 'package.json'))) {
79
+ return dir
80
+ }
81
+ dir = dirname(dir)
82
+ }
83
+
84
+ throw new Error('package.json not found in parent directories')
85
+ }
68
86
  export interface PluginOptions {
69
87
  model?: FormModel
70
88
  services?: Services
@@ -73,6 +91,13 @@ export interface PluginOptions {
73
91
  viewPaths?: string[]
74
92
  filters?: Record<string, FilterFunction>
75
93
  pluginPath?: string
94
+ nunjucks: {
95
+ baseLayoutPath: string
96
+ paths: string[]
97
+ }
98
+ viewContext: (
99
+ request: FormRequest | FormRequestPayload | null
100
+ ) => Record<string, unknown>
76
101
  }
77
102
 
78
103
  export const plugin = {
@@ -85,23 +110,25 @@ export const plugin = {
85
110
  services = defaultServices,
86
111
  controllers,
87
112
  cacheName,
88
- viewPaths,
89
113
  filters,
90
- pluginPath = PLUGIN_PATH
114
+ nunjucks: nunjucksOptions,
115
+ viewContext
91
116
  } = options
92
117
  const { formsService } = services
93
118
  const cacheService = new CacheService(server, cacheName)
94
119
 
95
- // Paths array to tell `vision` and `nunjucks` where template files are stored.
96
- // We need to include `VIEW_PATH` in addition the runtime path (node_modules)
97
- // to keep the local tests working
98
- const path = [`${pluginPath}/${VIEW_PATH}`, VIEW_PATH]
120
+ const packageRoot = findPackageRoot()
121
+ const govukFrontendPath = dirname(
122
+ resolvePkg.sync('govuk-frontend/package.json')
123
+ )
99
124
 
100
- // Include any additional user provided view paths so our internal views engine
101
- // can find any files they provide from the consumer side if using custom `page.view`s
102
- if (Array.isArray(viewPaths) && viewPaths.length) {
103
- path.push(...viewPaths)
104
- }
125
+ const viewPathResolved = join(packageRoot, VIEW_PATH)
126
+
127
+ const paths = [
128
+ ...nunjucksOptions.paths,
129
+ viewPathResolved,
130
+ join(govukFrontendPath, 'dist')
131
+ ]
105
132
 
106
133
  await server.register({
107
134
  plugin: vision,
@@ -127,10 +154,7 @@ export const plugin = {
127
154
  ) => {
128
155
  // Nunjucks also needs an additional path configuration
129
156
  // to use the templates and macros from `govuk-frontend`
130
- const environment = nunjucks.configure([
131
- ...path,
132
- 'node_modules/govuk-frontend/dist'
133
- ])
157
+ const environment = nunjucks.configure(paths)
134
158
 
135
159
  // Applies custom filters and globals for nunjucks
136
160
  // that are required by the `forms-engine-plugin`
@@ -142,12 +166,14 @@ export const plugin = {
142
166
  }
143
167
  }
144
168
  },
145
- path,
169
+ path: paths,
146
170
  // Provides global context used with all templates
147
171
  context
148
172
  }
149
173
  })
150
174
 
175
+ server.expose('baseLayoutPath', nunjucksOptions.baseLayoutPath)
176
+ server.expose('viewContext', viewContext)
151
177
  server.expose('cacheService', cacheService)
152
178
 
153
179
  server.app.model = model
@@ -1,46 +1,28 @@
1
- import { formMetadataSchema } from '@defra/forms-model'
2
-
3
- import { config } from '~/src/config/index.js'
4
- import { FormStatus } from '~/src/server/routes/types.js'
5
- import { getJson } from '~/src/server/services/httpService.js'
1
+ const error = Error(
2
+ 'Not implemented. Consider setting up a form loader or providing a service implementation.'
3
+ )
6
4
 
5
+ // eslint-disable-next-line jsdoc/require-returns-check
7
6
  /**
8
- * Retrieves a form definition from the form manager for a given slug
9
- * @param {string} slug - the slug of the form
7
+ * Dummy function to get form metadata.
8
+ * @param {string} _slug - the slug of the form
9
+ * @returns {Promise<FormMetadata>}
10
10
  */
11
- export async function getFormMetadata(slug) {
12
- const getJsonByType = /** @type {typeof getJson<FormMetadata>} */ (getJson)
13
-
14
- const { payload: metadata } = await getJsonByType(
15
- `${config.get('managerUrl')}/forms/slug/${slug}`
16
- )
17
-
18
- // Run it through the schema to coerce dates
19
- const result = formMetadataSchema.validate(metadata)
20
-
21
- if (result.error) {
22
- throw result.error
23
- }
24
-
25
- return result.value
11
+ export function getFormMetadata(_slug) {
12
+ throw error
26
13
  }
27
14
 
15
+ // eslint-disable-next-line jsdoc/require-returns-check
28
16
  /**
29
- * Retrieves a form definition from the form manager for a given id
30
- * @param {string} id - the id of the form
31
- * @param {FormStatus} state - the state of the form
17
+ * Dummy function to get form metadata.
18
+ * @param {string} _id - the id of the form
19
+ * @param {FormStatus} _state - the state of the form
20
+ * @returns {Promise<FormDefinition | undefined>}
32
21
  */
33
- export async function getFormDefinition(id, state) {
34
- const getJsonByType = /** @type {typeof getJson<FormDefinition>} */ (getJson)
35
-
36
- const suffix = state === FormStatus.Draft ? `/${state}` : ''
37
- const { payload: definition } = await getJsonByType(
38
- `${config.get('managerUrl')}/forms/${id}/definition${suffix}`
39
- )
40
-
41
- return definition
22
+ export function getFormDefinition(_id, _state) {
23
+ throw error
42
24
  }
43
25
 
44
26
  /**
45
- * @import { FormDefinition, FormMetadata } from '@defra/forms-model'
27
+ * @import { FormStatus, FormDefinition, FormMetadata } from '@defra/forms-model'
46
28
  */
@@ -0,0 +1,49 @@
1
+ import { config } from '~/src/config/index.js'
2
+ import { FileFormService } from '~/src/server/utils/file-form-service.js'
3
+
4
+ // Create shared form metadata
5
+ const now = new Date()
6
+ const user = { id: 'user', displayName: 'Username' }
7
+ const author = {
8
+ createdAt: now,
9
+ createdBy: user,
10
+ updatedAt: now,
11
+ updatedBy: user
12
+ }
13
+ const metadata = {
14
+ organisation: 'Defra',
15
+ teamName: 'Team name',
16
+ teamEmail: 'team@defra.gov.uk',
17
+ submissionGuidance: "Thanks for your submission, we'll be in touch",
18
+ notificationEmail: config.get('submissionEmailAddress'),
19
+ ...author,
20
+ live: author
21
+ }
22
+
23
+ /**
24
+ * Return an function rather than the service directly. This is to prevent consumer applications
25
+ * blowing up as they won't have these files on disk. We can defer the execution until when it's
26
+ * needed, i.e. the createServer function of the devtool.
27
+ */
28
+ export const formsService = async () => {
29
+ // Instantiate the file loader form service
30
+ const loader = new FileFormService()
31
+
32
+ // Add a Json form
33
+ await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.json', {
34
+ ...metadata,
35
+ id: '95e92559-968d-44ae-8666-2b1ad3dffd31',
36
+ title: 'Register as a unicorn breeder',
37
+ slug: 'register-as-a-unicorn-breeder'
38
+ })
39
+
40
+ // Add a Yaml form
41
+ await loader.addForm('src/server/forms/register-as-a-unicorn-breeder.yaml', {
42
+ ...metadata,
43
+ id: '641aeafd-13dd-40fa-9186-001703800efb',
44
+ title: 'Register as a unicorn breeder (yaml)',
45
+ slug: 'register-as-a-unicorn-breeder-yaml' // if we needed to validate any JSON logic, make it available for convenience
46
+ })
47
+
48
+ return loader.toFormsService()
49
+ }
@@ -1,4 +1,4 @@
1
- {% extends "layout.html" %}
1
+ {% extends baseLayoutPath %}
2
2
 
3
3
  {% from "govuk/components/panel/macro.njk" import govukPanel %}
4
4
 
@@ -35,7 +35,7 @@
35
35
  {% block bodyEnd %}
36
36
  {{ super() }}
37
37
  <script type="module" nonce="{{ cspNonce }}">
38
- import { initFileUpload } from '{{ getAssetPath("file-upload.js") }}';
38
+ import { initFileUpload } from '{{ getDxtAssetPath("file-upload.js") }}';
39
39
  if (document.readyState === 'loading') {
40
40
  document.addEventListener('DOMContentLoaded', initFileUpload);
41
41
  } else {
@@ -1,4 +1,4 @@
1
- {% extends 'layout.html' %}
1
+ {% extends baseLayoutPath %}
2
2
 
3
3
  {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
4
4
  {% from "partials/components.html" import componentList with context %}
@@ -1,4 +1,4 @@
1
- {% extends "layout.html" %}
1
+ {% extends baseLayoutPath %}
2
2
 
3
3
  {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
4
4
  {% from "govuk/components/button/macro.njk" import govukButton %}
@@ -1,4 +1,4 @@
1
- {% extends "layout.html" %}
1
+ {% extends baseLayoutPath %}
2
2
 
3
3
  {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
4
4
  {% from "govuk/components/button/macro.njk" import govukButton %}
@@ -1,4 +1,4 @@
1
- {% extends "layout.html" %}
1
+ {% extends baseLayoutPath %}
2
2
 
3
3
  {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
4
4
  {% from "govuk/components/summary-list/macro.njk" import govukSummaryList %}
@@ -3,7 +3,6 @@ import {
3
3
  type ResponseToolkit,
4
4
  type ServerRegisterPluginObject
5
5
  } from '@hapi/hapi'
6
- import { StatusCodes } from 'http-status-codes'
7
6
 
8
7
  /*
9
8
  * Add an `onPreResponse` listener to return error pages
@@ -20,36 +19,15 @@ export default {
20
19
  // processing the request
21
20
  const statusCode = response.output.statusCode
22
21
 
23
- // Check for a form model on the request
24
- // and use it to set the correct service name
25
- // and start page path. In the event of a error
26
- // happening inside a "form" level request, the header
27
- // then displays the contextual form text and href
28
- const model = request.app.model
29
- const viewModel = model
30
- ? {
31
- name: model.name,
32
- serviceUrl: `/${model.basePath}`
33
- }
34
- : undefined
35
-
36
- // In the event of 404
37
- // return the `404` view
38
- if (statusCode === StatusCodes.NOT_FOUND.valueOf()) {
39
- return h.view('404', viewModel).code(statusCode)
40
- }
41
-
42
- request.log('error', {
22
+ const error = {
43
23
  statusCode,
44
- data: response.data,
45
24
  message: response.message,
46
25
  stack: response.stack
47
- })
26
+ }
48
27
 
49
- request.logger.error(response.stack)
28
+ request.log('error', error)
50
29
 
51
- // The return the `500` view
52
- return h.view('500', viewModel).code(statusCode)
30
+ return h.response(error).code(statusCode)
53
31
  }
54
32
  return h.continue
55
33
  })
@@ -5,7 +5,6 @@ import Boom from '@hapi/boom'
5
5
  import { StatusCodes } from 'http-status-codes'
6
6
 
7
7
  import pkg from '~/package.json' with { type: 'json' }
8
- import { parseCookieConsent } from '~/src/common/cookies.js'
9
8
  import { config } from '~/src/config/index.js'
10
9
  import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
11
10
  import { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'
@@ -23,20 +22,8 @@ let webpackManifest
23
22
  * @param {FormRequest | FormRequestPayload | null} request
24
23
  */
25
24
  export function context(request) {
26
- const manifestPath = join(config.get('publicDir'), 'assets-manifest.json')
27
-
28
- if (!webpackManifest) {
29
- try {
30
- // eslint-disable-next-line -- Allow JSON type 'any'
31
- webpackManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
32
- } catch {
33
- logger.error(`Webpack ${basename(manifestPath)} not found`)
34
- }
35
- }
25
+ const { params, path, response } = request ?? {}
36
26
 
37
- const { params, path, query = {}, response, state } = request ?? {}
38
-
39
- const isForceAccess = 'force' in query
40
27
  const isPreviewMode = path?.startsWith(PREVIEW_PATH_PREFIX)
41
28
 
42
29
  // Only add the slug in to the context if the response is OK.
@@ -44,45 +31,68 @@ export function context(request) {
44
31
  const isResponseOK =
45
32
  !Boom.isBoom(response) && response?.statusCode === StatusCodes.OK
46
33
 
34
+ const pluginStorage = request?.server.plugins['forms-engine-plugin']
35
+ let consumerViewContext = {}
36
+
37
+ if (!pluginStorage) {
38
+ throw Error('context called before plugin registered')
39
+ }
40
+
41
+ if (!pluginStorage.baseLayoutPath) {
42
+ throw Error('Missing baseLayoutPath in plugin.options.nunjucks')
43
+ }
44
+
45
+ if ('viewContext' in pluginStorage) {
46
+ consumerViewContext = pluginStorage.viewContext(request)
47
+ }
48
+
47
49
  /** @type {ViewContext} */
48
50
  const ctx = {
51
+ // take consumers props first so we can override it
52
+ ...consumerViewContext,
53
+ baseLayoutPath: pluginStorage.baseLayoutPath,
49
54
  appVersion: pkg.version,
50
- assetPath: '/assets',
51
55
  config: {
52
56
  cdpEnvironment: config.get('cdpEnvironment'),
53
57
  designerUrl: config.get('designerUrl'),
54
58
  feedbackLink: encodeUrl(config.get('feedbackLink')),
55
59
  phaseTag: config.get('phaseTag'),
56
- serviceBannerText: config.get('serviceBannerText'),
57
60
  serviceName: config.get('serviceName'),
58
61
  serviceVersion: config.get('serviceVersion')
59
62
  },
60
63
  crumb: safeGenerateCrumb(request),
61
- cspNonce: request?.plugins.blankie?.nonces?.script,
62
- currentPath: request ? `${request.path}${request.url.search}` : undefined,
64
+ currentPath: `${request.path}${request.url.search}`,
63
65
  previewMode: isPreviewMode ? params?.state : undefined,
64
- slug: isResponseOK ? params?.slug : undefined,
65
-
66
- getAssetPath: (asset = '') => {
67
- return `/${webpackManifest?.[asset] ?? asset}`
68
- }
66
+ slug: isResponseOK ? params?.slug : undefined
69
67
  }
70
68
 
71
- if (!isForceAccess) {
72
- ctx.config.googleAnalyticsTrackingId = config.get(
73
- 'googleAnalyticsTrackingId'
74
- )
69
+ return ctx
70
+ }
71
+
72
+ /**
73
+ * Returns the context for the devtool. Consumers won't have access to this.
74
+ */
75
+ export function devtoolContext() {
76
+ const manifestPath = join(config.get('publicDir'), 'assets-manifest.json')
75
77
 
76
- if (typeof state?.cookieConsent === 'string') {
77
- ctx.cookieConsent = parseCookieConsent(state.cookieConsent)
78
+ if (!webpackManifest) {
79
+ try {
80
+ // eslint-disable-next-line -- Allow JSON type 'any'
81
+ webpackManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
82
+ } catch {
83
+ logger.error(`Webpack ${basename(manifestPath)} not found`)
78
84
  }
79
85
  }
80
86
 
81
- return ctx
87
+ return {
88
+ assetPath: '/assets',
89
+ getDxtAssetPath: (asset = '') => {
90
+ return `/${webpackManifest?.[asset] ?? asset}`
91
+ }
92
+ }
82
93
  }
83
94
 
84
95
  /**
85
- * @import { CookieConsent } from '~/src/common/types.js'
86
96
  * @import { ViewContext } from '~/src/server/plugins/nunjucks/types.js'
87
97
  * @import { FormRequest, FormRequestPayload } from '~/src/server/routes/types.js'
88
98
  */
@@ -1,28 +1,29 @@
1
1
  import { tmpdir } from 'node:os'
2
2
 
3
- import { config } from '~/src/config/index.js'
4
- import { encodeUrl } from '~/src/server/plugins/engine/helpers.js'
5
- import { context } from '~/src/server/plugins/nunjucks/context.js'
3
+ import {
4
+ context,
5
+ devtoolContext
6
+ } from '~/src/server/plugins/nunjucks/context.js'
6
7
 
7
8
  describe('Nunjucks context', () => {
8
9
  beforeEach(() => jest.resetModules())
9
10
 
10
11
  describe('Asset path', () => {
11
12
  it("should include 'assetPath' for GOV.UK Frontend icons", () => {
12
- const { assetPath } = context(null)
13
+ const { assetPath } = devtoolContext()
13
14
  expect(assetPath).toBe('/assets')
14
15
  })
15
16
  })
16
17
 
17
18
  describe('Asset helper', () => {
18
19
  it("should locate 'assets-manifest.json' assets", () => {
19
- const { getAssetPath } = context(null)
20
+ const { getDxtAssetPath } = devtoolContext()
20
21
 
21
- expect(getAssetPath('example.scss')).toBe(
22
+ expect(getDxtAssetPath('example.scss')).toBe(
22
23
  '/stylesheets/example.xxxxxxx.min.css'
23
24
  )
24
25
 
25
- expect(getAssetPath('example.mjs')).toBe(
26
+ expect(getDxtAssetPath('example.mjs')).toBe(
26
27
  '/javascripts/example.xxxxxxx.min.js'
27
28
  )
28
29
  })
@@ -32,43 +33,33 @@ describe('Nunjucks context', () => {
32
33
  const { config } = await import('~/src/config/index.js')
33
34
 
34
35
  // Import when isolated to avoid cache
35
- const { context } = await import(
36
+ const { devtoolContext } = await import(
36
37
  '~/src/server/plugins/nunjucks/context.js'
37
38
  )
38
39
 
39
40
  // Update config for missing manifest
40
41
  config.set('publicDir', tmpdir())
41
- const { getAssetPath } = context(null)
42
+ const { getDxtAssetPath } = devtoolContext()
42
43
 
43
44
  // Uses original paths when missing
44
- expect(getAssetPath('example.scss')).toBe('/example.scss')
45
- expect(getAssetPath('example.mjs')).toBe('/example.mjs')
45
+ expect(getDxtAssetPath('example.scss')).toBe('/example.scss')
46
+ expect(getDxtAssetPath('example.mjs')).toBe('/example.mjs')
46
47
  })
47
48
  })
48
49
 
49
50
  it('should return path to unknown assets', () => {
50
- const { getAssetPath } = context(null)
51
+ const { getDxtAssetPath } = devtoolContext()
51
52
 
52
- expect(getAssetPath()).toBe('/')
53
- expect(getAssetPath('example.jpg')).toBe('/example.jpg')
54
- expect(getAssetPath('example.gif')).toBe('/example.gif')
53
+ expect(getDxtAssetPath()).toBe('/')
54
+ expect(getDxtAssetPath('example.jpg')).toBe('/example.jpg')
55
+ expect(getDxtAssetPath('example.gif')).toBe('/example.gif')
55
56
  })
56
57
  })
57
58
 
58
59
  describe('Config', () => {
59
60
  it('should include environment, phase tag and service info', () => {
60
- const ctx = context(null)
61
-
62
- expect(ctx.config).toEqual(
63
- expect.objectContaining({
64
- cdpEnvironment: config.get('cdpEnvironment'),
65
- feedbackLink: encodeUrl(config.get('feedbackLink')),
66
- googleAnalyticsTrackingId: config.get('googleAnalyticsTrackingId'),
67
- phaseTag: config.get('phaseTag'),
68
- serviceBannerText: config.get('serviceBannerText'),
69
- serviceName: config.get('serviceName'),
70
- serviceVersion: config.get('serviceVersion')
71
- })
61
+ expect(() => context(null)).toThrow(
62
+ 'context called before plugin registered'
72
63
  )
73
64
  })
74
65
  })
@@ -83,6 +74,9 @@ describe('Nunjucks context', () => {
83
74
  plugins: {
84
75
  crumb: {
85
76
  generate: jest.fn()
77
+ },
78
+ 'forms-engine-plugin': {
79
+ baseLayoutPath: 'randomValue'
86
80
  }
87
81
  }
88
82
  },
@@ -113,6 +107,9 @@ describe('Nunjucks context', () => {
113
107
  plugins: {
114
108
  crumb: {
115
109
  generate: jest.fn().mockReturnValue(mockCrumb)
110
+ },
111
+ 'forms-engine-plugin': {
112
+ baseLayoutPath: 'randomValue'
116
113
  }
117
114
  }
118
115
  },