remix v3.0.0-alpha.4
-
BREAKING CHANGE: Remove the
remix/data-table/sqlexport. ImportSqlStatement,sql, andrawSqlfromremix/data-tableinstead.remix/data-table/sql-helpersremains available for adapter-facing SQL utilities.remix/data-tablenow exports theDatabaseclass as a runtime value. You can construct a database directly withnew Database(adapter, options)or keep usingcreateDatabase(adapter, options), which now delegates to the class constructor.BREAKING CHANGE:
remix/data-tableno longer exportsQueryBuilder. ImportQueryandqueryfromremix/data-table, then execute unbound queries withdb.exec(...).db.exec(...)now accepts only raw SQL orQueryvalues, and unbound terminal methods likefirst(),count(),insert(), andupdate()returnQueryobjects instead of separate command descriptor types.db.query(table)remains available as shorthand and now returns the same boundQueryclass.remix/data-table/migrationsno longer exports a separateDatabasetype alias. ImportDatabasefromremix/data-tablewhen you need the migrationdbtype directly.The incidental
QueryMethodtype export has also been removed; useDatabase['query']orQueryForTable<table>when you need that type shape.Added
package.jsonexports:remix/auth-middlewareto re-export APIs from@remix-run/auth-middlewareremix/authto re-export APIs from@remix-run/auth
-
Add
remix/cors-middlewareto re-export the CORS middleware APIs from@remix-run/cors-middleware. -
Update
remix/componentandremix/component/serverto re-export the latest@remix-run/componentframe-navigation APIs.remix/componentnow exposesnavigate(href, { src, target, history }),link(href, { src, target, history }),run({ loadModule, resolveFrame }), and thehandle.frames.topandhandle.frames.get(name)helpers, whileremix/component/serverre-exports the SSR frame source APIs includingframeSrc,topFrameSrc, andResolveFrameContext. -
Add browser-origin and CSRF protection middleware APIs to
remix.remix/cop-middlewareexposescop(options)for browser-focused cross-origin protection usingSec-Fetch-SitewithOriginfallback, trusted origins, and configurable bypasses.remix/csrf-middlewareexposescsrf(options)andgetCsrfToken(context)for session-backed CSRF tokens plus origin validation.- Apps can use either middleware independently or layer
cop(),session(), andcsrf()together when they want both browser-origin filtering and token-backed protection.
-
Bumped
@remix-run/*dependencies:async-context-middleware@0.2.0auth@0.1.0auth-middleware@0.1.0component@0.6.0compression-middleware@0.1.4cop-middleware@0.1.0cors-middleware@0.1.0csrf-middleware@0.1.0data-schema@0.2.0data-table@0.2.0data-table-mysql@0.2.0data-table-postgres@0.2.0data-table-sqlite@0.2.0fetch-router@0.18.0form-data-middleware@0.2.0form-data-parser@0.16.0logger-middleware@0.1.4method-override-middleware@0.1.5multipart-parser@0.15.0route-pattern@0.20.0session-middleware@0.2.0static-middleware@0.4.5
method-override-middleware v0.1.5
- Bumped
@remix-run/*dependencies:
session-middleware v0.2.0
-
BREAKING CHANGE: Session middleware no longer reads/writes
context.session.Session state is now stored on request context using the
Sessionclass itself as the context key and accessed withcontext.get(Session). -
session()now contributesSessiontofetch-router's typed request context, so apps deriving context from middleware can readcontext.get(Session)without manual type assertions.
- Bumped
@remix-run/*dependencies:
multipart-parser v0.15.0
-
BREAKING CHANGE:
parseMultipart(),parseMultipartStream(), andparseMultipartRequest()now enforce finite defaultmaxPartsandmaxTotalSizelimits, and addMaxPartsExceededErrorandMaxTotalSizeExceededErrorfor handling multipart envelope limit failures.Apps that intentionally accept large multipart requests may need to raise
maxPartsormaxTotalSizeexplicitly.
static-middleware v0.4.5
- Bumped
@remix-run/*dependencies:
logger-middleware v0.1.4
- Bumped
@remix-run/*dependencies:
route-pattern v0.20.0
-
BREAKING CHANGE: Make search param pattern decoding and serialization consistent with
URLSearchParams. Affects:RoutePattern.{match,href,search,ast.search}Previously,
RoutePatterntreated?qand?q=as different constraints:// Before: `?q` and `?q=` are different let url = new URL('https://example.com?q') // Matches "key only" constraint? new RoutePattern('?q').match(url) // ✅ match // Matches "key and value" constraint? new RoutePattern('?q=').match(url) // ❌ no match (`null`) // Different constraints serialized to different strings new RoutePattern('?q').search // -> 'q' new RoutePattern('?q=').search // -> 'q='
There were two main problems with that approach:
Unintuitive matching
// URL search looks like `?q=` let url = new URL('https://example.com?q=') // Pattern search looks like `?q=` let pattern = new RoutePattern('?q=') // But "key and value" constraint doesn't match! pattern.match(url) // ❌ no match (`null`)
Parsing and serialization
For consistency with
URLSearchParams, search param patterns should be parsed according to the WHATWGapplication/x-www-form-urlencodedparsing spec and should also encode spaces as+.Now, we use
URLSearchParamsto parse search param patterns to guarantee decodings are consistent:let url = new URL('https://example.com?q=a+b') // Decodes `+` to ` ` url.searchParams.getAll('q') // -> ['a b'] // Before let pattern = new RoutePattern('?q=a+b') // Does not decode `+` to ` ` pattern.ast.search.get('q') // -> ❌ Set { 'a+b' } // After let pattern = new RoutePattern('?q=a+b') // Decodes `+` to ` ` pattern.ast.search.get('q') // -> ✅ Set { 'a b' }
Similarly, now that
?qand?q=are semantically equivalent, they should serialize to the same thing:new URLSearchParams('q=').toString() // 'q=' // Before new RoutePattern('?q=').search // ❌ 'q' // After new RoutePattern('?q=').search // ✅ 'q='
As a result,
RoutePatterns can no longer represent a "key and any value" constraint. In practice, this was a niche use-case so we chose correctness and consistency withURLSearchParams. If the need for "key and any value" constraints arises, we can later introduce a separate syntax for that without the unintuitive shortcoming of?q=.With "key and any value" constraints removed, the
missing-search-paramerror type thrown byRoutePattern.hrefwas made obsolete and was removed. -
BREAKING CHANGE:
RoutePattern.astis now typed as deeply readonly.This was always the intended design; the type system now reflects it:
// Before pattern.ast = { ...pattern.ast, protocol: 'https' } pattern.ast.protocol = 'https' pattern.ast.port = '443' pattern.ast.hostname = null pattern.ast.pathname = otherPattern.ast.pathname pattern.ast.search.set('q', new Set(['x'])) pattern.ast.pathname.tokens.push({ type: 'text', text: 'x' }) pattern.ast.pathname.optionals.set(0, 1) // After pattern.ast = { ...pattern.ast, protocol: 'https' } // ~~~ // Cannot assign to 'ast' because it is a read-only property. (2703) pattern.ast.protocol = 'https' // ~~~~~~~~~ // Cannot assign to 'protocol' because it is a read-only property. (2540) pattern.ast.port = '443' // ~~~~ // Cannot assign to 'port' because it is a read-only property. (2540) pattern.ast.hostname = null // ~~~~~~~~ // Cannot assign to 'hostname' because it is a read-only property. (2540) pattern.ast.pathname = otherPattern.ast.pathname // ~~~~~~~~ // Cannot assign to 'pathname' because it is a read-only property. (2540) pattern.ast.search.set('q', new Set(['x'])) // ~~~ // Property 'set' does not exist on type 'ReadonlyMap<string, ReadonlySet<string> | null>'. (2339) pattern.ast.pathname.tokens.push({ type: 'text', text: 'x' }) // ~~~~ // Property 'push' does not exist on type 'ReadonlyArray<PartPatternToken>'. (2339) pattern.ast.pathname.optionals.set(0, 1) // ~~~ // Property 'set' does not exist on type 'ReadonlyMap<number, number>'. (2339)
-
Matches return decoded values for params in pathname
let pattern = new RoutePattern('/posts/:slug') let url = new URL('https://blog.example.com/posts/💿') pattern.match(url)?.params.slug // Before -> '%F0%9F%92%BF' // After -> '💿' url = new URL('https://blog.example.com/posts/café-hà-nội') pattern.match(url)?.params.slug // Before -> 'caf%C3%A9-h%C3%A0-n%E1%BB%99i' // After -> 'café-hà-nội' url = new URL('https://blog.example.com/posts/北京') pattern.match(url)?.params.slug // Before -> '%E5%8C%97%E4%BA%AC' // After -> '北京' url = new URL('https://blog.example.com/posts/مرحبا') pattern.match(url)?.params.slug // Before -> '%D9%85%D8%B1%D8%AD%D8%A8%D8%A7' // After -> 'مرحبا'
If you need percent-encoded text again, use
encodeURI:let url = new URL('https://blog.example.com/posts/💿') let slug = pattern.match(url)!.params.slug // -> 💿 encodeURI(slug) // -> '%F0%9F%92%BF'
-
Faster
TrieMatcher.match:O(m·log(m))->O(m)Previously,
TrieMatcher.matchinternally called.matchAll, then sorted the result to find the best match. Formmatching route patterns, this tookO(m·log(m))operations.Now,
TrieMatcher.matchloops over themmatches, keeping track of the best one, resulting inO(n)operations.In our benchmarks, this made our largest workload (~5000 route patterns) 17% faster with negligible or modest improvements to other workloads.
-
Faster type inference for
RoutePattern.href,RoutePattern.match, andParamsReduced type instantiations for parsing param types, resulting in ~2-5x faster in relevant type benchmarks, but varies depending on your route patterns. May fix
"Type instantiation is excessively deep and possibly infinite" (ts2589)for some apps.
async-context-middleware v0.2.0
getContext()can now be typed per app by augmentingAsyncContextTypes, which makesasyncContext()work cleanly with app-specificfetch-routerrequest context contracts.
- Bumped
@remix-run/*dependencies:
form-data-parser v0.16.0
-
BREAKING CHANGE:
parseFormData()now enforces finite default multipartmaxPartsandmaxTotalSizelimits and surfaces multipart limit failures directly instead of treating them as generic parse noise.Apps that intentionally accept large multipart submissions may need to raise these limits explicitly.
- Bumped
@remix-run/*dependencies:
fetch-router v0.18.0
-
BREAKING CHANGE: Action objects now use
handlerinstead ofaction.This applies to the object form accepted by
router.get(...),router.post(...), androuter.map(...), and toBuildActionobject definitions. -
BREAKING CHANGE: Remove
context.storage,context.session,context.sessionStarted,context.formData, andcontext.filesfrom@remix-run/fetch-router, and renamecreateStorageKey(...)tocreateContextKey(...).RequestContextnow provides request-scoped context methods directly (context.get(key),context.set(key, value), andcontext.has(key)), using keys created withcreateContextKey(...)or constructors likeSessionandFormData.Session middleware now stores the request session with
context.set(Session, session), and form-data middleware now stores parsed form data withcontext.set(FormData, formData). Uploaded files are read fromcontext.get(FormData)usingget(...)/getAll(...).RequestContextis now generic over route params and typed context entries (RequestContext<{ id: string }, entries>), and no longer accepts a request-method generic (RequestContext<'GET', ...>). -
BREAKING CHANGE:
router.map()controllers for route maps now require a single shape: an object with anactionsproperty and optionalmiddleware.Migration: Wrap existing controller objects in
actions. Nested route maps must also use nested controllers with{ actions, middleware? }. -
fetch-routernow threads request context types throughRouter,Controller, andBuildAction, and exports helpers likeMiddlewareContext,WithParams,MergeContext, andAnyParamsso apps can derive context contracts from installed middleware.
-
The
Action/BuildActionobject form accepted byrouter.get(...),router.post(...), androuter.map(...)now uses{ handler, middleware? }, so you can omitmiddlewareentirely instead of writingmiddleware: []when you do not need route middleware. -
Bumped
@remix-run/*dependencies: