chore: version packages - #245
Open
github-actions[bot] wants to merge 1 commit into
Open
Conversation
adesege
reviewed
Jul 27, 2026
adesege
left a comment
Contributor
There was a problem hiding this comment.
Benchmark
Details
| Benchmark suite | Current: 4a953f1 | Previous: ccb3f17 | Ratio |
|---|---|---|---|
test/benchmarks/request-response.bench.ts > Request/Response > simple GET - 200 |
36969.844135137144 ops/sec (±1.32%) |
44355.827899388205 ops/sec (±1.52%) |
1.20 |
test/benchmarks/request-response.bench.ts > Request/Response > GET with route params - 200 |
45996.258581652866 ops/sec (±0.62%) |
49077.55133303189 ops/sec (±1.07%) |
1.07 |
test/benchmarks/request-response.bench.ts > Request/Response > POST with JSON body - 201 |
16285.125619036411 ops/sec (±1.18%) |
24127.579263272528 ops/sec (±1.28%) |
1.48 |
test/benchmarks/request-response.bench.ts > Request/Response > POST invalid body - validation error |
4010.3241256510996 ops/sec (±3.11%) |
4332.798367697519 ops/sec (±3.08%) |
1.08 |
test/benchmarks/request-response.bench.ts > Request/Response > GET unknown route - 404 |
5999.544886524008 ops/sec (±1.44%) |
6877.491973422911 ops/sec (±1.38%) |
1.15 |
src/__benchmarks__/application.bench.ts > Application - Bootstrap > constructor only |
369828.7943581282 ops/sec (±0.64%) |
367439.1967778971 ops/sec (±2.58%) |
0.99 |
src/__benchmarks__/application.bench.ts > Application - Bootstrap > full initialize() |
36418.94472465844 ops/sec (±1.20%) |
40916.51636711715 ops/sec (±2.82%) |
1.12 |
src/__benchmarks__/application.bench.ts > Application - Service Resolution > resolve service after bootstrap |
42820.22578676439 ops/sec (±1.37%) |
46221.77899438597 ops/sec (±1.51%) |
1.08 |
src/__benchmarks__/application.bench.ts > Application - Multi-Controller Bootstrap > initialize with 5 controllers (8 routes) |
42603.526163578375 ops/sec (±0.72%) |
47297.584538015675 ops/sec (±0.94%) |
1.11 |
src/__benchmarks__/application.bench.ts > Application - Multi-Controller Bootstrap > resolve service after multi-controller bootstrap |
41885.811932707955 ops/sec (±0.65%) |
46712.398512126 ops/sec (±0.86%) |
1.12 |
src/di/__benchmarks__/container.bench.ts > Container - Registration > register class provider |
4271033.145793519 ops/sec (±1.68%) |
3659523.004609583 ops/sec (±0.26%) |
0.86 |
src/di/__benchmarks__/container.bench.ts > Container - Registration > registerSingleton |
4026860.8724799873 ops/sec (±2.13%) |
3674735.919157303 ops/sec (±0.23%) |
0.91 |
src/di/__benchmarks__/container.bench.ts > Container - Registration > registerValue |
4580004.16800319 ops/sec (±2.24%) |
3643329.0527346027 ops/sec (±0.97%) |
0.80 |
src/di/__benchmarks__/container.bench.ts > Container - Registration > registerFactory |
4670144.692362833 ops/sec (±0.74%) |
3731349.291036125 ops/sec (±0.79%) |
0.80 |
src/di/__benchmarks__/container.bench.ts > Container - Resolution > resolve class token |
1407599.000603626 ops/sec (±0.42%) |
1231183.1036989056 ops/sec (±0.71%) |
0.87 |
src/di/__benchmarks__/container.bench.ts > Container - Resolution > resolve symbol token |
1424788.6749439533 ops/sec (±0.41%) |
1316018.4734202325 ops/sec (±0.72%) |
0.92 |
src/di/__benchmarks__/container.bench.ts > Container - Resolution > resolve value token |
1933069.095325256 ops/sec (±2.28%) |
1783899.5774658315 ops/sec (±0.30%) |
0.92 |
src/di/__benchmarks__/container.bench.ts > Container - Resolution > resolve singleton token |
1352330.2527890466 ops/sec (±0.43%) |
1315930.5814242992 ops/sec (±0.55%) |
0.97 |
src/di/__benchmarks__/container.bench.ts > Container - Resolution > isRegistered check |
2106508.9635955375 ops/sec (±0.20%) |
1806360.778904609 ops/sec (±0.19%) |
0.86 |
src/di/__benchmarks__/container.bench.ts > Container - Conditional Binding > when().use().give().otherwise() |
2462564.419034541 ops/sec (±3.00%) |
1989351.3462046375 ops/sec (±2.25%) |
0.81 |
src/di/__benchmarks__/container.bench.ts > Container - Conditional Binding > when() with cached predicate |
2439603.0290414556 ops/sec (±0.63%) |
2077218.3049918814 ops/sec (±0.64%) |
0.85 |
src/module/__benchmarks__/module-registry.bench.ts > ModuleRegistry - Registration > register single module |
1353256.167691235 ops/sec (±0.62%) |
1318623.2747570868 ops/sec (±0.43%) |
0.97 |
src/module/__benchmarks__/module-registry.bench.ts > ModuleRegistry - Registration > register 3-level module tree |
679841.4819609317 ops/sec (±0.44%) |
674368.8967327109 ops/sec (±0.61%) |
0.99 |
src/module/__benchmarks__/module-registry.bench.ts > ModuleRegistry - Registration > register dynamic module (forRoot) |
1132211.8075242015 ops/sec (±0.39%) |
1033574.8692689876 ops/sec (±0.46%) |
0.91 |
src/module/__benchmarks__/module-registry.bench.ts > ModuleRegistry - Initialization > initialize with lifecycle hooks |
820519.6717920463 ops/sec (±0.57%) |
861264.3797109328 ops/sec (±0.67%) |
1.05 |
src/module/__benchmarks__/module-registry.bench.ts > ModuleRegistry - Collection > getAllControllers |
603021.1461219145 ops/sec (±0.47%) |
601067.2979531069 ops/sec (±0.70%) |
1.00 |
src/module/__benchmarks__/module-registry.bench.ts > ModuleRegistry - Collection > getAllConsumers |
599319.9916095391 ops/sec (±0.44%) |
598005.5897686194 ops/sec (±2.06%) |
1.00 |
src/module/__benchmarks__/module-registry.bench.ts > ModuleRegistry - Collection > getAllJobs |
602324.2990362238 ops/sec (±0.40%) |
606220.5693193878 ops/sec (±0.44%) |
1.01 |
src/router/__benchmarks__/route-registration.bench.ts > RouteRegistration - Configure > register controller with 5 OpenAPI routes |
16893.46751790366 ops/sec (±6.80%) |
20659.201604495036 ops/sec (±7.81%) |
1.22 |
src/router/__benchmarks__/route-registration.bench.ts > RouteRegistration - Configure > register single-route controller |
84336.03405772212 ops/sec (±8.21%) |
87549.06480088776 ops/sec (±9.59%) |
1.04 |
src/router/__benchmarks__/route-registration.bench.ts > RouteRegistration - Configure > register multiple controllers |
17238.784700155928 ops/sec (±6.85%) |
20055.348642388202 ops/sec (±7.48%) |
1.16 |
src/router/__benchmarks__/route-registration.bench.ts > Route Sorting > sort 10 routes by specificity |
334083.91447458765 ops/sec (±1.72%) |
348297.84898624144 ops/sec (±1.02%) |
1.04 |
src/router/__benchmarks__/route-registration.bench.ts > Route Sorting > sort 50 routes by specificity |
63716.075009950684 ops/sec (±0.38%) |
66342.982298651 ops/sec (±0.49%) |
1.04 |
src/router/__benchmarks__/route-registration.bench.ts > Route Sorting > sort 100 routes by specificity |
30807.01343620418 ops/sec (±0.38%) |
32949.193140155745 ops/sec (±0.54%) |
1.07 |
src/router/__benchmarks__/route-registration.bench.ts > Param Extraction > extractParamNames - static path |
12304712.0558419 ops/sec (±0.13%) |
12503091.774915366 ops/sec (±0.09%) |
1.02 |
src/router/__benchmarks__/route-registration.bench.ts > Param Extraction > extractParamNames - single param |
1801191.7982674923 ops/sec (±0.44%) |
2209847.5226742206 ops/sec (±1.46%) |
1.23 |
src/router/__benchmarks__/route-registration.bench.ts > Param Extraction > extractParamNames - multiple params |
1295222.9353269946 ops/sec (±0.65%) |
1332989.981337998 ops/sec (±0.60%) |
1.03 |
This comment was automatically generated by workflow using github-action-benchmark.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
stratal@0.1.0
Minor Changes
ccb3f17: Move routing onto plain Hono with lazy OpenAPI generation, add declarative response caching on Cloudflare Workers Caching, and add per-path locale detection.
@hono/zod-openapiand@asteasolutions/zod-to-openapi, and move the validation surface tozod/mini. A minimal no-schema worker previously shipped around 599 KB of zod and OpenAPI tooling because every app extended an OpenAPI-aware Hono app and every route registered through it; on a hello-world worker the bundle drops 944 KB to 504 KB raw, and the route-registration chunk 599 KB to 44 KB.params,query, orbody, so schema-less routes pull in no zod at all.ctx.param(),ctx.query()andctx.body()are unchanged.OpenAPIService.getSpec()— see Breaking Changes below.Cache-Controlheader on every response, and add declarative HTTP response caching through the newstratal/response-cacheentry. On a cache hit the Worker never runs, so no CPU is billed.@Cacheableare stampedCache-Control: private, no-store. Cloudflare Workers Caching applies RFC 9111 heuristic freshness, so a response with noCache-Controlat all is cached anyway — a200for two hours, a404for three minutes. Routes that already set their ownCache-Controlare left alone. If you relied on a response having noCache-Controlheader, set one explicitly.@Cacheable({ ttl, swr, tags, vary })forGETandHEADroutes, which emitsCache-Control: public, max-age=…[, stale-while-revalidate=…]plusCache-Tag.@PurgesCache({ tags, pathPrefixes, purgeEverything })for mutations, which purges after a2xxor3xx. The purge is awaited, and a failure is logged with the responsible route before being rethrown asCachePurgeError, rather than leaving the cache silently inconsistent with the database.ResponseCacheModule.forRoot({ defaults })to supplyttl,swrandvaryfor every@Cacheableroute.@Cacheablestays mandatory — defaults never make a route cacheable on their own. New errors:ResponseCacheConfigError,CachePurgeError,InvalidCacheTagError.{param.*},{query.*}and{data.*}into cache tags, with a.*suffix fanning an array out to one tag per element. A rendered tag must be printable ASCII with no space, comma or double quote, and at most 1024 bytes, or it throwsInvalidCacheTagError— commas and quotes are structural in theCache-Tagheader, so constrain or slugify any request-derived value before interpolating it. A{param.*}tag naming a segment the route does not declare is rejected at boot."cache": { "enabled": true }inwrangler.jsonc, Wrangler 4.69.0 or newer, and acompatibility_dateof2026-07-06or later. Without those, an app with cache decorators fails on its first request rather than silently not caching.@Cacheable({ partitionBy: [...] })now works. PartitionedGETandHEADreads are forwarded to a cached entrypoint, which places the resolved partitions in the part of the Workers Caching key that cannot be bypassed.cachedEntrypoint(stratal)fromstratal/workersalongside your default export, then configureResponseCacheModule.forRoot({ gateway: { entrypoint: 'Cached' }, primers, partitions }).partitionBy,partitionsandprimersthrow at boot whengateway.entrypointis absent — a partition an app cannot honour must fail loudly rather than cache per-caller data publicly. A guarded route is only ever cacheable with a non-emptypartitionBy;@Cacheableon a guarded route without one is a boot error, since a guarded response differs per caller. Anything that is not a partitioned read runs inline exactly as before, and a partition that fails to resolve runs inline and is stampedprivate, no-store.@PurgesCacheissues its purge over RPC to the cached entrypoint when running as the gateway, because mutations run inline in the gateway, whose cache is disabled, so an inline purge would report success and invalidate nothing.gateway.entrypointis type-checked against your Worker's exports. Once you have runwrangler types, only your real, non-defaultexport names are accepted, so a typo is a compile error rather than a runtime surprise. Without generated types it stays a plain string and is validated at runtime.detectionaccepts a(path) => optionsresolver, alongside the newI18nModule.forRootAsyncand a strategy-awarectx.setLocale. Different areas can now use different strategies — for example a path-localized public site with a cookie-localized/adminpanel — which is necessary when an area's session cookie is path-scoped.strategy: 'path'get a/:localevariant; everything else is served at its bare path and emits locale-less URLs, with no changes needed in URL builders.localecookie, now scoped by the resolvedcookieOptions, so a per-path cookie area writes{ path: '/admin' }instead of the defaultPath=/. Plainstrategy: 'cookie'behaviour is unchanged.ctx.setLocale(locale)overrides the locale for the current request only; persistence stays the detection layer's job.I18nModule.forRootAsync,LocaleUrlService.isPathLocalized(path),LocalePathService.isPathLocalized(path),LocalePathService.detectionFor(path),resolveDetectionForPath(), theDetectionResolver,DetectionConfigandResolvedDetectiontypes, and theLOCALE_COOKIEconstant.Content-Typeback withContent-Disposition: inlineon every disk. Because objects are served from the same origin as the application, an object stored astext/html, or as a scriptableimage/svg+xml, executed against whatever session fetched it. A signed URL does not help here: it controls who may fetch an object, not what the browser does with the bytes.application/pdf,image/png,image/jpeg,image/gifandimage/webprender inline. Everything else is returned asapplication/octet-streamwithContent-Disposition: attachment. The allowlist is the safe set rather than a blocklist of dangerous types, so a format nobody anticipated fails closed. This is a behaviour change if you relied on a non-allowlisted type rendering in the browser — it now downloads instead.X-Content-Type-Options: nosniff, which stops the browser sniffing past the content type to render a disguised payload, andContent-Security-Policy: sandbox; default-src 'none', so even an allowlisted file handled by a viewer or decoder gets an opaque origin with no scripting.#or?— most user-supplied filenames — being reported as missing, and stop a key containing a control character from producing a malformedContent-Dispositionheader. Non-ASCII filenames are preserved.groups.@Controllerand route options accept agroups: string[]label list; controller groups apply to every route, and route-level groups are appended. Resolved groups are exposed on each route's schema metadata asRouteSchemaMeta.groups, so the OpenAPIrouteFiltercan scope the document by group instead of by path string. AddsgetControllerGroups()for reading a controller's declared groups.describe()andnamed(), not just a description string. Pass an object to setexample,examples,title,deprecatedand more, all of which flow through to the generated OpenAPI document. A field's location — path, query or body — is still derived from its request slot, so there is no per-fieldin. Adds theSchemaMetaandSchemaMetaInputtypes.Responsereturned by a short-circuiting middleware even when an outer middleware forwards control withawait next()and discards the result. A middleware that returns early withctx.redirect(...)or any otherResponsepreviously had it silently dropped, leaving the request unfinalized and throwing "Context is not finalized". This applies both to chained middlewares and to separately registeredrouter.usechains. TheNexttype is widened to() => Promise<Response | void>so a forwarding middleware canreturn next()to propagate a downstream short-circuit without an unsafe cast; middlewares thatawait next()or ignore its result are unaffected./fr/auth/loginmatched the localized index route instead of its intended route, which could produce a redirect loop on a homepage that redirects elsewhere.ApplicationErrorgains an overridablereportContext()hook whose return value is merged into the logged data, so an error type can surface diagnostic detail to observability without a customreportable()callback. The reserved keysmessage,name,stackandtimestampcannot be overridden, and globally registered context still takes precedence.SchemaValidationErroruses this to log which field failed validation and why, where previously a failed request logged only a generic "Schema validation failed" line./openapi.jsonfailing when a route schema contains a type with no JSON Schema representation, such asz.custom,z.transform,z.instanceof,z.date,z.maporz.set. Those types now emit an empty "any" schema instead of throwing, so a single unrepresentable field no longer takes down the entire document.openapi3-tsas a direct dependency. It was previously resolved only transitively, so once the transitive provider was removed a clean install such as CI could not resolve it, breaking typecheck and build.@hono/zod-openapiruntime dependency, trimming the install footprint and removing a stale transitive zod surface.inertia:devshow progress live. Commands run inside a worker viaquarry.call()are unaffected, and their output is still returned in the command result.process.envinto Quarry's worker vars and secrets, so config passed through the environment resolves like any other binding. Local runs with a.dev.varsare unchanged, while CI and scripted runs that pass config through the environment — for example a deploy build supplying secrets as env vars — no longer fail config validation on a missing binding.The Workers runtime failed to starton a worker that declares a Cloudflare Workflow. The CLI host cannot own a workflow entrypoint, and cannot reach one defined in another worker in local development either, so workflow bindings are now stripped from the host and logged. Trigger workflows from the worker that defines them, through an HTTP or queue handler, rather than from the CLI host.finallyblock threw and masked the real result, and opportunistic TLS could not continue on the upgraded socket. Sending mail through the CLI was the common path affected.Breaking Changes
zod/mini. Thezre-export fromstratal/validationis removed — it only existed to share a single zod instance with the old OpenAPI integration. Import schema builders directly fromzod/miniusing named imports, e.g.import { object, string, optional } from 'zod/mini', and replace classic chaining with the functional API:z.string().min(1).optional()becomesoptional(string().check(minLength(1))).stratal/validationstill exportscuid2andwithZodI18n, plus the newdescribe()andnamed()helpers for attaching descriptions and OpenAPI component ids, sincezod/minihas no.describe()or.meta().OpenAPIService.getSpec()becomesgetSpec(container)and is async — update any direct call. TherouteFilteroption is now a metadata predicate(route: RouteSchemaMeta) => booleaninstead of(path, pathItem); filter onroute.groupsorroute.metarather than on the path string.CacheService.putis now fire-and-forget and can no longer report failure. It schedules the KV write throughwaitUntil, returns a promise that resolves immediately, and logs a rejected write instead of throwing — sotry { await cache.put(...) } catch { … }now sees success even when the value was never stored. KV reads are edge-cached but writes commit to KV's central store and can add hundreds of milliseconds to the request, and a cache is best-effort and eventually consistent, so this is the right default for cache writes; but any write that must not be silently lost has to move to the newCacheService.putDurable/TieredCacheService.putDurable, which await the write and throw on failure. Queue idempotency claims and failed-job records already use them, since deferring those would risk double-processing and silently lost failures. Every remaining write is now non-blocking, including the KV-backed rate limiter, which writes its counter through the same path.deleteis unchanged and remains durable and awaited: invalidations such as logout or permission busting must not be deferred.Cache-Controlheader. Routes without@Cacheableare stampedprivate, no-store. If you relied on a response having noCache-Controlat all, set one explicitly in the handler or a middleware — those are left untouched.application/pdf,image/png,image/jpeg,image/gifandimage/webprender inline; everything else downloads as an attachment. If you relied on another type rendering in the browser, serve that content from a separate origin, where a compromise cannot reach the application's session.@stratal/framework@0.1.0
Minor Changes
ccb3f17: Share permissions with the client for Inertia access control, add a Workers-safe database pool factory, and fix role lookups against a renamed user model.
accessControlis configured, so the client can gate on them. This backs the<Can>,<Cannot>,<HasRole>and<HasNoRole>components and theuseCan,useRoleanduseAccesshooks in@stratal/inertia, with permission strings and role names type-checked against a generated registry.createPoolFactory(env, makePool)to@stratal/framework/database, which builds the lazy pool factory a connection'sdialecthands to its dialect instance, choosing connection topology from the environment instead of hard-coding it. Writeconst pool = createPoolFactory(env, () => new Pool(config)), thendialect: () => new PostgresDialect({ pool }).STRATAL_DB_SHARED_POOLis set, it instead memoizes one pool per connection, and tears that pool down exactly once no matter how many clients disconnect.@stratal/testingsets the flag automatically, because the harness runs against a direct Postgres with no Hyperdrive to multiplex — a fresh pool per resolution would accumulate until parallel test files exhausted the server's connection limit. One shared pool per connection mirrors what Hyperdrive does in production and is safe because the pool holds no per-instance state. Dev and production are unaffected.AUTH_GATEWAY_PRIMERS, exported from@stratal/framework/auth, so guarded and per-tenant routes can use@Cacheable({ partitionBy: [...] }). The response-cache gateway resolves partitions outside the app's middleware chain, so a resolver callingctx.user()would otherwise throwUserNotAuthenticatedErroron every request; pass the constant asprimersalongsidegateway: { entrypoint }to runSessionVerificationMiddlewarefirst:ResponseCacheModule.forRoot({ gateway: { entrypoint: 'Cached' }, primers: AUTH_GATEWAY_PRIMERS, partitions: { user: (ctx) => ctx.user().id } }).AUTH_GATEWAY_PRIMERSis areadonlytuple, andprimersaccepts it directly — no need to spread it into a new array. Partitioned reads are then forwarded to the cached entrypoint, and a partition that fails to resolve runs inline and is stampedprivate, no-storerather than being cached publicly. On a cache miss the session lookup is paid twice, once in the gateway and once in the app's own chain; on a hit the app never runs, so only the gateway's lookup is paid.zod/minivalidation surface. Because this package re-exports the core routing and validation surface, the same migration applies — see Breaking Changes below.User. Setting a user's role, reading another user's roles, checking a permission and listing a user's permissions all threw when the model resolved to a different accessor, such as a pluralizedUsersmodel. Role lookups now resolve the user model through Better Auth regardless of ORM naming, and changing a role refreshes that user's sessions so it takes effect immediately.Breaking Changes
zod/mini. Thezre-export is gone from the validation surface this package re-exports. Import schema builders directly fromzod/miniusing named imports, and replace classic chaining with the functional API:z.string().min(1).optional()becomesoptional(string().check(minLength(1))). Usedescribe()andnamed()fromstratal/validationfor descriptions and OpenAPI component ids, sincezod/minihas no.describe()or.meta().OpenAPIService.getSpec()becomesgetSpec(container)and is async, androuteFilteris now a metadata predicate(route: RouteSchemaMeta) => booleaninstead of(path, pathItem).@stratal/inertia@0.1.0
Minor Changes
ccb3f17: Add build-time SSR exclusion and client-side access control, and fix several dev-runtime failures and oversized generated types.
stratalInertia()Vite plugin'sssrExcludeoption, and remove the runtime SSR opt-out. Client-only pages and their heavy dependencies were previously always bundled into the worker, because the SSR page glob pulled in every page, inflating cold start; disabling SSR at runtime skipped rendering but still shipped the code.stratalInertia({ ssrExclude: ['Admin/**', 'Reports/Heavy'] })takes page-component globs, matched against the page name, where*is a single segment and**any number. Excluded pages are dropped from the worker bundle and rendered client-only, while the browser bundle still includes them so they hydrate normally.ssr.disabledandctx.withoutSsr()— see Breaking Changes below.<Can>,<Cannot>,<HasRole>and<HasNoRole>components plus theuseCan,useRoleanduseAccesshooks, on a new@stratal/inertia/react/accessentry. They are gated on permissions the server shares automatically onceaccessControlis configured, and permission strings and role names are type-checked against a generated registry.ctx.share()calls were not detected at all, and shared props wrapped inalways(),defer(),optional(),merge()oronce()were typed as the wrapper instead of the value it resolves to.quarry inertia:devnow keeps the dev server alive, with a default threshold of 900 MB configurable through--heap-limit=<MB>. Supervision runs on macOS and Linux; elsewhere it is disabled with a warning.Cache-Controlheader and@Cacheableis available. A page is not cached when it carries flash data, is a partial reload, or contains aonce()prop. On a cache hit the SSR render is skipped entirely, so a cached page costs no render.zod/minivalidation surface. Because this package re-exports the core routing and validation surface, the same migration applies — see Breaking Changes below.ssrExclude, and share that decision with full-page renders. A direct visit or refresh of such a modal route previously failed withPage not foundand a 500, because the combined page was always rendered through SSR instead of honouring the exclusion.DocumentRendererService, registered under the newINERTIA_TOKENS.DocumentRenderertoken, which renders a builtPageinto an HTML documentResponseand owns the single decision between streaming SSR and a client-only shell — SSR is skipped when it is unconfigured, or when the page component was build-time excluded throughssrExclude.InertiaServiceand@stratal/inertia-modalboth delegate to it, so that rule lives in one place; anything rendering an Inertia document outside those paths should inject the token rather than duplicate the branch.import.meta.globpage resolvers that pass a second argument, such as{ eager: true }or{ import: 'default' }, preserving those options. Only the bare single-argument form was matched before, so option-bearing resolvers silently shipped excluded pages into the worker bundle.renderToStringandrenderToStaticMarkupare not available in the worker.ReferenceError: require is not definedreturning a 500 on every SSR page under the Workers dev and SSR runtime. React 19's server entry is a CommonJS shim whose conditional require is only resolved by Vite's dependency optimizer, and because this package is excluded from that optimizer to avoid duplicate framework instances, the shim was never converted and its barerequirereached the worker runtime.ReferenceError: require is not definedandmodule is not definedunder the Workers dev and SSR runtime when an app uses the ORM data layer (@zenstackhq/orm) or the email renderer (@react-email/render). Both reach CommonJS sub-dependencies through packages excluded from Vite's optimizer, so they were never converted to ESM. Each is optional and is only included when it resolves from the project.createPoolFactory is not a functionunder a linked or portal checkout, by excluding@stratal/frameworkfrom Vite's dependency optimizer alongside@stratal/inertiaandstratal. The optimized database subpath lost its named exports; because the framework also re-exports the core DI tokens and Hono surface, pre-bundling it while the core is excluded could split them into two copies as well.InertiaTranslationKeys | null, no longer defeat detection; thenullorundefinedmember is stripped for matching and re-attached on the emitted reference.MessageKeysfromstratal/i18nrather than being widened to the prefix-filteredInertiaTranslationKeys.Breaking Changes
ssr.disabledis removed fromInertiaModule.forRoot({ ssr }). Replace it with the Vite plugin'sssrExclude, which both skips SSR and drops the excluded pages from the worker bundle:stratalInertia({ ssrExclude: ['Admin/**'] }).ctx.withoutSsr()and thewithoutSsrcontext variable are removed. SSR exclusion is now build-time and declarative, so there is no per-request runtime opt-out — move the decision intossrExclude.zod/mini. Thezre-export is gone from the validation surface this package re-exports. Import schema builders directly fromzod/miniusing named imports, and replace classic chaining with the functional API:z.string().min(1).optional()becomesoptional(string().check(minLength(1))). Usedescribe()andnamed()fromstratal/validationfor descriptions and OpenAPI component ids, sincezod/minihas no.describe()or.meta().OpenAPIService.getSpec()becomesgetSpec(container)and is async, androuteFilteris now a metadata predicate(route: RouteSchemaMeta) => booleaninstead of(path, pathItem).@stratal/testing@0.1.0
Minor Changes
ccb3f17: Give each test file its own database, drain deferred work before a test finishes, and supply the cache and gateway bindings the runtime never populates.
truncateDbor the reset engine.createTestDatabaseGlobalSetupaccepts a one-timepreparehook to bake expensive baseline state, such as seed data or a default tenant schema, into the template once, so every file's database inherits it through the clone instead of rebuilding it per test.truncateDb(name?, opts?)accepts aResetOptionspreserve-list; the migration tables matching_prisma%are always preserved.ctx.cachebinding so cache-decorated routes are testable with no configuration. Neither Miniflare nor workerd ever populates it, so without this a single@Cacheableor@PurgesCacheroute would fail an app's entire suite on the first request.Test.createTestingModule()installs a stub by default:@Cacheableroutes return realCache-ControlandCache-Tagheaders, and purges succeed, recording eachPurgeSpecin call order onmodule.cache.purges. Passcache: falseto opt back into the unconfigured runtime, for example to test the configuration boot guard.ctx.exportsstub by default so adopting the response-cache gateway does not break existing suites. Assert forwarded requests and their resolved partitions throughmodule.gateway.loopbacks. The stub answers to any export name, because it cannot know yours, so a passing suite is not what proves your configured entrypoint is correct — the type check against your Worker's exports is. A wrong name otherwise surfaces on the first request after deploy, as aResponseCacheConfigErrornaming the exports it can actually see.ctx.waitUntilbeforefetch()resolves, mirroring the Workers runtime, which keeps a request alive until its deferred promises settle. A non-blocking listener's deferred side-effect, such as a database write, previously stayed in flight past the response and could still be running against a shared resource at the next request or at teardown, where disposing that resource hung the suite past the hook timeout. Deferred work now completes within the request that triggered it, andwaitUntilsemantics are otherwise unchanged.close()before tearing the app down.fetch()already drained per call, but the non-HTTP helpers for websockets, SSE and Quarry share the same queue, so a suite using only those could reach teardown with database writes still in flight and race the connection pool's disposal. Shutdown is now deterministic regardless of which helper enqueued the work.databaseturns on real file parallelism, and each file's setup clones the template into its own database — aCREATE DATABASE … TEMPLATEserialized across concurrent files by a Postgres advisory lock — on top of whatever the app provisions in its ownbeforeAll, such as a tenant or seed data. Under a full worker slot that routinely exceeds Vitest's 10 second default and fails with "Hook timed out in 10000ms" even though the work would have completed. This is a floor, not a ceiling: a project with heavier setup can still raisehookTimeoutfor its own suites.ReadableStream is disturbed; it is now consumed exactly once.Breaking Changes
sharedanddatabaseisolation toggle is gone, along with theisolationoption on bothstratalTest({ database })andcreateTestDatabaseGlobalSetup. PassstratalTest({ database: {} })to enable isolation and delete anyisolation:option;globalSetupno longer takes an isolation mode.createTestDatabaseGlobalSetupnow requiresschema. Add it if you were relying on the previous default.createDatabaseFromTemplate,deriveDbNameanddropDatabaseare removed. Per-file databases are created and reclaimed automatically, so remove any manual calls; usetruncateDbto reset state between tests within a file.@stratal/feature-flags@0.1.0
Patch Changes
ccb3f17: Released alongside the rest of the packages; nothing changed in this one.
@stratal/feature-flagsis republished at the same version as the packages it builds on rather than being left behind at the previous one. Its peer ranges onstrataland@stratal/inertiaare open-ended, so an existing install keeps resolving — upgrade only to keep one aligned set of versions across the framework.@stratal/inertia-modal@0.1.0
Patch Changes
ccb3f17: Render a modal route's background page client-only when that page is excluded from SSR.
stratalInertia({ ssrExclude }). A direct visit or refresh of such a modal route previously failed withPage not foundand a 500, because the combined page was always rendered through SSR instead of honouring the same exclusion as a full-page render. The excluded page now renders client-only for the browser bundle to hydrate, so a modal route works under both SSR and client-side rendering.