@defra/lis-infra-ui-services 0.47.0 → 0.48.0

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.
package/README.md CHANGED
@@ -47,7 +47,6 @@ import { createNunjucksConfig } from '@defra/lis-infra-ui-services/nunjucks/plug
47
47
  const nunjucksConfig = createNunjucksConfig({
48
48
  config, // reads root, assetPath, serviceName, isProduction, nunjucks.watch, nunjucks.noCache
49
49
  logger,
50
- getRequestBasePath,
51
50
  moduleId: 'cattle-home' // spokes only
52
51
  })
53
52
 
@@ -62,22 +61,24 @@ individual `/nunjucks/format-currency`, `/nunjucks/format-date` and `/nunjucks/q
62
61
  subpaths. `/nunjucks/globals` exports `PERMISSIONS` and `govukRebrand` (always `true`, which turns
63
62
  on the GOV.UK rebrand in templates).
64
63
 
65
- ### Base path
64
+ ### Asset path
66
65
 
67
- Hubs serve spokes under a path prefix. `createBasePathHelpersForConfig` gives the helpers
68
- `getRequestBasePath`, `buildAppPath`, `isPrefixedRequest`, `getRouteVariants` and `getAssetPaths`. The
69
- prefix comes from the module in `@defra/lis-hubs-infra-registry`.
66
+ A hub serves its built CSS and JavaScript at its configured `assetPath`. A spoke serves its own at
67
+ its `assets` mount from `@defra/lis-hubs-infra-registry` (`/assets/cattle-home`), which the hub
68
+ proxies to it unchanged, so the same URL works through a hub or directly.
69
+ `createAssetPathHelpers` resolves that root once; `getAssetPaths` feeds the static files plugin and
70
+ the Nunjucks context derives the same root itself from `moduleId`.
70
71
 
71
72
  ```js
72
- import { createBasePathHelpersForConfig } from '@defra/lis-infra-ui-services/base-path'
73
+ import { createAssetPathHelpers } from '@defra/lis-infra-ui-services/asset-path'
73
74
 
74
- const { getRequestBasePath, getAssetPaths } = createBasePathHelpersForConfig({
75
+ const { getAssetPaths } = createAssetPathHelpers({
75
76
  moduleId: 'cattle-home', // leave out in a hub
76
77
  assetPath: config.get('assetPath')
77
78
  })
78
79
  ```
79
80
 
80
- The standalone functions of the same names are exported too.
81
+ The standalone `getAssetRoot` is exported too.
81
82
 
82
83
  ### Static files
83
84
 
@@ -246,7 +247,6 @@ The contact link on these pages comes from the `errorPages` option of `createNun
246
247
  createNunjucksConfig({
247
248
  config,
248
249
  logger,
249
- getRequestBasePath,
250
250
  errorPages: {
251
251
  contactHref:
252
252
  'https://www.gov.uk/government/organisations/british-cattle-movement-service' // default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/lis-infra-ui-services",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "description": "UI-Services shared definitions for the livestock taxonomy topology.",
5
5
  "main": "src/index.js",
6
6
  "repository": {
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "exports": {
11
11
  ".": "./src/index.js",
12
- "./base-path": "./src/base-path.js",
12
+ "./asset-path": "./src/asset-path.js",
13
13
  "./config-formats": "./src/config-formats.js",
14
14
  "./cookies": "./src/cookies/index.js",
15
15
  "./cookies/cookies-policy": "./src/cookies/cookies-policy.js",
@@ -77,13 +77,13 @@
77
77
  "peerDependencies": {
78
78
  "@defra/lis-hubs-infra-access": ">=0.29.0 <1.0.0",
79
79
  "@defra/lis-hubs-infra-core": ">=0.4.0 <1.0.0",
80
- "@defra/lis-hubs-infra-registry": ">=0.6.0 <1.0.0",
80
+ "@defra/lis-hubs-infra-registry": ">=0.13.0 <1.0.0",
81
81
  "govuk-frontend": ">=6.5.0 <7.0.0"
82
82
  },
83
83
  "devDependencies": {
84
84
  "@defra/lis-hubs-infra-access": "^0.35.0",
85
85
  "@defra/lis-hubs-infra-core": "^0.17.0",
86
- "@defra/lis-hubs-infra-registry": "^0.6.0",
86
+ "@defra/lis-hubs-infra-registry": "^0.13.0",
87
87
  "@defra/lis-infra-eslint-config": "^0.6.0",
88
88
  "@hapi/hapi": "^21.4.10",
89
89
  "@vitest/coverage-istanbul": "^4.1.10",
@@ -0,0 +1,31 @@
1
+ import { getMountPath } from '@defra/lis-hubs-infra-registry'
2
+
3
+ /**
4
+ * Where an app's built CSS and JavaScript are served from. A hub serves its
5
+ * own at its configured asset path. A spoke serves its own at its `assets`
6
+ * mount from the registry, which the hub proxies to it unchanged, so the
7
+ * same URL works whether the spoke is reached through a hub or directly.
8
+ *
9
+ * @param {{ moduleId?: string, assetPath: string }} options the module id of a spoke (leave out in a hub) and the hub's own asset path
10
+ * @returns {string}
11
+ */
12
+ export function getAssetRoot({ moduleId, assetPath }) {
13
+ return moduleId ? getMountPath(moduleId, 'assets') : assetPath
14
+ }
15
+
16
+ /**
17
+ * @param {{ moduleId?: string, assetPath: string }} options
18
+ * @returns {{ getAssetRoot: () => string, getAssetPaths: () => string[] }}
19
+ */
20
+ export function createAssetPathHelpers({ moduleId, assetPath }) {
21
+ const assetRoot = getAssetRoot({ moduleId, assetPath })
22
+
23
+ return {
24
+ getAssetRoot() {
25
+ return assetRoot
26
+ },
27
+ getAssetPaths() {
28
+ return [assetRoot]
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,89 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest'
2
+
3
+ import { getMountPath } from '@defra/lis-hubs-infra-registry'
4
+ import { createAssetPathHelpers, getAssetRoot } from './asset-path.js'
5
+
6
+ vi.mock('@defra/lis-hubs-infra-registry', () => ({ getMountPath: vi.fn() }))
7
+
8
+ const mocks = {
9
+ getMountPath: vi.mocked(getMountPath)
10
+ }
11
+
12
+ describe('getAssetRoot()', () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks()
15
+ })
16
+
17
+ test('is the assets mount of the module when a moduleId is given', () => {
18
+ // Arrange
19
+ mocks.getMountPath.mockReturnValue('/assets/cattle-register')
20
+
21
+ // Act
22
+ const result = getAssetRoot({
23
+ moduleId: 'cattle-register',
24
+ assetPath: '/public'
25
+ })
26
+
27
+ // Assert
28
+ expect(mocks.getMountPath).toHaveBeenCalledWith('cattle-register', 'assets')
29
+ expect(result).toBe('/assets/cattle-register')
30
+ })
31
+
32
+ test('is the configured asset path when no moduleId is given', () => {
33
+ // Act
34
+ const result = getAssetRoot({ assetPath: '/public' })
35
+
36
+ // Assert
37
+ expect(result).toBe('/public')
38
+ expect(mocks.getMountPath).not.toHaveBeenCalled()
39
+ })
40
+
41
+ test('propagates an unknown module', () => {
42
+ // Arrange
43
+ mocks.getMountPath.mockImplementation(() => {
44
+ throw new Error('No module registered with id "unknown"')
45
+ })
46
+
47
+ // Act
48
+ let error
49
+ try {
50
+ getAssetRoot({ moduleId: 'unknown', assetPath: '/public' })
51
+ } catch (e) {
52
+ error = e
53
+ }
54
+
55
+ // Assert
56
+ expect(error).toEqual(new Error('No module registered with id "unknown"'))
57
+ })
58
+ })
59
+
60
+ describe('createAssetPathHelpers()', () => {
61
+ beforeEach(() => {
62
+ vi.clearAllMocks()
63
+ })
64
+
65
+ test('resolves the asset root once and serves assets there', () => {
66
+ // Arrange
67
+ mocks.getMountPath.mockReturnValue('/assets/cattle-register')
68
+
69
+ // Act
70
+ const helpers = createAssetPathHelpers({
71
+ moduleId: 'cattle-register',
72
+ assetPath: '/public'
73
+ })
74
+
75
+ // Assert
76
+ expect(helpers.getAssetRoot()).toBe('/assets/cattle-register')
77
+ expect(helpers.getAssetPaths()).toEqual(['/assets/cattle-register'])
78
+ expect(mocks.getMountPath).toHaveBeenCalledTimes(1)
79
+ })
80
+
81
+ test('serves a hub its own asset path', () => {
82
+ // Act
83
+ const helpers = createAssetPathHelpers({ assetPath: '/public' })
84
+
85
+ // Assert
86
+ expect(helpers.getAssetRoot()).toBe('/public')
87
+ expect(helpers.getAssetPaths()).toEqual(['/public'])
88
+ })
89
+ })
@@ -3,6 +3,8 @@ import path from 'node:path'
3
3
  import { hasPermission } from '@defra/lis-hubs-infra-access/authorization'
4
4
  import { HUBS, getModuleById } from '@defra/lis-hubs-infra-registry'
5
5
 
6
+ import { getAssetRoot } from '../asset-path.js'
7
+
6
8
  import { COOKIES_PATH, getCookiesPolicy } from '../cookies/cookies-policy.js'
7
9
 
8
10
  const HUB_SERVICE_NAME_PREFIX = 'lis-hubs-'
@@ -57,12 +59,11 @@ function createPermissionCheck(request) {
57
59
  */
58
60
 
59
61
  /**
60
- * @param {{ config: object, getRequestBasePath: Function, logger: object, readFileSync: Function, moduleId?: string, errorPages?: ErrorPageLinks }} options
62
+ * @param {{ config: object, logger: object, readFileSync: Function, moduleId?: string, errorPages?: ErrorPageLinks }} options
61
63
  * @returns {Function}
62
64
  */
63
65
  export function createNunjucksContextBuilder({
64
66
  config,
65
- getRequestBasePath,
66
67
  logger,
67
68
  readFileSync,
68
69
  moduleId,
@@ -72,6 +73,10 @@ export function createNunjucksContextBuilder({
72
73
  contactHref: errorPages.contactHref ?? BCMS_CONTACT_HREF
73
74
  }
74
75
  const sectionName = moduleId ? (getModuleById(moduleId)?.label ?? null) : null
76
+ const assetRoot = getAssetRoot({
77
+ moduleId,
78
+ assetPath: config.get('assetPath')
79
+ })
75
80
  const manifestPath = path.join(
76
81
  config.get('root'),
77
82
  '.public/.vite/manifest.json'
@@ -93,8 +98,6 @@ export function createNunjucksContextBuilder({
93
98
  }
94
99
  }
95
100
 
96
- const requestBasePath = getRequestBasePath(request)
97
- const assetRoot = `${requestBasePath}${config.get('assetPath')}`
98
101
  const requestHubId = resolveHubId(request)
99
102
 
100
103
  return {
@@ -1,8 +1,14 @@
1
1
  import assert from 'node:assert/strict'
2
- import { describe, expect, test } from 'vitest'
2
+ import { describe, expect, test, vi } from 'vitest'
3
3
 
4
+ import { getMountPath } from '@defra/lis-hubs-infra-registry'
4
5
  import { createNunjucksContextBuilder } from './context.js'
5
6
 
7
+ vi.mock('@defra/lis-hubs-infra-registry', async (importOriginal) => ({
8
+ ...(await importOriginal()),
9
+ getMountPath: vi.fn()
10
+ }))
11
+
6
12
  function createConfig(overrides = {}) {
7
13
  const values = {
8
14
  root: '/workspace/example-app',
@@ -23,7 +29,6 @@ test('createNunjucksContextBuilder resolves built asset paths from the Vite mani
23
29
  const loggerMessages = []
24
30
  const contextBuilder = createNunjucksContextBuilder({
25
31
  config: createConfig(),
26
- getRequestBasePath: () => '',
27
32
  logger: {
28
33
  error(loggedError, message) {
29
34
  loggerMessages.push(message)
@@ -51,7 +56,6 @@ test('createNunjucksContextBuilder falls back to the source asset path without l
51
56
  const loggerMessages = []
52
57
  const contextBuilder = createNunjucksContextBuilder({
53
58
  config: createConfig(),
54
- getRequestBasePath: () => '',
55
59
  logger: {
56
60
  error(loggedError, message) {
57
61
  loggerMessages.push(message)
@@ -75,7 +79,6 @@ test('createNunjucksContextBuilder resolves sectionName from the module registry
75
79
  const loggerMessages = []
76
80
  const contextBuilder = createNunjucksContextBuilder({
77
81
  config: createConfig(),
78
- getRequestBasePath: () => '',
79
82
  logger: {
80
83
  error(loggedError, message) {
81
84
  loggerMessages.push(message)
@@ -95,7 +98,6 @@ test('createNunjucksContextBuilder leaves sectionName null when no moduleId is g
95
98
  const loggerMessages = []
96
99
  const contextBuilder = createNunjucksContextBuilder({
97
100
  config: createConfig(),
98
- getRequestBasePath: () => '',
99
101
  logger: {
100
102
  error(loggedError, message) {
101
103
  loggerMessages.push(message)
@@ -114,7 +116,6 @@ test('createNunjucksContextBuilder logs once when the Vite manifest is unavailab
114
116
  const loggerMessages = []
115
117
  const contextBuilder = createNunjucksContextBuilder({
116
118
  config: createConfig({ isProduction: true }),
117
- getRequestBasePath: () => '',
118
119
  logger: {
119
120
  error(loggedError, message) {
120
121
  loggerMessages.push(message)
@@ -134,11 +135,12 @@ test('createNunjucksContextBuilder logs once when the Vite manifest is unavailab
134
135
  assert.deepEqual(loggerMessages, ['Vite manifest.json not found'])
135
136
  })
136
137
 
137
- test('createNunjucksContextBuilder always sets serviceUrl to the hub root even when rendered under a spoke base path', () => {
138
+ test('createNunjucksContextBuilder serves a spoke its assets from its assets mount, with serviceUrl still the hub root', () => {
138
139
  const loggerMessages = []
140
+ vi.mocked(getMountPath).mockReturnValue('/assets/cattle-register')
139
141
  const contextBuilder = createNunjucksContextBuilder({
140
142
  config: createConfig(),
141
- getRequestBasePath: () => '/cattle/register',
143
+ moduleId: 'cattle-register',
142
144
  logger: {
143
145
  error(loggedError, message) {
144
146
  loggerMessages.push(message)
@@ -155,15 +157,19 @@ test('createNunjucksContextBuilder always sets serviceUrl to the hub root even w
155
157
 
156
158
  const context = contextBuilder({ headers: {}, path: '/' })
157
159
 
160
+ expect(getMountPath).toHaveBeenCalledWith('cattle-register', 'assets')
158
161
  assert.equal(context.serviceUrl, '/')
159
- assert.equal(context.assetPath, '/cattle/register/public/assets')
162
+ assert.equal(context.assetPath, '/assets/cattle-register/assets')
163
+ assert.equal(
164
+ context.getAssetPath('src/client/stylesheets/application.scss'),
165
+ '/assets/cattle-register/assets/application-123.css'
166
+ )
160
167
  assert.deepEqual(loggerMessages, [])
161
168
  })
162
169
 
163
170
  function createSignedInContextBuilder() {
164
171
  return createNunjucksContextBuilder({
165
172
  config: createConfig(),
166
- getRequestBasePath: () => '',
167
173
  logger: { error: () => undefined },
168
174
  readFileSync: () => '{}'
169
175
  })
@@ -252,7 +258,6 @@ describe('createNunjucksContextBuilder() hub flags', () => {
252
258
  // Arrange
253
259
  const contextBuilder = createNunjucksContextBuilder({
254
260
  config: createConfig(),
255
- getRequestBasePath: () => '',
256
261
  logger: {},
257
262
  readFileSync: () => '{}',
258
263
  moduleId: 'cattle-register'
@@ -275,7 +280,6 @@ describe('createNunjucksContextBuilder() hub flags', () => {
275
280
  // Arrange
276
281
  const contextBuilder = createNunjucksContextBuilder({
277
282
  config: createConfig(),
278
- getRequestBasePath: () => '',
279
283
  logger: {},
280
284
  readFileSync: () => '{}',
281
285
  moduleId: 'cattle-register'
@@ -298,7 +302,6 @@ describe('createNunjucksContextBuilder() hub flags', () => {
298
302
  // Arrange
299
303
  const contextBuilder = createNunjucksContextBuilder({
300
304
  config: createConfig(),
301
- getRequestBasePath: () => '',
302
305
  logger: {},
303
306
  readFileSync: () => '{}',
304
307
  moduleId: 'cattle-register'
@@ -317,7 +320,6 @@ describe('createNunjucksContextBuilder() hub flags', () => {
317
320
  // Arrange
318
321
  const contextBuilder = createNunjucksContextBuilder({
319
322
  config: createConfig(),
320
- getRequestBasePath: () => '',
321
323
  logger: {},
322
324
  readFileSync: () => '{}',
323
325
  moduleId: 'cattle-register'
@@ -342,7 +344,6 @@ describe('createNunjucksContextBuilder() errorPages', () => {
342
344
  // Arrange
343
345
  const contextBuilder = createNunjucksContextBuilder({
344
346
  config: createConfig(),
345
- getRequestBasePath: () => '',
346
347
  logger: {},
347
348
  readFileSync: () => '{}'
348
349
  })
@@ -362,7 +363,6 @@ describe('createNunjucksContextBuilder() errorPages', () => {
362
363
  // Arrange
363
364
  const contextBuilder = createNunjucksContextBuilder({
364
365
  config: createConfig(),
365
- getRequestBasePath: () => '',
366
366
  logger: {},
367
367
  readFileSync: () => '{}',
368
368
  errorPages: {
@@ -384,7 +384,6 @@ describe('createNunjucksContextBuilder() errorPages', () => {
384
384
  // Arrange
385
385
  const contextBuilder = createNunjucksContextBuilder({
386
386
  config: createConfig(),
387
- getRequestBasePath: () => '',
388
387
  logger: {},
389
388
  readFileSync: () => '{}',
390
389
  errorPages: { contactHref: undefined }
@@ -407,7 +406,6 @@ describe('createNunjucksContextBuilder() hasPermission()', () => {
407
406
  // Arrange
408
407
  const contextBuilder = createNunjucksContextBuilder({
409
408
  config: createConfig(),
410
- getRequestBasePath: () => '',
411
409
  logger: {},
412
410
  readFileSync: () => '{}'
413
411
  })
@@ -436,7 +434,6 @@ describe('createNunjucksContextBuilder() hasPermission()', () => {
436
434
  // Arrange
437
435
  const contextBuilder = createNunjucksContextBuilder({
438
436
  config: createConfig(),
439
- getRequestBasePath: () => '',
440
437
  logger: {},
441
438
  readFileSync: () => '{}'
442
439
  })
@@ -465,7 +462,6 @@ describe('createNunjucksContextBuilder() hasPermission()', () => {
465
462
  // Arrange
466
463
  const contextBuilder = createNunjucksContextBuilder({
467
464
  config: createConfig(),
468
- getRequestBasePath: () => '',
469
465
  logger: {},
470
466
  readFileSync: () => '{}'
471
467
  })
@@ -494,7 +490,6 @@ describe('createNunjucksContextBuilder() hasPermission()', () => {
494
490
  // Arrange
495
491
  const contextBuilder = createNunjucksContextBuilder({
496
492
  config: createConfig(),
497
- getRequestBasePath: () => '',
498
493
  logger: {},
499
494
  readFileSync: () => '{}'
500
495
  })
@@ -523,7 +518,6 @@ describe('createNunjucksContextBuilder() hasPermission()', () => {
523
518
  // Arrange
524
519
  const contextBuilder = createNunjucksContextBuilder({
525
520
  config: createConfig(),
526
- getRequestBasePath: () => '',
527
521
  logger: {},
528
522
  readFileSync: () => '{}'
529
523
  })
@@ -543,7 +537,6 @@ describe('createNunjucksContextBuilder() hasPermission()', () => {
543
537
  // Arrange
544
538
  const contextBuilder = createNunjucksContextBuilder({
545
539
  config: createConfig(),
546
- getRequestBasePath: () => '',
547
540
  logger: {},
548
541
  readFileSync: () => '{}'
549
542
  })
@@ -571,7 +564,6 @@ describe('createNunjucksContextBuilder() hasPermission()', () => {
571
564
  // Arrange
572
565
  const contextBuilder = createNunjucksContextBuilder({
573
566
  config: createConfig(),
574
- getRequestBasePath: () => '',
575
567
  logger: {},
576
568
  readFileSync: () => '{}'
577
569
  })
@@ -597,7 +589,6 @@ describe('createNunjucksContextBuilder() hasPermission()', () => {
597
589
  // Arrange
598
590
  const contextBuilder = createNunjucksContextBuilder({
599
591
  config: createConfig(),
600
- getRequestBasePath: () => '',
601
592
  logger: {},
602
593
  readFileSync: () => '{}'
603
594
  })
@@ -99,16 +99,10 @@ function buildNunjucksEnvironment({
99
99
 
100
100
  /**
101
101
  * @import { ErrorPageLinks } from './context.js'
102
- * @param {{ config: object, logger: object, getRequestBasePath: Function, moduleId?: string, errorPages?: ErrorPageLinks }} options
102
+ * @param {{ config: object, logger: object, moduleId?: string, errorPages?: ErrorPageLinks }} options
103
103
  * @returns {object}
104
104
  */
105
- export function createNunjucksConfig({
106
- config,
107
- logger,
108
- getRequestBasePath,
109
- moduleId,
110
- errorPages
111
- }) {
105
+ export function createNunjucksConfig({ config, logger, moduleId, errorPages }) {
112
106
  const projectRoot = config.get('root')
113
107
  const infrastructureRoot = path.resolve(
114
108
  path.dirname(fileURLToPath(import.meta.url)),
@@ -124,7 +118,6 @@ export function createNunjucksConfig({
124
118
 
125
119
  const context = createNunjucksContextBuilder({
126
120
  config,
127
- getRequestBasePath,
128
121
  logger,
129
122
  readFileSync,
130
123
  moduleId,
@@ -46,7 +46,7 @@ describe('createNunjucksConfig()', () => {
46
46
  rmSync(projectRoot, { recursive: true, force: true })
47
47
  })
48
48
 
49
- test('defaults to an empty base path when no moduleId is given', () => {
49
+ test('builds a config for a hub when no moduleId is given', () => {
50
50
  // Arrange
51
51
  const config = {
52
52
  get: vi.fn((key) => (key === 'root' ? projectRoot : false))
@@ -55,8 +55,7 @@ describe('createNunjucksConfig()', () => {
55
55
  // Act
56
56
  const nunjucksConfig = createNunjucksConfig({
57
57
  config,
58
- logger: {},
59
- getRequestBasePath: vi.fn()
58
+ logger: {}
60
59
  })
61
60
 
62
61
  // Assert
@@ -77,8 +76,7 @@ describe('createNunjucksConfig()', () => {
77
76
  // Act
78
77
  createNunjucksConfig({
79
78
  config,
80
- logger: {},
81
- getRequestBasePath: vi.fn()
79
+ logger: {}
82
80
  })
83
81
 
84
82
  // Assert
@@ -101,7 +99,6 @@ describe('createNunjucksConfig()', () => {
101
99
  const nunjucksConfig = createNunjucksConfig({
102
100
  config,
103
101
  logger: {},
104
- getRequestBasePath: vi.fn(() => ''),
105
102
  errorPages
106
103
  })
107
104
  const context = nunjucksConfig.options.context({ headers: {}, path: '/' })
package/src/base-path.js DELETED
@@ -1,148 +0,0 @@
1
- /** @import { Request } from '@hapi/hapi' */
2
-
3
- import { getBasePathForModule } from '@defra/lis-hubs-infra-registry'
4
-
5
- function normalizePath(path) {
6
- if (!path || path === '/') {
7
- return '/'
8
- }
9
-
10
- return path.startsWith('/') ? path : `/${path}`
11
- }
12
-
13
- function normalizeBasePath(path) {
14
- if (!path) {
15
- return ''
16
- }
17
-
18
- const normalizedPath = normalizePath(path)
19
-
20
- return normalizedPath === '/' ? '' : normalizedPath
21
- }
22
-
23
- function getForwardedPrefix(request) {
24
- const forwardedPrefix = request?.headers?.['x-forwarded-prefix']
25
-
26
- if (typeof forwardedPrefix !== 'string') {
27
- return ''
28
- }
29
-
30
- return normalizeBasePath(forwardedPrefix.trim())
31
- }
32
-
33
- /**
34
- * @param {{ request: Request, basePath: string }} options
35
- * @returns {boolean}
36
- */
37
- export function isPrefixedRequest({ request, basePath }) {
38
- const normalizedBasePath = normalizeBasePath(basePath)
39
-
40
- if (!normalizedBasePath || !request) {
41
- return false
42
- }
43
-
44
- const forwardedPrefix = getForwardedPrefix(request)
45
-
46
- if (forwardedPrefix) {
47
- return forwardedPrefix === normalizedBasePath
48
- }
49
-
50
- return (
51
- request.path === normalizedBasePath ||
52
- request.path.startsWith(`${normalizedBasePath}/`)
53
- )
54
- }
55
-
56
- /**
57
- * @param {{ request: Request, basePath: string }} options
58
- * @returns {string}
59
- */
60
- export function getRequestBasePath({ request, basePath }) {
61
- const normalizedBasePath = normalizeBasePath(basePath)
62
-
63
- return isPrefixedRequest({ request, basePath: normalizedBasePath })
64
- ? normalizedBasePath
65
- : ''
66
- }
67
-
68
- /**
69
- * @param {{ request: Request, routePath?: string, basePath?: string }} options
70
- * @returns {string}
71
- */
72
- export function buildAppPath({ request, routePath = '/', basePath = '' }) {
73
- const requestBasePath = getRequestBasePath({ request, basePath })
74
- const normalizedPath = normalizePath(routePath)
75
-
76
- if (normalizedPath === '/') {
77
- return requestBasePath || '/'
78
- }
79
-
80
- return `${requestBasePath}${normalizedPath}`
81
- }
82
-
83
- /**
84
- * @param {{ routePath?: string, basePath?: string }} options
85
- * @returns {string[]}
86
- */
87
- export function getRouteVariants({
88
- routePath = '/',
89
- basePath: _basePath = ''
90
- }) {
91
- const normalizedPath = normalizePath(routePath)
92
- return [normalizedPath]
93
- }
94
-
95
- /**
96
- * @param {{ basePath?: string, assetPath: string }} options
97
- * @returns {string[]}
98
- */
99
- export function getAssetPaths({ basePath: _basePath = '', assetPath }) {
100
- return [assetPath]
101
- }
102
-
103
- /**
104
- * @param {{ moduleId?: string, assetPath: string }} options
105
- * @returns {object}
106
- */
107
- export function createBasePathHelpersForConfig({ moduleId, assetPath }) {
108
- const basePath = moduleId ? getBasePathForModule(moduleId) : ''
109
-
110
- function getBasePath() {
111
- return basePath
112
- }
113
-
114
- return {
115
- getBasePath,
116
- isPrefixedRequest(request) {
117
- return isPrefixedRequest({
118
- request,
119
- basePath: getBasePath()
120
- })
121
- },
122
- getRequestBasePath(request) {
123
- return getRequestBasePath({
124
- request,
125
- basePath: getBasePath()
126
- })
127
- },
128
- buildAppPath(request, routePath = '/') {
129
- return buildAppPath({
130
- request,
131
- routePath,
132
- basePath: getBasePath()
133
- })
134
- },
135
- getRouteVariants(routePath = '/') {
136
- return getRouteVariants({
137
- routePath,
138
- basePath: getBasePath()
139
- })
140
- },
141
- getAssetPaths() {
142
- return getAssetPaths({
143
- basePath: getBasePath(),
144
- assetPath
145
- })
146
- }
147
- }
148
- }
@@ -1,294 +0,0 @@
1
- import { beforeEach, describe, expect, test, vi } from 'vitest'
2
-
3
- import { getBasePathForModule } from '@defra/lis-hubs-infra-registry'
4
- import {
5
- buildAppPath,
6
- createBasePathHelpersForConfig,
7
- getAssetPaths,
8
- getRequestBasePath,
9
- getRouteVariants,
10
- isPrefixedRequest
11
- } from './base-path.js'
12
-
13
- vi.mock('@defra/lis-hubs-infra-registry')
14
-
15
- const mocks = {
16
- getBasePathForModule: vi.mocked(getBasePathForModule)
17
- }
18
-
19
- describe('isPrefixedRequest()', () => {
20
- beforeEach(() => {
21
- vi.clearAllMocks()
22
- })
23
-
24
- test('detects a forwarded prefix that matches the configured base path', () => {
25
- // Arrange
26
- const request = {
27
- path: '/about',
28
- headers: {
29
- 'x-forwarded-prefix': '/chicken/move'
30
- }
31
- }
32
-
33
- // Act
34
- const result = isPrefixedRequest({ request, basePath: '/chicken/move' })
35
-
36
- // Assert
37
- expect(result).toBe(true)
38
- })
39
-
40
- test('ignores a forwarded prefix that does not match the configured base path', () => {
41
- // Arrange
42
- const request = {
43
- path: '/about',
44
- headers: {
45
- 'x-forwarded-prefix': '/goat/move'
46
- }
47
- }
48
-
49
- // Act
50
- const result = isPrefixedRequest({ request, basePath: '/chicken/move' })
51
-
52
- // Assert
53
- expect(result).toBe(false)
54
- })
55
- })
56
-
57
- describe('getRequestBasePath()', () => {
58
- beforeEach(() => {
59
- vi.clearAllMocks()
60
- })
61
-
62
- test('preserves direct requests to the configured base path', () => {
63
- // Arrange
64
- const request = {
65
- path: '/chicken/move/about',
66
- headers: {}
67
- }
68
-
69
- // Act
70
- const result = getRequestBasePath({ request, basePath: '/chicken/move' })
71
-
72
- // Assert
73
- expect(result).toBe('/chicken/move')
74
- })
75
- })
76
-
77
- describe('buildAppPath()', () => {
78
- beforeEach(() => {
79
- vi.clearAllMocks()
80
- })
81
-
82
- test('uses the forwarded prefix for outbound paths when present', () => {
83
- // Arrange
84
- const request = {
85
- path: '/about',
86
- headers: {
87
- 'x-forwarded-prefix': '/chicken/move'
88
- }
89
- }
90
-
91
- // Act
92
- const result = buildAppPath({
93
- request,
94
- routePath: '/more-info',
95
- basePath: '/chicken/move'
96
- })
97
-
98
- // Assert
99
- expect(result).toBe('/chicken/move/more-info')
100
- })
101
-
102
- test('returns just the base path for the root route', () => {
103
- // Arrange
104
- const request = {
105
- path: '/chicken/move',
106
- headers: {}
107
- }
108
-
109
- // Act
110
- const result = buildAppPath({
111
- request,
112
- routePath: '/',
113
- basePath: '/chicken/move'
114
- })
115
-
116
- // Assert
117
- expect(result).toBe('/chicken/move')
118
- })
119
-
120
- test('falls back to / for the root route with no base path', () => {
121
- // Arrange
122
- const request = {
123
- path: '/',
124
- headers: {}
125
- }
126
-
127
- // Act
128
- const result = buildAppPath({ request, routePath: '/' })
129
-
130
- // Assert
131
- expect(result).toBe('/')
132
- })
133
- })
134
-
135
- describe('getRouteVariants()', () => {
136
- test('only returns the internal route path', () => {
137
- // Arrange
138
- const routePath = '/about'
139
-
140
- // Act
141
- const result = getRouteVariants({ routePath, basePath: '/chicken/move' })
142
-
143
- // Assert
144
- expect(result).toEqual(['/about'])
145
- })
146
- })
147
-
148
- describe('getAssetPaths()', () => {
149
- test('only returns the internal asset path', () => {
150
- // Arrange
151
- const assetPath = '/public'
152
-
153
- // Act
154
- const result = getAssetPaths({ basePath: '/chicken/move', assetPath })
155
-
156
- // Assert
157
- expect(result).toEqual(['/public'])
158
- })
159
- })
160
-
161
- describe('createBasePathHelpersForConfig()', () => {
162
- beforeEach(() => {
163
- vi.clearAllMocks()
164
- })
165
-
166
- test('resolves the base path from the module registry when moduleId is given', () => {
167
- // Arrange
168
- mocks.getBasePathForModule.mockReturnValue('/cattle/register')
169
-
170
- // Act
171
- const helpers = createBasePathHelpersForConfig({
172
- moduleId: 'cattle-register',
173
- assetPath: '/public'
174
- })
175
-
176
- // Assert
177
- expect(mocks.getBasePathForModule).toHaveBeenCalledWith('cattle-register')
178
- expect(helpers.getBasePath()).toBe('/cattle/register')
179
- })
180
-
181
- test('defaults to an empty base path when no moduleId is given', () => {
182
- // Arrange
183
- // Act
184
- const helpers = createBasePathHelpersForConfig({ assetPath: '/public' })
185
-
186
- // Assert
187
- expect(helpers.getBasePath()).toBe('')
188
- expect(mocks.getBasePathForModule).not.toHaveBeenCalled()
189
- })
190
-
191
- test('binds the resolved base path to isPrefixedRequest', () => {
192
- // Arrange
193
- mocks.getBasePathForModule.mockReturnValue('/cattle/register')
194
- const helpers = createBasePathHelpersForConfig({
195
- moduleId: 'cattle-register',
196
- assetPath: '/public'
197
- })
198
- const request = {
199
- path: '/cattle/register/about',
200
- headers: {}
201
- }
202
-
203
- // Act
204
- const result = helpers.isPrefixedRequest(request)
205
-
206
- // Assert
207
- expect(result).toBe(true)
208
- })
209
-
210
- test('binds the resolved base path to getRequestBasePath', () => {
211
- // Arrange
212
- mocks.getBasePathForModule.mockReturnValue('/cattle/register')
213
- const helpers = createBasePathHelpersForConfig({
214
- moduleId: 'cattle-register',
215
- assetPath: '/public'
216
- })
217
- const request = {
218
- path: '/cattle/register/about',
219
- headers: {}
220
- }
221
-
222
- // Act
223
- const result = helpers.getRequestBasePath(request)
224
-
225
- // Assert
226
- expect(result).toBe('/cattle/register')
227
- })
228
-
229
- test('binds the resolved base path to buildAppPath', () => {
230
- // Arrange
231
- mocks.getBasePathForModule.mockReturnValue('/cattle/register')
232
- const helpers = createBasePathHelpersForConfig({
233
- moduleId: 'cattle-register',
234
- assetPath: '/public'
235
- })
236
- const request = {
237
- path: '/cattle/register/about',
238
- headers: {}
239
- }
240
-
241
- // Act
242
- const result = helpers.buildAppPath(request, '/more-info')
243
-
244
- // Assert
245
- expect(result).toBe('/cattle/register/more-info')
246
- })
247
-
248
- test('binds the resolved base path to getRouteVariants', () => {
249
- // Arrange
250
- mocks.getBasePathForModule.mockReturnValue('/cattle/register')
251
- const helpers = createBasePathHelpersForConfig({
252
- moduleId: 'cattle-register',
253
- assetPath: '/public'
254
- })
255
-
256
- // Act
257
- const result = helpers.getRouteVariants('/about')
258
-
259
- // Assert
260
- expect(result).toEqual(['/about'])
261
- })
262
-
263
- test('defaults buildAppPath and getRouteVariants to the root route path', () => {
264
- // Arrange
265
- mocks.getBasePathForModule.mockReturnValue('/cattle/register')
266
- const helpers = createBasePathHelpersForConfig({
267
- moduleId: 'cattle-register',
268
- assetPath: '/public'
269
- })
270
- const request = {
271
- path: '/cattle/register',
272
- headers: {}
273
- }
274
-
275
- // Act
276
- const appPath = helpers.buildAppPath(request)
277
- const routeVariants = helpers.getRouteVariants()
278
-
279
- // Assert
280
- expect(appPath).toBe('/cattle/register')
281
- expect(routeVariants).toEqual(['/'])
282
- })
283
-
284
- test('binds the configured asset path to getAssetPaths', () => {
285
- // Arrange
286
- const helpers = createBasePathHelpersForConfig({ assetPath: '/public' })
287
-
288
- // Act
289
- const result = helpers.getAssetPaths()
290
-
291
- // Assert
292
- expect(result).toEqual(['/public'])
293
- })
294
- })