inertia-sails 0.2.1 → 0.3.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/index.js CHANGED
@@ -4,7 +4,16 @@
4
4
  * @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions, and/or initialization logic.
5
5
  * @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
6
6
  */
7
- const inertia = require('./private/inertia-middleware')
7
+ const inertia = require('./lib/middleware/inertia-middleware')
8
+ const render = require('./lib/render')
9
+ const location = require('./lib/location')
10
+
11
+ const DeferProp = require('./lib/props/defer-prop')
12
+ const OptionalProp = require('./lib/props/optional-prop')
13
+ const MergeProp = require('./lib/props/merge-prop')
14
+ const AlwaysProp = require('./lib/props/always-prop')
15
+ const handleBadRequest = require('./lib/handle-bad-request')
16
+
8
17
  module.exports = function defineInertiaHook(sails) {
9
18
  let hook
10
19
  const routesToBindInertiaTo = [
@@ -20,7 +29,10 @@ module.exports = function defineInertiaHook(sails) {
20
29
  defaults: {
21
30
  inertia: {
22
31
  rootView: 'app',
23
- version: 1
32
+ version: 1,
33
+ history: {
34
+ encrypt: false
35
+ }
24
36
  }
25
37
  },
26
38
  initialize: async function () {
@@ -28,27 +40,145 @@ module.exports = function defineInertiaHook(sails) {
28
40
  sails.inertia = hook
29
41
  sails.inertia.sharedProps = {}
30
42
  sails.inertia.sharedViewData = {}
31
- sails.on('router:before', function routerBefore() {
32
- routesToBindInertiaTo.forEach(function iterator(routeAddress) {
43
+ sails.inertia.shouldEncryptHistory = sails.config.inertia.history.encrypt
44
+ sails.inertia.shouldClearHistory = false
45
+ sails.on('router:before', function () {
46
+ routesToBindInertiaTo.forEach(function (routeAddress) {
33
47
  sails.router.bind(routeAddress, inertia(hook))
34
48
  })
35
49
  })
36
50
  },
37
51
 
38
- share: (key, value = null) => (sails.inertia.sharedProps[key] = value),
52
+ /**
53
+ * Share a property globally
54
+ * @param {string} key - The key of the property
55
+ * @param {*} value - The value of the property
56
+ */
57
+ share(key, value = null) {
58
+ return (sails.inertia.sharedProps[key] = value)
59
+ },
39
60
 
40
- getShared: (key = null) =>
41
- sails.inertia.sharedProps[key] ?? sails.inertia.sharedProps,
61
+ /**
62
+ * Get shared properties
63
+ * @param {string|null} key - The key of the property to get, or null to get all
64
+ * @returns {*} - The shared property or all shared properties
65
+ */
66
+ getShared(key = null) {
67
+ return sails.inertia.sharedProps[key] ?? sails.inertia.sharedProps
68
+ },
42
69
 
43
- flushShared: (key) => {
44
- key
70
+ /**
71
+ * Flush shared properties
72
+ * @param {string|null} key - The key of the property to flush, or null to flush all
73
+ */
74
+ flushShared(key) {
75
+ return key
45
76
  ? delete sails.inertia.sharedProps[key]
46
77
  : (sails.inertia.sharedProps = {})
47
78
  },
48
79
 
49
- viewData: (key, value) => (sails.inertia.sharedViewData[key] = value),
80
+ /**
81
+ * Add view data
82
+ * @param {string} key - The key of the view data
83
+ * @param {*} value - The value of the view data
84
+ */
85
+ viewData(key, value) {
86
+ return (sails.inertia.sharedViewData[key] = value)
87
+ },
88
+
89
+ /**
90
+ * Get view data
91
+ * @param {string} key - The key of the view data to get
92
+ * @returns {*} - The view data
93
+ */
94
+ getViewData(key) {
95
+ return sails.inertia.sharedViewData[key] ?? sails.inertia.sharedViewData
96
+ },
97
+
98
+ /**
99
+ * Create an optional prop
100
+ * This allows you to define properties that are only evaluated when accessed.
101
+ * @docs https://docs.sailscasts.com/boring-stack/partial-reloads#lazy-data-evaluation
102
+ * @param {Function} callback - The callback function to execute
103
+ * @returns {OptionalProp} - The optional prop
104
+ */
105
+ optional(callback) {
106
+ return new OptionalProp(callback)
107
+ },
108
+
109
+ /**
110
+ * Create a mergeable prop
111
+ * This allows you to merge multiple props together.
112
+ * @docs https://docs.sailscasts.com/boring-stack/merging-props
113
+ * @param {Function} callback - The callback function to execute
114
+ * @returns {MergeProp} - The mergeable prop
115
+ */
116
+ merge(callback) {
117
+ return new MergeProp(callback)
118
+ },
119
+
120
+ /**
121
+ * Create an always prop
122
+ * Always props are resolved on every request, whether partial or not.
123
+ * @docs https://docs.sailscasts.com/boring-stack/partial-reloads#lazy-data-evaluation
124
+ * @param {Function} callback - The callback function
125
+ * @returns {AlwaysProp} - The always prop
126
+ */
127
+ always(callback) {
128
+ return new AlwaysProp(callback)
129
+ },
130
+ /**
131
+ * Create a deferred prop
132
+ * This allows you to load certain page data after the initial render.
133
+ * @docs https://docs.sailscasts.com/boring-stack/deferred-props
134
+ * @param {Function} cb - The callback function to execute
135
+ * @param {string} group - The group name
136
+ * @returns {DeferProp} - The deferred prop
137
+ */
138
+ defer(cb, group = 'default') {
139
+ return new DeferProp(cb, group)
140
+ },
141
+
142
+ /**
143
+ * Render the response
144
+ * @param {Object} req - The request object
145
+ * @param {Object} res - The response object
146
+ * @param {Object} data - The data to render
147
+ * @returns {*} - The rendered response
148
+ */
149
+ render(req, res, data) {
150
+ return render(req, res, data)
151
+ },
152
+ /**
153
+ * Handle Inertia redirects
154
+ * See https://docs.sailscasts.com/boring-stack/redirects
155
+ * @param {Object} req - The request object
156
+ * @param {Object} res - The response object
157
+ * @param {string} url - The URL to redirect to
158
+ * @returns {Object} - The response object with the redirect
159
+ */
160
+ location(req, res, url) {
161
+ return location(req, res, url)
162
+ },
50
163
 
51
- getViewData: (key) =>
52
- sails.inertia.sharedViewData[key] ?? sails.inertia.sharedViewData
164
+ /**
165
+ * Encrypt history
166
+ * @docs https://docs.sailscasts.com/boring-stack/history-encryption
167
+ * @param {boolean} encrypt - Whether to encrypt the history
168
+ */
169
+ encryptHistory(encrypt = true) {
170
+ sails.inertia.shouldEncryptHistory = encrypt
171
+ },
172
+
173
+ /**
174
+ * Clear history state.
175
+ * @docs https://docs.sailscasts.com/boring-stack/history-encryption#clearing-history
176
+ */
177
+ clearHistory() {
178
+ sails.inertia.shouldClearHistory = true
179
+ },
180
+ handleBadRequest(req, res, optionaLData) {
181
+ return handleBadRequest(req, res, optionaLData)
182
+ }
53
183
  }
54
184
  }
@@ -0,0 +1,65 @@
1
+ // @ts-nocheck
2
+ module.exports = function handleBadRequest(req, res, optionalData) {
3
+ const sails = req._sails
4
+ // Define the status code to send in the response.
5
+ const statusCodeToSet = 400
6
+
7
+ // Check if it's an Inertia request
8
+ if (req.header('X-Inertia')) {
9
+ if (optionalData && optionalData.problems) {
10
+ const errors = {}
11
+ optionalData.problems.forEach((problem) => {
12
+ if (typeof problem === 'object') {
13
+ Object.keys(problem).forEach((propertyName) => {
14
+ const sanitizedProblem = problem[propertyName].replace(/\.$/, '') // Trim trailing dot
15
+ if (!errors[propertyName]) {
16
+ errors[propertyName] = [sanitizedProblem]
17
+ } else {
18
+ errors[propertyName].push(sanitizedProblem)
19
+ }
20
+ })
21
+ } else {
22
+ const regex = /"(.*?)"/
23
+ const matches = problem.match(regex)
24
+
25
+ if (matches && matches.length > 1) {
26
+ const propertyName = matches[1]
27
+ const sanitizedProblem = problem
28
+ .replace(/"([^"]+)"/, '$1')
29
+ .replace('\n', '')
30
+ .replace('·', '')
31
+ .trim()
32
+ if (!errors[propertyName]) {
33
+ errors[propertyName] = [sanitizedProblem]
34
+ } else {
35
+ errors[propertyName].push(sanitizedProblem)
36
+ }
37
+ }
38
+ }
39
+ })
40
+ req.session.errors = errors
41
+ return res.redirect(303, req.get('Referrer') || '/')
42
+ }
43
+ }
44
+
45
+ // If not an Inertia request, perform the normal badRequest response
46
+ if (optionalData === undefined) {
47
+ sails.log.info('Ran custom response: res.badRequest()')
48
+ return res.sendStatus(statusCodeToSet)
49
+ } else if (_.isError(optionalData)) {
50
+ sails.log.info(
51
+ 'Custom response `res.badRequest()` called with an Error:',
52
+ optionalData
53
+ )
54
+
55
+ if (!_.isFunction(optionalData.toJSON)) {
56
+ if (process.env.NODE_ENV === 'production') {
57
+ return res.sendStatus(statusCodeToSet)
58
+ } else {
59
+ return res.status(statusCodeToSet).send(optionalData.stack)
60
+ }
61
+ }
62
+ } else {
63
+ return res.status(statusCodeToSet).send(optionalData)
64
+ }
65
+ }
@@ -0,0 +1,27 @@
1
+ const pickPropsToResolve = require('../props/pick-props-to-resolve')
2
+ const resolveDeferredProps = require('../props/resolve-deferred-props')
3
+ const resolveMergeProps = require('../props/resolve-merge-props')
4
+ const resolvePageProps = require('../props/resolve-page-props')
5
+
6
+ module.exports = async function buildPageObject(req, component, pageProps) {
7
+ const sails = req._sails
8
+ let url = req.url || req.originalUrl
9
+ const assetVersion = sails.config.inertia.version
10
+ const currentVersion =
11
+ typeof assetVersion === 'function' ? assetVersion() : assetVersion
12
+ const propsToResolve = pickPropsToResolve(req, component, {
13
+ ...sails.inertia.sharedProps,
14
+ ...pageProps
15
+ })
16
+
17
+ return {
18
+ component,
19
+ url,
20
+ version: currentVersion,
21
+ props: await resolvePageProps(propsToResolve),
22
+ clearHistory: sails.inertia.shouldClearHistory,
23
+ encryptHistory: sails.inertia.shouldEncryptHistory,
24
+ ...resolveMergeProps(req, pageProps),
25
+ ...resolveDeferredProps(req, component, pageProps)
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+ module.exports = Symbol('ignoreFirstLoad')
@@ -2,6 +2,9 @@ module.exports = {
2
2
  INERTIA: 'X-Inertia',
3
3
  VERSION: 'X-Inertia-Version',
4
4
  PARTIAL_DATA: 'X-Inertia-Partial-Data',
5
+ PARTIAL_EXCEPT: 'X-Inertia-Partial-Except',
6
+ ERROR_BAG: 'X-Inertia-Error-Bag',
7
+ RESET: 'X-Inertia-Reset',
5
8
  PARTIAL_COMPONENT: 'X-Inertia-Partial-Component',
6
9
  LOCATION: 'X-Inertia-Location'
7
10
  }
@@ -0,0 +1,4 @@
1
+ const { PARTIAL_COMPONENT } = require('./inertia-headers')
2
+ module.exports = function isInertiaPartialRequest(req, component) {
3
+ return req.get(PARTIAL_COMPONENT) === component
4
+ }
@@ -1,3 +1,6 @@
1
+ // @ts-nocheck
2
+ const { ERROR_BAG } = require('./inertia-headers')
3
+
1
4
  /**
2
5
  * @module resolveValidationErrors
3
6
  * @description Resolves and formats validation errors from the session for Inertia responses.
@@ -22,7 +25,7 @@ module.exports = function resolveValidationErrors(req) {
22
25
  return result
23
26
  }, {})
24
27
 
25
- const inertiaErrorBag = req.headers['x-inertia-error-bag']
28
+ const inertiaErrorBag = req.headers[ERROR_BAG]
26
29
 
27
30
  if (inertiaErrorBag && collectedErrors[inertiaErrorBag]) {
28
31
  const selectedErrors = {
@@ -0,0 +1,19 @@
1
+ const { INERTIA, LOCATION } = require('./helpers/inertia-headers')
2
+
3
+ module.exports = function (req, res, url) {
4
+ // If this is an Inertia request, send a 409 Conflict response
5
+ // with the X-Inertia-Location header so the client performs a full window.location visit.
6
+ if (req.get(INERTIA)) {
7
+ res.set(LOCATION, url)
8
+ return res.status(409).end()
9
+ }
10
+
11
+ // For non-Inertia requests:
12
+ // If the method is PUT, PATCH, or DELETE, force a 303 redirect (so the next request is a GET)
13
+ if (['PUT', 'PATCH', 'DELETE'].includes(req.method)) {
14
+ return res.redirect(303, url)
15
+ }
16
+
17
+ // Otherwise, perform a standard redirect (default is typically a 302)
18
+ return res.redirect(url)
19
+ }
@@ -1,6 +1,6 @@
1
- const isInertiaRequest = require('./is-inertia-request')
1
+ const isInertiaRequest = require('../helpers/is-inertia-request')
2
2
 
3
- const resolveValidationErrors = require('./resolve-validation-errors')
3
+ const resolveValidationErrors = require('../helpers/resolve-validation-errors')
4
4
  function inertia(hook) {
5
5
  return function inertiaMiddleware(req, res, next) {
6
6
  if (isInertiaRequest(req)) {
@@ -0,0 +1,8 @@
1
+ const MergeableProp = require('./mergeable-prop')
2
+
3
+ module.exports = class AlwaysProp extends MergeableProp {
4
+ constructor(callback) {
5
+ super()
6
+ this.callback = callback
7
+ }
8
+ }
@@ -0,0 +1,15 @@
1
+ const ignoreFirstLoadSymbol = require('../helpers/ignore-first-load-symbol')
2
+ const MergeableProp = require('./mergeable-prop')
3
+
4
+ module.exports = class DeferProp extends MergeableProp {
5
+ constructor(callback, group) {
6
+ super()
7
+ this.callback = callback
8
+ this.group = group
9
+ this[ignoreFirstLoadSymbol] = true
10
+ }
11
+
12
+ getGroup() {
13
+ return this.group
14
+ }
15
+ }
@@ -0,0 +1,9 @@
1
+ const MergeableProp = require('./mergeable-prop')
2
+
3
+ module.exports = class MergeProp extends MergeableProp {
4
+ constructor(callback) {
5
+ super()
6
+ this.callback = callback
7
+ this.shouldMerge = true
8
+ }
9
+ }
@@ -0,0 +1,10 @@
1
+ module.exports = class MergeableProp {
2
+ constructor() {
3
+ this.shouldMerge = false
4
+ }
5
+
6
+ merge() {
7
+ this.shouldMerge = true
8
+ return this
9
+ }
10
+ }
@@ -0,0 +1,8 @@
1
+ const ignoreFirstLoadSymbol = require('../helpers/ignore-first-load-symbol')
2
+
3
+ module.exports = class OptionalProp {
4
+ constructor(callback) {
5
+ this.callback = callback
6
+ this[ignoreFirstLoadSymbol] = true
7
+ }
8
+ }
@@ -0,0 +1,34 @@
1
+ const isInertiaPartialRequest = require('../helpers/is-inertia-partial-request')
2
+ const ignoreFirstLoadSymbol = require('../helpers/ignore-first-load-symbol')
3
+ const { PARTIAL_DATA, PARTIAL_EXCEPT } = require('../helpers/inertia-headers')
4
+ const resolveOnlyProps = require('./resolve-only-props')
5
+ const resolveExceptProps = require('./resolve-except-props')
6
+ const AlwaysProp = require('./always-prop')
7
+
8
+ module.exports = function pickPropsToResolve(req, component, props = {}) {
9
+ const isPartial = isInertiaPartialRequest(req, component)
10
+ let newProps = props
11
+
12
+ if (!isPartial) {
13
+ newProps = Object.fromEntries(
14
+ Object.entries(props).filter(([_, value]) => {
15
+ if (value && value[ignoreFirstLoadSymbol]) return false
16
+ return true
17
+ })
18
+ )
19
+ }
20
+
21
+ if (isPartial && req.get(PARTIAL_DATA)) {
22
+ newProps = resolveOnlyProps(req, newProps)
23
+ }
24
+
25
+ if (isPartial && req.get(PARTIAL_EXCEPT)) {
26
+ newProps = resolveExceptProps(req, newProps)
27
+ }
28
+
29
+ for (const [key, value] of Object.entries(props)) {
30
+ if (value instanceof AlwaysProp) newProps[key] = props[key]
31
+ }
32
+
33
+ return newProps
34
+ }
@@ -0,0 +1,15 @@
1
+ const isInertiaPartialRequest = require('../helpers/is-inertia-partial-request')
2
+ const DeferProp = require('./defer-prop')
3
+ module.exports = function resolveDeferredProps(req, component, pageProps) {
4
+ if (isInertiaPartialRequest(req, component)) return {}
5
+ const deferredProps = Object.entries(pageProps || {})
6
+ .filter(([_, value]) => value instanceof DeferProp)
7
+ .map(([key, value]) => ({ key, group: value.getGroup() }))
8
+ .reduce((groups, { key, group }) => {
9
+ if (!groups[group]) groups[group] = []
10
+ groups[group].push(key)
11
+ return groups
12
+ }, {})
13
+
14
+ return Object.keys(deferredProps).length ? { deferredProps } : {}
15
+ }
@@ -0,0 +1,9 @@
1
+ const { PARTIAL_EXCEPT } = require('../helpers/inertia-headers')
2
+ module.exports = function resolveExceptProps(req, props) {
3
+ const partialExceptHeader = req.get(PARTIAL_EXCEPT)
4
+ const except = partialExceptHeader.split(',').filter(Boolean)
5
+
6
+ for (const key of except) delete props[key]
7
+
8
+ return props
9
+ }
@@ -0,0 +1,13 @@
1
+ const { RESET } = require('../helpers/inertia-headers')
2
+ const MergeableProp = require('./mergeable-prop')
3
+ module.exports = function resolveMergeProps(req, pageProps) {
4
+ const inertiaResetHeader = req.get(RESET)
5
+ const resetProps = new Set(inertiaResetHeader?.split(',').filter(Boolean))
6
+
7
+ const mergeProps = Object.entries(pageProps || {})
8
+ .filter(([_, value]) => value instanceof MergeableProp && value.shouldMerge)
9
+ .map(([key]) => key)
10
+ .filter((key) => !resetProps.has(key))
11
+
12
+ return mergeProps.length ? { mergeProps } : {}
13
+ }
@@ -0,0 +1,21 @@
1
+ const { PARTIAL_DATA } = require('../helpers/inertia-headers')
2
+ /**
3
+ * Extracts only the props specified by the partial header from the given props object.
4
+ *
5
+ * This function reads the header defined by the PARTIAL_DATA constant from the request,
6
+ * which should contain a comma-separated list of property keys. It then constructs and returns
7
+ * a new object that includes only those keys from the provided props object.
8
+ *
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.
12
+ */
13
+ module.exports = function resolveOnlyProps(req, props) {
14
+ const partialOnlyHeader = req.get(PARTIAL_DATA)
15
+ const only = partialOnlyHeader.split(',').filter(Boolean)
16
+ let newProps = {}
17
+
18
+ for (const key of only) newProps[key] = props[key]
19
+
20
+ return newProps
21
+ }
@@ -0,0 +1,24 @@
1
+ const resolveProp = require('./resolve-prop')
2
+ /**
3
+ * Resolve all page props.
4
+ *
5
+ * This function iterates over each property in the given object.
6
+ * If a property is a function, it is invoked with the provided context.
7
+ * Then, every property is passed to resolveProp() to see if it needs
8
+ * any special handling.
9
+ *
10
+ * @param {Object} [props={}] - An object containing page props.
11
+ * @returns {Promise<Object>} A promise that resolves to a new object with resolved props.
12
+ */
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)
24
+ }
@@ -0,0 +1,27 @@
1
+ const OptionalProp = require('./optional-prop')
2
+ const MergeProp = require('./merge-prop')
3
+ const DeferProp = require('./defer-prop')
4
+ const AlwaysProp = require('./always-prop')
5
+ /**
6
+ * Resolve a single prop.
7
+ *
8
+ * If the value is an instance of one of our special prop types,
9
+ * we assume it has a "callback" property that should be executed
10
+ * to retrieve the final value.
11
+ *
12
+ * @param {string} key - The key for the prop.
13
+ * @param {any} value - The value to resolve.
14
+ * @returns {Promise<[string, any]>} A promise that resolves to a key-value pair.
15
+ */
16
+ module.exports = async function resolveProp(key, value) {
17
+ if (
18
+ value instanceof OptionalProp ||
19
+ value instanceof MergeProp ||
20
+ value instanceof DeferProp ||
21
+ value instanceof AlwaysProp
22
+ ) {
23
+ return [key, await value.callback()]
24
+ }
25
+
26
+ return [key, value]
27
+ }
package/lib/render.js ADDED
@@ -0,0 +1,35 @@
1
+ const { encode } = require('querystring')
2
+ const inertiaHeaders = require('./helpers/inertia-headers')
3
+ const buildPageObject = require('./helpers/build-page-object')
4
+
5
+ module.exports = async function render(req, res, data) {
6
+ const sails = req._sails
7
+
8
+ const sharedViewData = sails.inertia.sharedViewData
9
+ const rootView = sails.config.inertia.rootView
10
+
11
+ const allViewData = {
12
+ ...sharedViewData,
13
+ ...data.viewData
14
+ }
15
+
16
+ let page = await buildPageObject(req, data.page, data.props)
17
+
18
+ const queryParams = req.query
19
+ if (req.method === 'GET' && Object.keys(queryParams).length) {
20
+ // Keep original request query params
21
+ page.url += `?${encode(queryParams)}`
22
+ }
23
+
24
+ if (req.get(inertiaHeaders.INERTIA)) {
25
+ res.set(inertiaHeaders.INERTIA, true)
26
+ res.set('Vary', 'Accept')
27
+ return res.json(page)
28
+ } else {
29
+ // Implements full page reload
30
+ return res.view(rootView, {
31
+ page,
32
+ viewData: allViewData
33
+ })
34
+ }
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inertia-sails",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "The Sails adapter for Inertia.",
5
5
  "main": "index.js",
6
6
  "sails": {
@@ -8,13 +8,12 @@
8
8
  "hookName": "inertia"
9
9
  },
10
10
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1",
12
- "prepare": "cd .. && husky install"
11
+ "test": "echo \"Error: no test specified\" && exit 1"
13
12
  },
14
13
  "repository": {
15
14
  "type": "git",
16
15
  "url": "git+https://github.com/sailscastshq/boring-stack.git",
17
- "directory": "inertia-sails"
16
+ "directory": "packages/inertia-sails"
18
17
  },
19
18
  "peerDependencies": {
20
19
  "sails": ">=1",
@@ -29,5 +28,6 @@
29
28
  "bugs": {
30
29
  "url": "https://github.com/sailscastshq/boring-stack/issues"
31
30
  },
32
- "homepage": "https://github.com/sailscastshq/boring-stack/inertia-sails#readme"
31
+ "homepage": "https://github.com/sailscastshq/boring-stack/tree/main/inertia-sails#readme",
32
+ "devDependencies": {}
33
33
  }
Binary file
File without changes
File without changes