Releases: sailscastshq/boring-stack
The Boring Stack 1.5.0
The Boring Stack 1.5.0
The Boring Stack 1.5.0 is the release that moves the Sails + Inertia foundation to stable Inertia v3 and rounds out the developer experience around rendering, validation, testing, and errors.
This release includes inertia-sails@1.5.0, rsbuild-plugin-inertia@0.0.1, updated templates, selective SSR, Precognition, richer v3 protocol metadata, and Youch-powered development errors.
Highlights
- Stable Inertia v3 support across
inertia-sailsand the Vue, React, and Svelte templates. - New
rsbuild-plugin-inertiapackage for Rsbuild page resolution, lazy page splitting, optional Axios adapter stubbing, and SSR build wiring. - Selective Inertia SSR that runs inside Sails without a separate SSR server process.
- Inertia Precognition support for live server-side validation using the same Action2 inputs and
badRequestflow. - Rescuable deferred props with
rescuedProps, plus the.rescue()and{ rescue: true }APIs. sails.inertia.preserveFragment()for redirects that should keep the current hash.- Production Inertia error pages for
403,404,500, and503. - Youch-powered development error pages that Inertia can show inside its development modal.
- Template test structure moved toward Sounding-first functional testing.
- Templates updated to Shipwright
^1.4.0, Inertia adapters^3.1.1, andinertia-sails@^1.5.0.
Inertia v3 support
The root page payload now follows Inertia v3's dedicated JSON script shape:
<div id="app"></div>
<script type="application/json" data-page="app">
<%- JSON.stringify(page).replace(/</g, '\\u003c') %>
</script>The adapter now emits the protocol metadata the v3 client adapters need:
sharedPropsclearHistoryencryptHistorydeferredPropsmergePropsprependPropsdeepMergePropsmatchPropsOnscrollPropsrescuedPropspreserveFragment
This keeps Sails actions simple while giving the Inertia client the full v3 page object shape.
Rescuable deferred props
Deferred props can now fail gracefully when the data is useful but not critical:
analytics: sails.inertia
.defer(async () => {
return await Analytics.getExpensiveReport()
})
.rescue()You can also pass the intent inline:
analytics: sails.inertia.defer(
async () => {
return await Analytics.getExpensiveReport()
},
{ rescue: true }
)When a rescuable deferred prop fails, inertia-sails omits it from props and reports the key in rescuedProps. Use this for secondary panels such as analytics, recommendations, and activity feeds. Do not use it for critical page data.
Preserved fragments
Use preserveFragment() when a redirect should carry the current hash:
sails.inertia.preserveFragment()
return '/articles/new-slug'This is opt-in because fragment preservation should be a redirect-level product decision, not hidden global behavior.
rsbuild-plugin-inertia
rsbuild-plugin-inertia centralizes Inertia-specific Rsbuild behavior:
- default page resolver injection for
./pages - an Inertia/Vite-style
pagesshorthand for non-standard directories - lazy page loading by default for page-level code splitting
lazy: falsefor single-bundle apps- Vue, React, and Svelte adapter detection
- automatic SSR build environment setup when
assets/js/ssr.jsexists - optional Axios adapter stubbing when Axios is not installed
Template config/shipwright.js files now use this shape:
const { pluginVue } = require('@rsbuild/plugin-vue')
const { pluginInertia } = require('rsbuild-plugin-inertia')
module.exports.shipwright = {
build: {
plugins: [pluginVue(), pluginInertia()]
}
}Use pluginReact() or pluginSvelte() for React and Svelte apps.
Because the plugin stubs Inertia v3's optional Axios adapter import when Axios is not installed, templates no longer need Axios unless the app code imports Axios directly.
Selective SSR
The Boring Stack now supports Inertia SSR without requiring a separate SSR server process. Shipwright builds a private SSR bundle, and inertia-sails imports it in-process from Sails.
Enable SSR for every Inertia page:
module.exports.inertia = {
ssr: true
}Or enable it selectively:
module.exports.inertia = {
ssr: {
enabled: true,
pages: ['index', 'pricing', 'blog/show']
}
}Opt out per response:
return {
page: 'dashboard/index',
ssr: false,
props: { user }
}Use SSR for public pages where first paint, SEO, and social sharing matter. Skip it for most authenticated dashboards and heavily browser-dependent screens.
Precognition
Precognition comes from the Laravel/Inertia ecosystem. It lets the client ask the same server action to validate data before the real submit happens.
That means you do not duplicate server validation in the browser.
Client:
const form = useForm({
email: ''
}).withPrecognition('post', '/forgot-password')
form.validate('email')Server response:
// api/responses/precognitionSuccess.js
module.exports = function precognitionSuccess() {
return this.req._sails.inertia.handlePrecognitionSuccess(this.req, this.res)
}Server action:
if (sails.inertia.isPrecognitive(this.req)) {
return exits.precognitionSuccess()
}For availability checks such as "username is taken", use shouldValidate() so a blur on one field does not run every expensive validation rule:
if (sails.inertia.shouldValidate('username', this.req)) {
const exists = await User.count({ username })
if (exists > 0) {
throw {
badSignupRequest: {
problems: [{ username: 'Username is already taken.' }]
}
}
}
}Precognition is best for signup, forgot password, invite, username, profile, onboarding, and billing forms where early server-backed feedback improves the product.
Humanized validation errors
inertia-sails now humanizes common Action2, Anchor, and RTTC validation messages before they reach Inertia forms. Precognition and normal submit-time errors share the same path, so the user sees consistent field messages whether validation happened on blur or on submit.
Rich development errors with Youch
Development server errors now render with Youch.
For Inertia requests, Inertia shows the Youch HTML in its development error modal. For normal browser visits, the browser gets the full Youch page.
The Youch output includes readable stack frames, source snippets, request context, and sanitized metadata. Cookies, authorization headers, CSRF tokens, passwords, secrets, and session-looking values are redacted before rendering.
We also opened an upstream Inertia issue for the tiny empty modal flash that can appear before iframe content is written: inertiajs/inertia#3130.
Production Inertia error pages
Templates now ship an error page:
assets/js/pages/error.vueassets/js/pages/error.jsxassets/js/pages/error.svelte
inertia-sails renders that page by default for:
403404500503
The page receives:
{
status: 404,
title: 'Page not found',
message: 'The page you are looking for could not be found.'
}Templates also include notFound and forbidden responses that delegate to sails.inertia.handleErrorPage().
Hybrid apps can keep EJS error pages by setting:
module.exports.inertia = {
errorPage: false
}Sounding and template tests
The templates now lean into Sounding as the default testing story:
- unit tests for small pure logic
- functional tests for Sails actions, responses, auth, mail, and Inertia responses
- browser tests only when browser behavior itself matters
This gives Boring Stack apps a calmer test split and avoids making every workflow a full browser automation problem.
Upgrade guide
1. Update packages
Vue:
npm install inertia-sails@^1.5.0 @inertiajs/vue3@^3.1.1 rsbuild-plugin-inertia@latest sails-hook-shipwright@^1.4.0
npm uninstall axiosReact:
npm install inertia-sails@^1.5.0 @inertiajs/react@^3.1.1 rsbuild-plugin-inertia@latest sails-hook-shipwright@^1.4.0
npm uninstall axiosSvelte:
npm install inertia-sails@^1.5.0 @inertiajs/svelte@^3.1.1 rsbuild-plugin-inertia@latest sails-hook-shipwright@^1.4.0
npm uninstall axiosKeep Axios only if your app imports Axios directly.
2. Update views/app.ejs
Move the page payload to the dedicated JSON script:
<div id="app"></div>
<script type="application/json" data-page="app">
<%- JSON.stringify(page).replace(/</g, '\\u003c') %>
</script>If you enable SSR, render ssr.body when present and include ssr.head in the document head.
3. Update config/shipwright.js
Add pluginInertia() after the framework plugin:
const { pluginVue } = require('@rsbuild/plugin-vue')
const { pluginInertia } = require('rsbuild-plugin-inertia')
module.exports.shipwright = {
build: {
plugins: [pluginVue(), pluginInertia()]
}
}4. Simplify assets/js/app.js
If your pages live in assets/js/pages, remove custom page resolution and let rsbuild-plugin-inertia inject the default resolver:
createInertiaApp({
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) }).use(plugin).mount(el)
}
})Use pages: './screens' or the object form only when your app has a non-standard page directory.
5. Add production error pages
Create an error page under your page directory and add notFound and forbidden responses that call sails.inertia.handleErrorPage().
6. Add Precognition where it helps
Add api/responses/precognitionSuccess.js, return that exit before side effects, and call .withPrecognition() on the client form.
7. Add SSR only where it pays for itself
Create assets/js/ssr.js, enable ssr: true or selective ssr.pages, and verify view-source: contains rendered HTML for SSR-enabled pages.
8. Use the new protocol APIs intentio...
v1.2.3
inertia-sails v1.3.3
Bug Fixes
inertiaContext middleware not injected when middleware order is not configured (#193)
Fixed a bug where the inertiaContext middleware was never injected into the HTTP middleware stack when sails.config.http.middleware.order was not explicitly defined in config/http.js — which is the default for all Sails apps.
This caused sails.inertia.share() calls in routes.before handlers to run outside AsyncLocalStorage context, silently dropping all shared props and logging warnings:
warn: sails.inertia.share('loggedInUser') called outside request context. Value was not stored.
warn: sails.inertia.share('navProjects') called outside request context. Value was not stored.
Root cause: The configure phase checked mw.order && ... before injecting inertiaContext. Since the default Sails scaffold comments out middleware.order, mw.order was undefined and the injection was skipped entirely. The middleware function was registered but never placed in the execution order.
Fix: When mw.order is not defined, initialize it with Sails' default middleware order, then inject inertiaContext before the router as intended.
Full Changelog
- fix(inertia-sails): inject inertiaContext middleware when order is not configured (#194)
Full Changelog: v1.2.2...v1.2.3
v1.2.2
inertia-sails v1.3.2
Bug Fixes
Locals now accessible via locals.xxx in EJS templates (#188)
Fixed a bug where <%= locals.title %> in EJS templates always returned undefined, causing fallback defaults to render instead of dynamic values passed via the locals API (sails.inertia.local(), sails.inertia.localGlobally(), or action-level return { locals: {...} }).
Before: Dynamic locals were silently ignored — the rendered HTML always showed fallback values.
<!-- Always rendered "My App" regardless of what locals were set -->
<title><%= locals.title || 'My App' %></title>After: Dynamic locals correctly render in the EJS template.
<!-- Now correctly renders the dynamic value from the action -->
<title><%= locals.title || 'My App' %></title>Root cause: Sails's default EJS renderer creates an internal options.locals object for template helpers (blocks, layout, partial). EJS compiles templates with with(data) { ... }, which caused locals inside the template to resolve to this internal object rather than the actual view data. The fix pre-populates data.locals with user-provided values so they survive the with scoping.
Full Changelog
- fix(inertia-sails): ensure locals are accessible via
locals.xxxin EJS templates (#189)
Full Changelog: v1.2.1...v1.2.2
v1.2.1
inertia-sails v1.3.1
Security: Shared props no longer leak across requests
This release fixes a critical bug where user-specific shared props could leak between requests, causing one user's data (e.g., loggedInUser) to appear in another user's session — including unauthenticated visitors.
This affects all Boring Stack apps that call sails.inertia.share() in routes.before handlers, which is the standard pattern for passing logged-in user data to the frontend.
The bug
Three issues combined to create a data leak:
-
share()silently fell back to global storage — When called outside AsyncLocalStorage context,share()wrote toglobalSharedProps, a process-level singleton. That data persisted and bled into every subsequent request. -
flushShared()never cleaned global storage — Theglobalparameter defaulted tofalse, so stale data inglobalSharedPropswas never cleared between requests. -
Hook load order created a race condition — inertia-sails set up AsyncLocalStorage context in
routes.before, but other hooks (like the custom hook) also useroutes.beforeto callshare(). If another hook's handler ran first,share()was called before context existed, triggering the global fallback.
The fix
AsyncLocalStorage context is now injected as HTTP middleware during configure(), guaranteeing it exists before any hook's routes.before handler runs.
Request lifecycle (before):
routes.before handlers (race condition!) → router → controller
Request lifecycle (after):
cookieParser → session → ... → inertiaContext → routes.before handlers → router → controller
↑
AsyncLocalStorage context
is ready before any hook
calls share()
The fix uses Sails' configure() lifecycle phase — which runs for all hooks before any hook's initialize() — to inject an inertiaContext middleware into the HTTP stack before the router.
For socket requests (which bypass Express middleware and go through sails.router.route() directly), the existing routes.before handlers remain as a fallback with a guard that skips setup if context was already established by the HTTP middleware.
Breaking changes
share() no longer falls back to global storage
Previously, calling share() outside a request context silently wrote to globalSharedProps. Now it logs a warning and discards the value.
// This still works — called inside routes.before, controllers, helpers, etc.
sails.inertia.share('loggedInUser', user)
// This now logs a warning instead of silently leaking data
// Use shareGlobally() for truly global data like app name
sails.inertia.shareGlobally('appName', 'My App')If you see warnings like share('key') called outside request context, move the call into a routes.before handler, controller, or helper — anywhere inside the request lifecycle.
flushShared() always cleans global storage
The global parameter has been removed. flushShared() now always cleans both request-scoped and global storage to prevent stale data.
// Before
sails.inertia.flushShared('key', true) // had to pass true to clean global
sails.inertia.flushShared() // only cleaned request-scoped
// After
sails.inertia.flushShared('key') // cleans both request-scoped and global
sails.inertia.flushShared() // cleans both request-scoped and globallocal() no longer falls back to global storage
Same change as share() — use localGlobally() for global locals.
How to verify the fix
-
Update inertia-sails:
npm install inertia-sails@1.3.1
-
Start your app and log in as User A in one browser
-
Open the app in a different browser or incognito window (not logged in)
-
Verify the unauthenticated browser does not show User A's avatar, name, or any session data
-
Check server logs — there should be no
share() called outside request contextwarnings under normal operation
Additional fixes
- TypeScript: Fixed
AsyncLocalStorage.run()callback type error — wrapped the callback to satisfy the() => anysignature expected by therun()overload
References
Migration guide
No code changes required in your app. Just update the dependency:
npm install inertia-sails@1.3.1If you were calling flushShared() with the second global argument, remove it — global is now always cleaned:
// Before
sails.inertia.flushShared('key', true)
// After
sails.inertia.flushShared('key')Full Changelog: v1.2.0...v1.2.1
v1.2.0
inertia-sails v1.2.0
Locals — Sails-idiomatic view data
This release replaces the viewData API with locals, aligning inertia-sails with how Sails.js natively handles view data. In Sails, the second argument to res.view() is an object of locals — each key becomes a top-level variable in your EJS template. inertia-sails now works the same way.
What changed
The viewData key in action returns is now locals:
// Before
return {
page: 'courses/show',
props: { course },
viewData: {
title: course.title,
description: course.description,
ogImage: course.thumbnailUrl
}
}
// After
return {
page: 'courses/show',
props: { course },
locals: {
title: course.title,
description: course.description,
ogImage: course.thumbnailUrl
}
}The API methods on sails.inertia have been renamed:
| Before | After |
|---|---|
sails.inertia.viewData(key, value) |
sails.inertia.local(key, value) |
sails.inertia.viewDataGlobally(key, value) |
sails.inertia.localGlobally(key, value) |
sails.inertia.getViewData(key) |
sails.inertia.getLocals(key) |
Locals are now spread directly into res.view()
Previously, all view data was nested under a viewData key, requiring awkward access patterns in EJS:
<!-- Before: nested, requires typeof guard -->
<title><%= typeof viewData !== 'undefined' && viewData.title ? viewData.title : 'My App' %></title>Now locals are spread directly, so each one is a first-class EJS variable. Use the built-in locals object for safe access with fallbacks:
<!-- After: clean, idiomatic EJS -->
<title><%= locals.title || 'My App' %></title>Three ways to set locals
From actions — the most common pattern:
return {
page: 'blog/show',
props: { post },
locals: {
title: `${post.title} | My Blog`,
description: post.excerpt,
ogImage: post.coverImageUrl
}
}From hooks/middleware — for request-scoped values (concurrency-safe via AsyncLocalStorage):
sails.inertia.local('canonicalUrl', `https://myapp.com${req.path}`)Globally — for app-wide defaults set once at boot:
sails.inertia.localGlobally('title', 'My App')
sails.inertia.localGlobally('ogImage', '/images/default-og.png')Locals merge in precedence order: global → request-scoped → action (last wins).
Migration guide
-
Update inertia-sails
npm install inertia-sails@1.2.0
-
Rename
viewDatatolocalsin action returnsFind and replace
viewData:withlocals:in your controllers. -
Rename API calls
Find Replace with sails.inertia.viewData(sails.inertia.local(sails.inertia.viewDataGlobally(sails.inertia.localGlobally(sails.inertia.getViewData(sails.inertia.getLocals( -
Update
views/app.ejsReplace
viewData.xreferences withlocals.x:<!-- Before --> <title><%= typeof viewData !== 'undefined' && viewData.title ? viewData.title : 'My App' %></title> <meta name="description" content="<%= typeof viewData !== 'undefined' && viewData.description ? viewData.description : '' %>" /> <!-- After --> <title><%= locals.title || 'My App' %></title> <meta name="description" content="<%= locals.description || '' %>" />
Breaking changes
viewDatakey in action returns no longer recognized — uselocalssails.inertia.viewData()removed — usesails.inertia.local()sails.inertia.viewDataGlobally()removed — usesails.inertia.localGlobally()sails.inertia.getViewData()removed — usesails.inertia.getLocals()viewDatais no longer passed as a nested object tores.view()— locals are spread directly
Documentation
- Locals (inertia-sails) — full API reference, precedence rules, real-world examples
- Locals (Boring Stack) — practical guide with quick examples
Full Changelog: v1.1.0...v1.2.0
v1.1.0
inertia-sails v1.1.0
What's New
Early AsyncLocalStorage Context for Hooks
The AsyncLocalStorage context is now set up in routes.before instead of in middleware. This means other Sails hooks can now use sails.inertia.share() with proper request-scoped context.
Before this release, if you called sails.inertia.share() from a hook's routes.before, the data would fall back to global storage because hooks run before middleware. This could cause race conditions where data leaked between concurrent requests.
After this release, context is established early, so hook-based sharing works correctly:
// api/hooks/custom/index.js
module.exports = function defineCustomHook(sails) {
return {
routes: {
before: {
'GET /*': {
skipAssets: true,
fn: async function (req, res, next) {
// This now works correctly with request-scoped context!
if (req.session.userId) {
sails.inertia.share('currentUser', await User.findOne(req.session.userId))
} else {
sails.inertia.flushShared('currentUser')
}
return next()
}
}
}
}
}
}Fixed
- Race condition in hooks:
sails.inertia.share()called from hooks now properly uses request-scoped storage instead of falling back to global storage (#180)
Changed
- Middleware simplified to only handle validation errors (context setup moved to
routes.before) - Updated
flushShared()JSDoc to note that context is always available in hooks
Migration
No migration needed. This is a backward-compatible enhancement. Existing code continues to work unchanged.
Full Changelog: v1.0.1...v1.1.0
v1.0.1
The Boring Stack 1.0.1
Patch release with infinite scroll implementation and test infrastructure improvements.
What's Changed
Infinite Scroll Support
Added sails.inertia.scroll() helper for building infinite scroll lists with proper pagination:
Action:
const page = this.req.param('page', 0)
const perPage = 10
const invoices = await Invoice.find({ status }).paginate(page, perPage)
const total = await Invoice.count({ status })
return {
page: 'invoices/index',
props: {
invoices: sails.inertia.scroll(() => invoices, { page, perPage, total })
}
}Vue Component:
<InfiniteScroll :data="invoices" preserve-url>
<Link v-for="invoice in invoices.data" :key="invoice.id" :href="`/invoices/${invoice.id}`">
{{ invoice.name }}
</Link>
<template #loading>
<div class="spinner" />
</template>
</InfiniteScroll>The helper automatically:
- Wraps data with pagination metadata (
current_page,per_page,total,last_page,next_page,prev_page) - Merges new pages with existing data on the client
- Works seamlessly with Inertia's
<InfiniteScroll>component
Test Infrastructure
- Refactored unit tests to use
getSails()singleton pattern for faster test execution - Split helper tests into individual files (
tests/unit/helpers/*.test.js) - Standardized e2e tests to use
.test.jsextension - All templates now have consistent
tests/directory structure:
tests/
├── e2e/
│ └── pages/
│ └── home.test.js
├── unit/
│ └── helpers/
│ ├── capitalize.test.js
│ └── get-user-initials.test.js
└── util/
└── get-sails.js
Bug Fixes
- Fixed duplicate
timeoutproperty in ascent template playwright configs - Updated ascent templates to use
testDir: './tests/e2e'for consistency with mellow templates
Configuration
- Simplified
config/inertia.jsin all templates to empty object (auto-versioning handles the rest)
Full Changelog: v1.0.0...v1.0.1
v1.0.0
The Boring Stack 1.0.0
The most significant release since the project began. This release represents a fundamental architectural shift in how inertia-sails handles request-scoped state, plus a wealth of new features that bring us to feature parity with inertia-laravel.
Highlights
- AsyncLocalStorage - Bulletproof request isolation fixes critical race condition bugs
- Once Props - Intelligent caching reduces database queries across page navigations
- Automatic Asset Versioning - Zero-config asset versioning with Shipwright integration
New Features
AsyncLocalStorage for Request Isolation
All request-scoped APIs now use Node.js's AsyncLocalStorage to prevent data leakage between concurrent requests:
| API | Now Request-Scoped |
|---|---|
share() |
✅ |
viewData() |
✅ |
flash() |
✅ |
encryptHistory() |
✅ |
clearHistory() |
✅ |
setRootView() |
✅ |
refreshOnce() |
✅ |
Once Props
Cache expensive data across page navigations with once():
sails.inertia.share('loggedInUser',
sails.inertia.once(async () => {
return await User.findOne({ id: req.session.userId })
})
)Chainable options:
sails.inertia.once(() => fetchPermissions())
.as('user-permissions') // Custom cache key
.until(3600) // Expire after 1 hour
.fresh(needsRefresh) // Conditionally force refreshFlash Messages
Proper session-based flash messages that don't persist in browser history:
sails.inertia.flash('success', 'Invoice sent!')
sails.inertia.flash({ success: 'Saved!', highlight: 'billing-section' })Infinite Scroll with scroll()
return {
page: 'invoices/index',
props: {
invoices: sails.inertia.scroll(() => invoices, { page, perPage, total })
}
}Deep Merge
Recursively merge nested objects with deepMerge():
settings: sails.inertia.deepMerge(() => updatedSettings)Per-Request Root Views
sails.inertia.setRootView('auth') // Use views/auth.ejsSafe Back Navigation
return sails.inertia.back('/dashboard') // Returns referrer or fallbackAutomatic Asset Versioning
- With Shipwright: Reads
.tmp/public/manifest.jsonand generates MD5 hash - Without Shipwright: Falls back to server startup timestamp
No configuration needed!
Global Sharing
For truly global data (app name, version), use shareGlobally():
sails.inertia.shareGlobally('appName', 'My App')Breaking Changes
share() is Now Request-Scoped
If you were relying on share() persisting across requests, use shareGlobally() instead.
Flash Messages API
Replace this.req.flash() with sails.inertia.flash().
Back Navigation
Replace return 'back' with return sails.inertia.back('/fallback').
Upgrading
1. Update inertia-sails
npm install inertia-sails@latest2. Update Custom Hook
Wrap shared user data with once():
// Before
sails.inertia.share('loggedInUser', user)
// After
sails.inertia.share('loggedInUser',
sails.inertia.once(async () => {
return await User.findOne({ id: req.session.userId })
})
)3. Add refreshOnce() to Update Actions
sails.inertia.refreshOnce('loggedInUser')
sails.inertia.flash('success', 'Profile updated!')4. Copy Updated Responses
Copy the latest serverError.js from the templates to your api/responses/ folder.
Testing Improvements
- Refactored unit tests to use
getSails()singleton pattern - Split helper tests into individual files (
tests/unit/helpers/*.test.js) - Standardized e2e tests to use
.test.jsextension - All templates now have consistent
tests/directory structure
Documentation
Thank You
This release wouldn't be possible without the community. Special thanks to everyone who reported issues, suggested features, and tested early builds.
Full Changelog: v0.9.0...v1.0.0
v0.9.0
What's Changed
- feat: add standardized testing architecture to all templates by @DominusKelvin in #174
- feat: health check 173 and updated Dockerfiles by @DominusKelvin in #175
Full Changelog: v0.8.0...v0.9.0
v0.8.0
What's Changed
- feat: enhance profile and team update functionality with file uploads and improved error handling by @DominusKelvin in #168
- Move templates to Sails Wish 1.0
Full Changelog: v0.7.5...v0.8.0