inertia-sails 0.3.3 → 1.0.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 +312 -60
- package/index.js +464 -27
- package/lib/handle-bad-request.js +20 -3
- package/lib/helpers/build-page-object.js +56 -8
- package/lib/helpers/inertia-headers.js +35 -8
- package/lib/helpers/request-context.js +199 -0
- package/lib/middleware/inertia-middleware.js +31 -9
- package/lib/props/always-prop.js +24 -0
- package/lib/props/defer-prop.js +38 -0
- package/lib/props/merge-prop.js +35 -0
- package/lib/props/mergeable-prop.js +27 -0
- package/lib/props/once-prop.js +105 -0
- package/lib/props/optional-prop.js +23 -0
- package/lib/props/pick-props-to-resolve.js +8 -0
- package/lib/props/resolve-merge-props.js +28 -4
- package/lib/props/resolve-once-props.js +105 -0
- package/lib/props/resolve-prop.js +3 -1
- package/lib/props/scroll-prop.js +80 -0
- package/lib/render.js +6 -4
- package/lib/responses/server-error.js +306 -0
- package/package.json +1 -1
- package/test.js +391 -0
package/README.md
CHANGED
|
@@ -1,71 +1,297 @@
|
|
|
1
|
-
#
|
|
1
|
+
# inertia-sails
|
|
2
|
+
|
|
3
|
+
The official Inertia.js adapter for Sails.js, powering [The Boring JavaScript Stack](https://sailscasts.com/boring).
|
|
2
4
|
|
|
3
5
|
## Installation
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install inertia-sails
|
|
9
|
+
```
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
Or use [create-sails](https://github.com/sailscastshq/create-sails) to scaffold a complete app:
|
|
8
12
|
|
|
9
|
-
```
|
|
10
|
-
|
|
13
|
+
```bash
|
|
14
|
+
npx create-sails my-app
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
## Quick Start
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
### 1. Configure Inertia
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
```js
|
|
22
|
+
// config/inertia.js
|
|
23
|
+
module.exports.inertia = {
|
|
24
|
+
rootView: 'app', // views/app.ejs
|
|
25
|
+
version: 1 // Asset version for cache busting
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Create a root view
|
|
30
|
+
|
|
31
|
+
```ejs
|
|
32
|
+
<!-- views/app.ejs -->
|
|
33
|
+
<!DOCTYPE html>
|
|
34
|
+
<html>
|
|
35
|
+
<head>
|
|
36
|
+
<meta charset="utf-8">
|
|
37
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
38
|
+
<%- shipwright.styles() %>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div id="app" data-page="<%- JSON.stringify(page) %>"></div>
|
|
42
|
+
<%- shipwright.scripts() %>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 3. Create an action
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// api/controllers/dashboard/view-dashboard.js
|
|
51
|
+
module.exports = {
|
|
52
|
+
exits: {
|
|
53
|
+
success: { responseType: 'inertia' }
|
|
54
|
+
},
|
|
55
|
+
fn: async function () {
|
|
56
|
+
return {
|
|
57
|
+
page: 'dashboard/index',
|
|
58
|
+
props: {
|
|
59
|
+
stats: await Stats.find()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
18
65
|
|
|
19
|
-
##
|
|
66
|
+
## API Reference
|
|
20
67
|
|
|
21
68
|
### Responses
|
|
22
69
|
|
|
23
|
-
|
|
70
|
+
#### `responseType: 'inertia'`
|
|
71
|
+
|
|
72
|
+
Return an Inertia page response:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
return {
|
|
76
|
+
page: 'users/index', // Component name
|
|
77
|
+
props: { users: [...] }, // Props passed to component
|
|
78
|
+
viewData: { title: '...' } // Data for root EJS template
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### `responseType: 'inertiaRedirect'`
|
|
83
|
+
|
|
84
|
+
Return a URL string to redirect:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
return '/dashboard'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Sharing Data
|
|
91
|
+
|
|
92
|
+
#### `share(key, value)`
|
|
93
|
+
|
|
94
|
+
Share data with the current request (request-scoped):
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
sails.inertia.share('flash', { success: 'Saved!' })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### `shareGlobally(key, value)`
|
|
101
|
+
|
|
102
|
+
Share data across all requests (app-wide):
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
// In hook initialization
|
|
106
|
+
sails.inertia.shareGlobally('appName', 'My App')
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### `viewData(key, value)`
|
|
110
|
+
|
|
111
|
+
Share data with the root EJS template:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
sails.inertia.viewData('title', 'Dashboard')
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Once Props (Cached)
|
|
118
|
+
|
|
119
|
+
Cache expensive props across navigations. The client tracks cached props and skips re-fetching.
|
|
120
|
+
|
|
121
|
+
#### `once(callback)`
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// In custom hook
|
|
125
|
+
sails.inertia.share(
|
|
126
|
+
'loggedInUser',
|
|
127
|
+
sails.inertia.once(async () => {
|
|
128
|
+
return await User.findOne({ id: req.session.userId })
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Chainable methods:**
|
|
134
|
+
|
|
135
|
+
- `.as(key)` - Custom cache key
|
|
136
|
+
- `.until(seconds)` - TTL expiration
|
|
137
|
+
- `.fresh(condition)` - Force refresh
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
sails.inertia
|
|
141
|
+
.once(() => fetchPermissions())
|
|
142
|
+
.as('user-permissions')
|
|
143
|
+
.until(3600) // Cache for 1 hour
|
|
144
|
+
.fresh(req.query.refresh === 'true')
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### `shareOnce(key, callback)`
|
|
148
|
+
|
|
149
|
+
Shorthand for `share()` + `once()`:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
sails.inertia.shareOnce('countries', () => Country.find())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### `refreshOnce(keys)`
|
|
156
|
+
|
|
157
|
+
Force refresh cached props from an action:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
// After updating user profile
|
|
161
|
+
await User.updateOne({ id: userId }).set({ fullName })
|
|
162
|
+
sails.inertia.refreshOnce('loggedInUser')
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Flash Messages
|
|
166
|
+
|
|
167
|
+
One-time messages that don't persist in browser history:
|
|
24
168
|
|
|
25
|
-
|
|
169
|
+
```js
|
|
170
|
+
sails.inertia.flash('success', 'Profile updated!')
|
|
171
|
+
sails.inertia.flash({ error: 'Failed', field: 'email' })
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Access in your frontend via `page.props.flash`.
|
|
175
|
+
|
|
176
|
+
### Deferred Props
|
|
177
|
+
|
|
178
|
+
Load props after initial page render:
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
return {
|
|
182
|
+
page: 'dashboard',
|
|
183
|
+
props: {
|
|
184
|
+
// Loads immediately
|
|
185
|
+
user: currentUser,
|
|
186
|
+
// Loads after render
|
|
187
|
+
analytics: sails.inertia.defer(async () => {
|
|
188
|
+
return await Analytics.getExpensiveReport()
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Merge Props
|
|
195
|
+
|
|
196
|
+
Merge with existing client-side data (useful for infinite scroll):
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
// Shallow merge
|
|
200
|
+
messages: sails.inertia.merge(() => newMessages)
|
|
201
|
+
|
|
202
|
+
// Deep merge (nested objects)
|
|
203
|
+
settings: sails.inertia.deepMerge(() => updatedSettings)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Infinite Scroll
|
|
207
|
+
|
|
208
|
+
Paginate data with automatic merge behavior:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
const page = this.req.param('page', 0)
|
|
212
|
+
const perPage = 20
|
|
213
|
+
const invoices = await Invoice.find().paginate(page, perPage)
|
|
214
|
+
const total = await Invoice.count()
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
page: 'invoices/index',
|
|
218
|
+
props: {
|
|
219
|
+
invoices: sails.inertia.scroll(() => invoices, {
|
|
220
|
+
page,
|
|
221
|
+
perPage,
|
|
222
|
+
total,
|
|
223
|
+
wrapper: 'data' // Wraps in { data: [...], meta: {...} }
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### History Encryption
|
|
230
|
+
|
|
231
|
+
Encrypt sensitive data in browser history:
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
sails.inertia.encryptHistory() // Enable for current request
|
|
235
|
+
sails.inertia.clearHistory() // Clear history state
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Root View
|
|
239
|
+
|
|
240
|
+
Change the root template per-request:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
sails.inertia.setRootView('auth') // Use views/auth.ejs
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Back Navigation
|
|
247
|
+
|
|
248
|
+
Get the referrer URL for redirects:
|
|
26
249
|
|
|
27
|
-
|
|
250
|
+
```js
|
|
251
|
+
return sails.inertia.back('/dashboard') // Fallback if no referrer
|
|
252
|
+
```
|
|
28
253
|
|
|
29
|
-
|
|
254
|
+
### Optional Props
|
|
30
255
|
|
|
31
|
-
|
|
256
|
+
Props only included when explicitly requested via partial reload:
|
|
32
257
|
|
|
33
258
|
```js
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
|
|
39
|
-
*/
|
|
259
|
+
categories: sails.inertia.optional(() => Category.find())
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Always Props
|
|
40
263
|
|
|
264
|
+
Props included even in partial reloads:
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
csrf: sails.inertia.always(() => this.req.csrfToken())
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Custom Hook Example
|
|
271
|
+
|
|
272
|
+
Share user data across all authenticated pages using `once()` for caching:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
// api/hooks/custom/index.js
|
|
41
276
|
module.exports = function defineCustomHook(sails) {
|
|
42
277
|
return {
|
|
43
|
-
/**
|
|
44
|
-
* Runs when this Sails app loads/lifts.
|
|
45
|
-
*/
|
|
46
|
-
initialize: async function () {
|
|
47
|
-
sails.log.info('Initializing custom hook (`custom`)')
|
|
48
|
-
},
|
|
49
278
|
routes: {
|
|
50
279
|
before: {
|
|
51
|
-
'GET
|
|
280
|
+
'GET /*': {
|
|
52
281
|
skipAssets: true,
|
|
53
282
|
fn: async function (req, res, next) {
|
|
54
283
|
if (req.session.userId) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
'
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
sails.inertia.share('loggedInUser', loggedInUser)
|
|
68
|
-
return next()
|
|
284
|
+
sails.inertia.share(
|
|
285
|
+
'loggedInUser',
|
|
286
|
+
sails.inertia.once(async () => {
|
|
287
|
+
return await User.findOne({ id: req.session.userId }).select([
|
|
288
|
+
'id',
|
|
289
|
+
'email',
|
|
290
|
+
'fullName',
|
|
291
|
+
'avatarUrl'
|
|
292
|
+
])
|
|
293
|
+
})
|
|
294
|
+
)
|
|
69
295
|
}
|
|
70
296
|
return next()
|
|
71
297
|
}
|
|
@@ -76,30 +302,56 @@ module.exports = function defineCustomHook(sails) {
|
|
|
76
302
|
}
|
|
77
303
|
```
|
|
78
304
|
|
|
79
|
-
##
|
|
305
|
+
## Custom Responses
|
|
306
|
+
|
|
307
|
+
Copy these to `api/responses/`:
|
|
308
|
+
|
|
309
|
+
- **inertia.js** - Handle Inertia page responses
|
|
310
|
+
- **inertiaRedirect.js** - Handle Inertia redirects
|
|
311
|
+
- **badRequest.js** - Validation errors with redirect back
|
|
312
|
+
- **serverError.js** - Error modal in dev, graceful redirect in prod
|
|
80
313
|
|
|
81
|
-
|
|
314
|
+
## Architecture
|
|
315
|
+
|
|
316
|
+
inertia-sails uses **AsyncLocalStorage** for request-scoped state, preventing data leaks between concurrent requests. This is critical for `share()`, `flash()`, `setRootView()`, and other per-request APIs.
|
|
317
|
+
|
|
318
|
+
## Configuration
|
|
82
319
|
|
|
83
320
|
```js
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
321
|
+
// config/inertia.js
|
|
322
|
+
module.exports.inertia = {
|
|
323
|
+
// Root EJS template (default: 'app')
|
|
324
|
+
rootView: 'app',
|
|
325
|
+
|
|
326
|
+
// Asset version for cache busting (optional - auto-detected by default)
|
|
327
|
+
// version: 'custom-version',
|
|
328
|
+
|
|
329
|
+
// History encryption settings
|
|
330
|
+
history: {
|
|
331
|
+
encrypt: false
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Automatic Asset Versioning
|
|
337
|
+
|
|
338
|
+
inertia-sails automatically handles asset versioning:
|
|
339
|
+
|
|
340
|
+
1. **With Shipwright**: Reads `.tmp/public/manifest.json` and generates an MD5 hash. Version changes when any bundled asset changes.
|
|
93
341
|
|
|
342
|
+
2. **Without Shipwright**: Falls back to server startup timestamp, ensuring fresh assets on each restart.
|
|
343
|
+
|
|
344
|
+
You can override this with a custom version if needed:
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
// config/inertia.js
|
|
94
348
|
module.exports.inertia = {
|
|
95
|
-
|
|
96
|
-
* https://inertiajs.com/asset-versioning
|
|
97
|
-
* You can pass a string, number that changes when your assets change
|
|
98
|
-
* or a function that returns the said string, number.
|
|
99
|
-
* e.g 4 or () => 4
|
|
100
|
-
*/
|
|
101
|
-
// version: 1,
|
|
349
|
+
version: 'v2.1.0' // Or a function: () => myCustomVersion()
|
|
102
350
|
}
|
|
103
351
|
```
|
|
104
352
|
|
|
105
|
-
|
|
353
|
+
## References
|
|
354
|
+
|
|
355
|
+
- [The Boring Stack Docs](https://docs.sailscasts.com/boring-stack)
|
|
356
|
+
- [Inertia.js](https://inertiajs.com)
|
|
357
|
+
- [Sails.js](https://sailsjs.com)
|