inertia-sails 1.3.2 → 1.4.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.
Files changed (41) hide show
  1. package/README.md +55 -2
  2. package/index.js +133 -81
  3. package/lib/handle-bad-request.js +20 -6
  4. package/lib/helpers/build-page-object.js +117 -12
  5. package/lib/helpers/inertia-headers.js +4 -1
  6. package/lib/helpers/is-inertia-partial-request.js +10 -0
  7. package/lib/helpers/is-inertia-request.js +9 -0
  8. package/lib/helpers/request-context.js +49 -8
  9. package/lib/helpers/resolve-asset-version.js +12 -0
  10. package/lib/helpers/resolve-validation-errors.js +12 -5
  11. package/lib/location.js +11 -0
  12. package/lib/middleware/inertia-middleware.js +7 -2
  13. package/lib/props/always-prop.js +6 -2
  14. package/lib/props/defer-prop.js +46 -6
  15. package/lib/props/get-partial-data.js +9 -0
  16. package/lib/props/merge-prop.js +7 -4
  17. package/lib/props/merge-targets.js +114 -0
  18. package/lib/props/mergeable-prop.js +87 -0
  19. package/lib/props/once-prop.js +9 -1
  20. package/lib/props/optional-prop.js +12 -4
  21. package/lib/props/pick-props-to-resolve.js +14 -1
  22. package/lib/props/resolve-deferred-props.js +17 -1
  23. package/lib/props/resolve-except-props.js +11 -0
  24. package/lib/props/resolve-merge-props.js +81 -20
  25. package/lib/props/resolve-once-props.js +17 -7
  26. package/lib/props/resolve-only-props.js +10 -3
  27. package/lib/props/resolve-page-props.js +72 -13
  28. package/lib/props/resolve-scroll-props.js +17 -3
  29. package/lib/props/scroll-prop.js +30 -8
  30. package/lib/render.js +68 -9
  31. package/lib/responses/server-error.js +23 -6
  32. package/lib/types.js +68 -0
  33. package/package.json +3 -2
  34. package/test.js +53 -12
  35. package/tests/helpers/build-page-object.test.js +183 -0
  36. package/tests/helpers/preserve-fragment.test.js +97 -0
  37. package/tests/props/merge-targets.test.js +74 -0
  38. package/tests/props/resolve-merge-props.test.js +151 -0
  39. package/tests/props/resolve-page-props.test.js +70 -0
  40. package/tests/props/resolve-scroll-props.test.js +51 -0
  41. package/tests/render.test.js +197 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Utilities for describing how mergeable props should be merged by the client.
3
+ *
4
+ * @typedef {import('../types').MergeOperation} MergeOperation
5
+ * @typedef {import('../types').MergeOptions} MergeOptions
6
+ * @typedef {string|string[]|Record<string, string|null>|null} MergeTargetInput
7
+ */
8
+
9
+ /**
10
+ * @returns {MergeOperation}
11
+ */
12
+ function createDefaultMergeOperation() {
13
+ return {
14
+ direction: 'append',
15
+ path: null,
16
+ matchOn: null,
17
+ isDefault: true
18
+ }
19
+ }
20
+
21
+ /**
22
+ * @param {MergeOptions|string|null|undefined} options
23
+ * @returns {MergeOptions}
24
+ */
25
+ function normalizeMergeOptions(options) {
26
+ return typeof options === 'string' ? { matchOn: options } : options || {}
27
+ }
28
+
29
+ /**
30
+ * @param {MergeTargetInput} paths
31
+ * @param {MergeOptions|string} [options]
32
+ * @returns {Array<{ path: string|null, matchOn: string|null }>}
33
+ */
34
+ function normalizeMergeTargets(paths, options = {}) {
35
+ const normalizedOptions = normalizeMergeOptions(options)
36
+
37
+ if (paths === null || paths === undefined) {
38
+ return [
39
+ {
40
+ path: null,
41
+ matchOn:
42
+ typeof normalizedOptions.matchOn === 'string'
43
+ ? normalizedOptions.matchOn
44
+ : null
45
+ }
46
+ ]
47
+ }
48
+
49
+ if (Array.isArray(paths)) {
50
+ return paths.map((path) => ({
51
+ path: normalizePath(path),
52
+ matchOn: resolveTargetMatchOn(path, normalizedOptions)
53
+ }))
54
+ }
55
+
56
+ if (typeof paths === 'object') {
57
+ return Object.entries(paths).map(([path, matchOn]) => ({
58
+ path: normalizePath(path),
59
+ matchOn: matchOn || null
60
+ }))
61
+ }
62
+
63
+ return [
64
+ {
65
+ path: normalizePath(paths),
66
+ matchOn: resolveTargetMatchOn(paths, normalizedOptions)
67
+ }
68
+ ]
69
+ }
70
+
71
+ /**
72
+ * @param {string} path
73
+ * @returns {string|null}
74
+ */
75
+ function normalizePath(path) {
76
+ return path === '' ? null : path
77
+ }
78
+
79
+ /**
80
+ * @param {string} path
81
+ * @param {MergeOptions} options
82
+ * @returns {string|null}
83
+ */
84
+ function resolveTargetMatchOn(path, options) {
85
+ if (!options.matchOn) return null
86
+ if (typeof options.matchOn === 'object') return options.matchOn[path] || null
87
+ return options.matchOn
88
+ }
89
+
90
+ /**
91
+ * @param {string} key
92
+ * @param {string|null} path
93
+ * @returns {string}
94
+ */
95
+ function resolvePropPath(key, path) {
96
+ return path ? `${key}.${path}` : key
97
+ }
98
+
99
+ /**
100
+ * @template T
101
+ * @param {T[]} values
102
+ * @returns {T[]}
103
+ */
104
+ function unique(values) {
105
+ return [...new Set(values)]
106
+ }
107
+
108
+ module.exports = {
109
+ createDefaultMergeOperation,
110
+ normalizeMergeOptions,
111
+ normalizeMergeTargets,
112
+ resolvePropPath,
113
+ unique
114
+ }
@@ -1,3 +1,15 @@
1
+ const {
2
+ createDefaultMergeOperation,
3
+ normalizeMergeOptions,
4
+ normalizeMergeTargets
5
+ } = require('./merge-targets')
6
+
7
+ /**
8
+ * @typedef {import('../types').MergeOperation} MergeOperation
9
+ * @typedef {import('../types').MergeOptions} MergeOptions
10
+ * @typedef {string|string[]|Record<string, string|null>|null} MergeTargetInput
11
+ */
12
+
1
13
  /**
2
14
  * MergeableProp - Base class for props that can be merged during partial reloads.
3
15
  *
@@ -12,6 +24,10 @@ module.exports = class MergeableProp {
12
24
  this.shouldMerge = false
13
25
  /** @type {boolean} - Whether to deep merge this prop */
14
26
  this.shouldDeepMerge = false
27
+ /** @type {MergeOperation[]} */
28
+ this.mergeOperations = []
29
+ /** @type {string[]} - Paths to use when matching merge items */
30
+ this.matchOnPaths = []
15
31
  }
16
32
 
17
33
  /**
@@ -21,9 +37,33 @@ module.exports = class MergeableProp {
21
37
  */
22
38
  merge() {
23
39
  this.shouldMerge = true
40
+ this.shouldDeepMerge = false
41
+ if (this.mergeOperations.length === 0) {
42
+ this.mergeOperations.push(createDefaultMergeOperation())
43
+ }
24
44
  return this
25
45
  }
26
46
 
47
+ /**
48
+ * Append this prop, or one or more nested paths, during partial reloads.
49
+ * @param {MergeTargetInput} [paths] - Path(s) to append, or a path-to-matchOn map
50
+ * @param {MergeOptions|string} [options] - Options, or a matchOn string
51
+ * @returns {MergeableProp} - Returns this for chaining
52
+ */
53
+ append(paths = null, options = {}) {
54
+ return this._addMergeOperations('append', paths, options)
55
+ }
56
+
57
+ /**
58
+ * Prepend this prop, or one or more nested paths, during partial reloads.
59
+ * @param {MergeTargetInput} [paths] - Path(s) to prepend, or a path-to-matchOn map
60
+ * @param {MergeOptions|string} [options] - Options, or a matchOn string
61
+ * @returns {MergeableProp} - Returns this for chaining
62
+ */
63
+ prepend(paths = null, options = {}) {
64
+ return this._addMergeOperations('prepend', paths, options)
65
+ }
66
+
27
67
  /**
28
68
  * Enable deep merging for this prop.
29
69
  * Recursively merges nested objects instead of replacing them.
@@ -32,6 +72,53 @@ module.exports = class MergeableProp {
32
72
  deepMerge() {
33
73
  this.shouldMerge = true
34
74
  this.shouldDeepMerge = true
75
+ this.mergeOperations = []
35
76
  return this
36
77
  }
78
+
79
+ /**
80
+ * Configure one or more match-on paths for merge operations.
81
+ * @param {string|string[]} paths - Path(s) ending with the field to match on
82
+ * @returns {MergeableProp} - Returns this for chaining
83
+ */
84
+ matchOn(paths) {
85
+ const normalizedPaths = Array.isArray(paths) ? paths : [paths]
86
+ normalizedPaths.filter(Boolean).forEach((path) => {
87
+ this.matchOnPaths.push(path)
88
+ })
89
+ return this
90
+ }
91
+
92
+ /**
93
+ * @param {'append'|'prepend'} direction
94
+ * @param {MergeTargetInput} paths
95
+ * @param {MergeOptions|string} options
96
+ * @returns {MergeableProp}
97
+ */
98
+ _addMergeOperations(direction, paths, options) {
99
+ const normalizedOptions = normalizeMergeOptions(options)
100
+
101
+ this.shouldMerge = true
102
+ this.shouldDeepMerge = false
103
+ this._clearDefaultMergeOperation()
104
+
105
+ normalizeMergeTargets(paths, normalizedOptions).forEach((target) => {
106
+ this.mergeOperations.push({
107
+ direction,
108
+ path: target.path,
109
+ matchOn: target.matchOn
110
+ })
111
+ })
112
+
113
+ return this
114
+ }
115
+
116
+ _clearDefaultMergeOperation() {
117
+ if (
118
+ this.mergeOperations.length === 1 &&
119
+ this.mergeOperations[0].isDefault
120
+ ) {
121
+ this.mergeOperations = []
122
+ }
123
+ }
37
124
  }
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @typedef {import('../types').PropCallback} PropCallback
3
+ */
4
+
1
5
  /**
2
6
  * OnceProp - A prop that is resolved only once and cached across navigations.
3
7
  *
@@ -33,12 +37,16 @@
33
37
  module.exports = class OnceProp {
34
38
  /**
35
39
  * Create a new OnceProp instance
36
- * @param {Function} callback - The callback function to resolve the prop value
40
+ * @param {PropCallback} callback - The callback function to resolve the prop value
37
41
  */
38
42
  constructor(callback) {
43
+ /** @type {PropCallback} */
39
44
  this.callback = callback
45
+ /** @type {string|null} */
40
46
  this._key = null
47
+ /** @type {number|null} */
41
48
  this._ttl = null
49
+ /** @type {boolean} */
42
50
  this._refresh = false
43
51
  }
44
52
 
@@ -1,5 +1,9 @@
1
1
  const ignoreFirstLoadSymbol = require('../helpers/ignore-first-load-symbol')
2
2
 
3
+ /**
4
+ * @typedef {import('../types').PropCallback} PropCallback
5
+ */
6
+
3
7
  /**
4
8
  * OptionalProp - A prop that is only evaluated when explicitly requested.
5
9
  *
@@ -20,12 +24,16 @@ const ignoreFirstLoadSymbol = require('../helpers/ignore-first-load-symbol')
20
24
  module.exports = class OptionalProp {
21
25
  /**
22
26
  * Create a new OptionalProp instance
23
- * @param {Function} callback - The callback function to resolve the prop value
27
+ * @param {PropCallback} callback - The callback function to resolve the prop value
24
28
  */
25
29
  constructor(callback) {
26
- /** @type {Function} */
30
+ /** @type {PropCallback} */
27
31
  this.callback = callback
28
- /** @type {boolean} */
29
- this[ignoreFirstLoadSymbol] = true
32
+ Object.defineProperty(this, ignoreFirstLoadSymbol, {
33
+ value: true,
34
+ enumerable: true,
35
+ configurable: true,
36
+ writable: true
37
+ })
30
38
  }
31
39
  }
@@ -7,6 +7,17 @@ const resolveExceptProps = require('./resolve-except-props')
7
7
  const { filterOnceProps } = require('./resolve-once-props')
8
8
  const AlwaysProp = require('./always-prop')
9
9
 
10
+ /**
11
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
12
+ * @typedef {import('../types').InertiaProps} InertiaProps
13
+ */
14
+
15
+ /**
16
+ * @param {InertiaRequest} req
17
+ * @param {string} component
18
+ * @param {InertiaProps} [props]
19
+ * @returns {InertiaProps}
20
+ */
10
21
  module.exports = function pickPropsToResolve(req, component, props = {}) {
11
22
  const isPartial = isInertiaPartialRequest(req, component)
12
23
  const isInertia = isInertiaRequest(req)
@@ -15,7 +26,9 @@ module.exports = function pickPropsToResolve(req, component, props = {}) {
15
26
  if (!isPartial) {
16
27
  newProps = Object.fromEntries(
17
28
  Object.entries(props).filter(([_, value]) => {
18
- if (value && value[ignoreFirstLoadSymbol]) return false
29
+ if (value && /** @type {any} */ (value)[ignoreFirstLoadSymbol]) {
30
+ return false
31
+ }
19
32
  return true
20
33
  })
21
34
  )
@@ -1,7 +1,23 @@
1
1
  const isInertiaPartialRequest = require('../helpers/is-inertia-partial-request')
2
2
  const DeferProp = require('./defer-prop')
3
+
4
+ /**
5
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
6
+ * @typedef {import('../types').InertiaProps} InertiaProps
7
+ *
8
+ * @typedef {Object} DeferredPropsMetadata
9
+ * @property {Record<string, string[]>} [deferredProps]
10
+ */
11
+
12
+ /**
13
+ * @param {InertiaRequest} req
14
+ * @param {string} component
15
+ * @param {InertiaProps} pageProps
16
+ * @returns {DeferredPropsMetadata}
17
+ */
3
18
  module.exports = function resolveDeferredProps(req, component, pageProps) {
4
19
  if (isInertiaPartialRequest(req, component)) return {}
20
+ /** @type {Record<string, string[]>} */
5
21
  const deferredProps = Object.entries(pageProps || {})
6
22
  .filter(([_, value]) => value instanceof DeferProp)
7
23
  .map(([key, value]) => ({ key, group: value.getGroup() }))
@@ -9,7 +25,7 @@ module.exports = function resolveDeferredProps(req, component, pageProps) {
9
25
  if (!groups[group]) groups[group] = []
10
26
  groups[group].push(key)
11
27
  return groups
12
- }, {})
28
+ }, /** @type {Record<string, string[]>} */ ({}))
13
29
 
14
30
  return Object.keys(deferredProps).length ? { deferredProps } : {}
15
31
  }
@@ -1,4 +1,15 @@
1
1
  const { PARTIAL_EXCEPT } = require('../helpers/inertia-headers')
2
+
3
+ /**
4
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
5
+ * @typedef {import('../types').InertiaProps} InertiaProps
6
+ */
7
+
8
+ /**
9
+ * @param {InertiaRequest} req
10
+ * @param {InertiaProps} props
11
+ * @returns {InertiaProps}
12
+ */
2
13
  module.exports = function resolveExceptProps(req, props) {
3
14
  const partialExceptHeader = req.get(PARTIAL_EXCEPT)
4
15
  const except = partialExceptHeader.split(',').filter(Boolean)
@@ -1,37 +1,98 @@
1
- const { RESET } = require('../helpers/inertia-headers')
1
+ const {
2
+ INFINITE_SCROLL_MERGE_INTENT,
3
+ RESET
4
+ } = require('../helpers/inertia-headers')
2
5
  const MergeableProp = require('./mergeable-prop')
6
+ const { resolvePropPath, unique } = require('./merge-targets')
7
+ const ScrollProp = require('./scroll-prop')
8
+
9
+ /**
10
+ * @typedef {import('../types').InertiaProps} InertiaProps
11
+ * @typedef {{ get: (header: string) => any }} HeaderRequest
12
+ *
13
+ * @typedef {Object} MergePropsMetadata
14
+ * @property {string[]} [mergeProps]
15
+ * @property {string[]} [prependProps]
16
+ * @property {string[]} [deepMergeProps]
17
+ * @property {string[]} [matchPropsOn]
18
+ */
3
19
 
4
20
  /**
5
21
  * Resolve merge props metadata for the page response.
6
22
  * Returns mergeProps and deepMergeProps arrays for the client.
7
- * @param {Object} req - The request object
8
- * @param {Object} pageProps - The page props
9
- * @returns {Object} - Object with mergeProps and/or deepMergeProps arrays
23
+ * @param {HeaderRequest} req - The request object
24
+ * @param {InertiaProps} pageProps - The page props
25
+ * @returns {MergePropsMetadata} - Object with mergeProps and/or deepMergeProps arrays
10
26
  */
11
27
  module.exports = function resolveMergeProps(req, pageProps) {
12
28
  const inertiaResetHeader = req.get(RESET)
13
29
  const resetProps = new Set(inertiaResetHeader?.split(',').filter(Boolean))
30
+ const infiniteScrollMergeIntent = req.get(INFINITE_SCROLL_MERGE_INTENT)
31
+
32
+ /** @type {string[]} */
33
+ const mergeProps = []
34
+ /** @type {string[]} */
35
+ const prependProps = []
36
+ /** @type {string[]} */
37
+ const deepMergeProps = []
38
+ /** @type {string[]} */
39
+ const matchPropsOn = []
40
+
41
+ Object.entries(pageProps || {}).forEach(([key, value]) => {
42
+ if (!(value instanceof MergeableProp) || !value.shouldMerge) return
43
+ if (resetProps.has(key)) return
44
+
45
+ if (value instanceof ScrollProp) {
46
+ const propPath = resolvePropPath(key, value.wrapper)
47
+ if (resetProps.has(propPath)) return
48
+
49
+ if (infiniteScrollMergeIntent === 'prepend') {
50
+ prependProps.push(propPath)
51
+ } else {
52
+ mergeProps.push(propPath)
53
+ }
54
+
55
+ if (value.matchOnPath) {
56
+ matchPropsOn.push(resolvePropPath(propPath, value.matchOnPath))
57
+ }
58
+
59
+ return
60
+ }
61
+
62
+ if (value.shouldDeepMerge) {
63
+ deepMergeProps.push(key)
64
+ value.matchOnPaths.forEach((path) => {
65
+ matchPropsOn.push(resolvePropPath(key, path))
66
+ })
67
+ return
68
+ }
69
+
70
+ value.mergeOperations.forEach((operation) => {
71
+ const propPath = resolvePropPath(key, operation.path)
72
+ if (resetProps.has(propPath)) return
14
73
 
15
- const mergeableEntries = Object.entries(pageProps || {}).filter(
16
- ([key, value]) =>
17
- value instanceof MergeableProp &&
18
- value.shouldMerge &&
19
- !resetProps.has(key)
20
- )
74
+ if (operation.direction === 'prepend') {
75
+ prependProps.push(propPath)
76
+ } else {
77
+ mergeProps.push(propPath)
78
+ }
21
79
 
22
- // Props that should be shallow merged (appended)
23
- const mergeProps = mergeableEntries
24
- .filter(([_, value]) => !value.shouldDeepMerge)
25
- .map(([key]) => key)
80
+ if (operation.matchOn) {
81
+ matchPropsOn.push(resolvePropPath(propPath, operation.matchOn))
82
+ }
83
+ })
26
84
 
27
- // Props that should be deep merged
28
- const deepMergeProps = mergeableEntries
29
- .filter(([_, value]) => value.shouldDeepMerge)
30
- .map(([key]) => key)
85
+ value.matchOnPaths.forEach((path) => {
86
+ matchPropsOn.push(resolvePropPath(key, path))
87
+ })
88
+ })
31
89
 
90
+ /** @type {MergePropsMetadata} */
32
91
  const result = {}
33
- if (mergeProps.length) result.mergeProps = mergeProps
34
- if (deepMergeProps.length) result.deepMergeProps = deepMergeProps
92
+ if (mergeProps.length) result.mergeProps = unique(mergeProps)
93
+ if (prependProps.length) result.prependProps = unique(prependProps)
94
+ if (deepMergeProps.length) result.deepMergeProps = unique(deepMergeProps)
95
+ if (matchPropsOn.length) result.matchPropsOn = unique(matchPropsOn)
35
96
 
36
97
  return result
37
98
  }
@@ -2,14 +2,22 @@ const OnceProp = require('./once-prop')
2
2
  const { EXCEPT_ONCE_PROPS } = require('../helpers/inertia-headers')
3
3
  const requestContext = require('../helpers/request-context')
4
4
 
5
+ /**
6
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
7
+ * @typedef {import('../types').InertiaProps} InertiaProps
8
+ *
9
+ * @typedef {Object} OncePropsMetadata
10
+ * @property {Record<string, { prop: string, expiresAt: number|null }>} [onceProps]
11
+ */
12
+
5
13
  /**
6
14
  * Get the list of once-props that the client already has cached.
7
15
  * These are sent via the X-Inertia-Except-Once-Props header.
8
- * @param {Object} req - The request object
16
+ * @param {InertiaRequest} req - The request object
9
17
  * @returns {string[]} - Array of prop keys the client already has
10
18
  */
11
19
  function getExceptOnceProps(req) {
12
- const header = req.get(EXCEPT_ONCE_PROPS) || ''
20
+ const header = String(req.get(EXCEPT_ONCE_PROPS) || '')
13
21
  return header
14
22
  ? header
15
23
  .split(',')
@@ -28,9 +36,9 @@ function getExceptOnceProps(req) {
28
36
  * - They are marked for refresh via sails.inertia.refreshOnce()
29
37
  * - The client doesn't have them cached
30
38
  *
31
- * @param {Object} req - The request object
32
- * @param {Object} props - The props object
33
- * @returns {Object} - Filtered props with cached once-props removed
39
+ * @param {InertiaRequest} req - The request object
40
+ * @param {InertiaProps} props - The props object
41
+ * @returns {InertiaProps} - Filtered props with cached once-props removed
34
42
  */
35
43
  function filterOnceProps(req, props) {
36
44
  const exceptOnceProps = getExceptOnceProps(req)
@@ -40,6 +48,7 @@ function filterOnceProps(req, props) {
40
48
  return props
41
49
  }
42
50
 
51
+ /** @type {InertiaProps} */
43
52
  const filtered = {}
44
53
  for (const [key, value] of Object.entries(props)) {
45
54
  // Keep the prop if it's not a OnceProp
@@ -75,10 +84,11 @@ function filterOnceProps(req, props) {
75
84
  /**
76
85
  * Build the onceProps metadata for the page response.
77
86
  * This tells the client which props are "once" props and their expiration.
78
- * @param {Object} props - The props object
79
- * @returns {Object} - Object with onceProps key if any exist, empty object otherwise
87
+ * @param {InertiaProps} props - The props object
88
+ * @returns {OncePropsMetadata} - Object with onceProps key if any exist, empty object otherwise
80
89
  */
81
90
  function resolveOncePropsMetadata(props) {
91
+ /** @type {Record<string, { prop: string, expiresAt: number|null }>} */
82
92
  const onceProps = {}
83
93
 
84
94
  for (const [key, value] of Object.entries(props)) {
@@ -1,4 +1,10 @@
1
1
  const { PARTIAL_DATA } = require('../helpers/inertia-headers')
2
+
3
+ /**
4
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
5
+ * @typedef {import('../types').InertiaProps} InertiaProps
6
+ */
7
+
2
8
  /**
3
9
  * Extracts only the props specified by the partial header from the given props object.
4
10
  *
@@ -6,13 +12,14 @@ const { PARTIAL_DATA } = require('../helpers/inertia-headers')
6
12
  * which should contain a comma-separated list of property keys. It then constructs and returns
7
13
  * a new object that includes only those keys from the provided props object.
8
14
  *
9
- * @param {Object} req - The Express-style request object. It must have a `get` method to retrieve headers.
10
- * @param {Object} props - The complete set of props.
11
- * @returns {Object} An object containing only the properties whose keys were specified in the partial header.
15
+ * @param {InertiaRequest} req - The Express-style request object. It must have a `get` method to retrieve headers.
16
+ * @param {InertiaProps} props - The complete set of props.
17
+ * @returns {InertiaProps} An object containing only the properties whose keys were specified in the partial header.
12
18
  */
13
19
  module.exports = function resolveOnlyProps(req, props) {
14
20
  const partialOnlyHeader = req.get(PARTIAL_DATA)
15
21
  const only = partialOnlyHeader.split(',').filter(Boolean)
22
+ /** @type {InertiaProps} */
16
23
  let newProps = {}
17
24
 
18
25
  for (const key of only) newProps[key] = props[key]
@@ -1,4 +1,64 @@
1
+ const DeferProp = require('./defer-prop')
1
2
  const resolveProp = require('./resolve-prop')
3
+
4
+ /**
5
+ * @typedef {import('../types').InertiaProps} InertiaProps
6
+ * @typedef {import('../types').ResolvedPageProps} ResolvedPageProps
7
+ *
8
+ * @typedef {((props?: InertiaProps) => Promise<InertiaProps>) & {
9
+ * withMetadata: (props?: InertiaProps) => Promise<ResolvedPageProps>
10
+ * }} ResolvePageProps
11
+ */
12
+
13
+ /**
14
+ * @param {any} value
15
+ * @returns {boolean}
16
+ */
17
+ function shouldRescueProp(value) {
18
+ return value instanceof DeferProp && value.shouldRescueProp()
19
+ }
20
+
21
+ /**
22
+ * @param {InertiaProps} [props]
23
+ * @returns {Promise<ResolvedPageProps>}
24
+ */
25
+ async function resolvePagePropsWithMetadata(props = {}) {
26
+ const resolved = await Promise.all(
27
+ Object.entries(props).map(async ([key, value]) => {
28
+ try {
29
+ if (typeof value === 'function') {
30
+ const result = await value()
31
+ return { entry: await resolveProp(key, result) }
32
+ }
33
+ return { entry: await resolveProp(key, value) }
34
+ } catch (error) {
35
+ if (shouldRescueProp(value)) {
36
+ return { rescuedProp: key }
37
+ }
38
+ throw error
39
+ }
40
+ })
41
+ )
42
+
43
+ /** @type {Array<[string, any]>} */
44
+ const entries = []
45
+ /** @type {string[]} */
46
+ const rescuedProps = []
47
+
48
+ for (const result of resolved) {
49
+ if (result.rescuedProp) {
50
+ rescuedProps.push(result.rescuedProp)
51
+ continue
52
+ }
53
+ entries.push(result.entry)
54
+ }
55
+
56
+ return {
57
+ props: Object.fromEntries(entries),
58
+ rescuedProps
59
+ }
60
+ }
61
+
2
62
  /**
3
63
  * Resolve all page props.
4
64
  *
@@ -7,18 +67,17 @@ const resolveProp = require('./resolve-prop')
7
67
  * Then, every property is passed to resolveProp() to see if it needs
8
68
  * any special handling.
9
69
  *
10
- * @param {Object} [props={}] - An object containing page props.
11
- * @returns {Promise<Object>} A promise that resolves to a new object with resolved props.
70
+ * @param {InertiaProps} [props={}] - An object containing page props.
71
+ * @returns {Promise<InertiaProps>} A promise that resolves to a new object with resolved props.
12
72
  */
13
- module.exports = async function resolvePageProps(props = {}) {
14
- const entries = await Promise.all(
15
- Object.entries(props).map(async ([key, value]) => {
16
- if (typeof value === 'function') {
17
- const result = await value()
18
- return resolveProp(key, result)
19
- }
20
- return resolveProp(key, value)
21
- })
22
- )
23
- return Object.fromEntries(entries)
73
+ async function resolvePageProps(props = {}) {
74
+ const resolved = await resolvePagePropsWithMetadata(props)
75
+ return resolved.props
24
76
  }
77
+
78
+ /** @type {ResolvePageProps} */
79
+ const exportedResolvePageProps = Object.assign(resolvePageProps, {
80
+ withMetadata: resolvePagePropsWithMetadata
81
+ })
82
+
83
+ module.exports = exportedResolvePageProps