Skip to content

Releases: sailscastshq/boring-stack

The Boring Stack 1.5.0

28 May 09:49

Choose a tag to compare

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-sails and the Vue, React, and Svelte templates.
  • New rsbuild-plugin-inertia package 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 badRequest flow.
  • 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, and 503.
  • 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, and inertia-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:

  • sharedProps
  • clearHistory
  • encryptHistory
  • deferredProps
  • mergeProps
  • prependProps
  • deepMergeProps
  • matchPropsOn
  • scrollProps
  • rescuedProps
  • preserveFragment

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 pages shorthand for non-standard directories
  • lazy page loading by default for page-level code splitting
  • lazy: false for single-bundle apps
  • Vue, React, and Svelte adapter detection
  • automatic SSR build environment setup when assets/js/ssr.js exists
  • 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.vue
  • assets/js/pages/error.jsx
  • assets/js/pages/error.svelte

inertia-sails renders that page by default for:

  • 403
  • 404
  • 500
  • 503

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 axios

React:

npm install inertia-sails@^1.5.0 @inertiajs/react@^3.1.1 rsbuild-plugin-inertia@latest sails-hook-shipwright@^1.4.0
npm uninstall axios

Svelte:

npm install inertia-sails@^1.5.0 @inertiajs/svelte@^3.1.1 rsbuild-plugin-inertia@latest sails-hook-shipwright@^1.4.0
npm uninstall axios

Keep 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...

Read more

v1.2.3

06 Mar 12:59

Choose a tag to compare

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

02 Mar 17:47

Choose a tag to compare

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.xxx in EJS templates (#189)

Full Changelog: v1.2.1...v1.2.2

v1.2.1

27 Feb 10:36

Choose a tag to compare

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:

  1. share() silently fell back to global storage — When called outside AsyncLocalStorage context, share() wrote to globalSharedProps, a process-level singleton. That data persisted and bled into every subsequent request.

  2. flushShared() never cleaned global storage — The global parameter defaulted to false, so stale data in globalSharedProps was never cleared between requests.

  3. Hook load order created a race condition — inertia-sails set up AsyncLocalStorage context in routes.before, but other hooks (like the custom hook) also use routes.before to call share(). 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 global

local() no longer falls back to global storage

Same change as share() — use localGlobally() for global locals.

How to verify the fix

  1. Update inertia-sails:

    npm install inertia-sails@1.3.1
  2. Start your app and log in as User A in one browser

  3. Open the app in a different browser or incognito window (not logged in)

  4. Verify the unauthenticated browser does not show User A's avatar, name, or any session data

  5. Check server logs — there should be no share() called outside request context warnings under normal operation

Additional fixes

  • TypeScript: Fixed AsyncLocalStorage.run() callback type error — wrapped the callback to satisfy the () => any signature expected by the run() overload

References

Migration guide

No code changes required in your app. Just update the dependency:

npm install inertia-sails@1.3.1

If 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

22 Feb 03:45

Choose a tag to compare

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

  1. Update inertia-sails

    npm install inertia-sails@1.2.0
  2. Rename viewData to locals in action returns

    Find and replace viewData: with locals: in your controllers.

  3. Rename API calls

    Find Replace with
    sails.inertia.viewData( sails.inertia.local(
    sails.inertia.viewDataGlobally( sails.inertia.localGlobally(
    sails.inertia.getViewData( sails.inertia.getLocals(
  4. Update views/app.ejs

    Replace viewData.x references with locals.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

  • viewData key in action returns no longer recognized — use locals
  • sails.inertia.viewData() removed — use sails.inertia.local()
  • sails.inertia.viewDataGlobally() removed — use sails.inertia.localGlobally()
  • sails.inertia.getViewData() removed — use sails.inertia.getLocals()
  • viewData is no longer passed as a nested object to res.view() — locals are spread directly

Documentation

Full Changelog: v1.1.0...v1.2.0

v1.1.0

15 Jan 16:35

Choose a tag to compare

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

15 Jan 10:38

Choose a tag to compare

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.js extension
  • 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 timeout property in ascent template playwright configs
  • Updated ascent templates to use testDir: './tests/e2e' for consistency with mellow templates

Configuration

  • Simplified config/inertia.js in all templates to empty object (auto-versioning handles the rest)

Full Changelog: v1.0.0...v1.0.1

v1.0.0

15 Jan 02:33

Choose a tag to compare

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 refresh

Flash 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.ejs

Safe Back Navigation

return sails.inertia.back('/dashboard')  // Returns referrer or fallback

Automatic Asset Versioning

  • With Shipwright: Reads .tmp/public/manifest.json and 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@latest

2. 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.js extension
  • 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

08 Jan 14:12

Choose a tag to compare

What's Changed

Full Changelog: v0.8.0...v0.9.0

v0.8.0

07 Jan 21:20

Choose a tag to compare

What's Changed

Full Changelog: v0.7.5...v0.8.0