drizzle-team / drizzle-team/drizzle-orm
applyMixins throws TypeError on Hermes (Expo SDK 53+ default-flip of `unstable_enablePackageExports`)
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
### Summary
`drizzle-orm@1.0.0-rc.2` (and `0.45.x`) crashes at module-init on Hermes/Metro/Expo SDK 53+ with `TypeError: Cannot read property 'prototype' of undefined` inside `applyMixins(SQLiteSelectBase, [QueryPromise])`. The crash silences the entire app boot — no useful stack trace surfaces in production.
This is the same canonical class as expo/expo#36551, expo/expo#36588, and Firebase JS Auth #9020 — all rooted in the SDK 53+ default-flip of `unstable_enablePackageExports = true` causing Metro to pick the ESM `import` condition, which makes eager named imports fire mixin composition before the dependency module finishes initializing.
A defensive guard in `applyMixins` would convert the silent crash into a diagnostic error pointing developers at the right substrate boundary. Precedent: #1594 (Dec 2023, commit `a4d758d`) added similar Hermes-targeted hardening (the `if (name === 'constructor') continue;` skip).
### Reproduction
- Expo SDK: 55 (also affects 53, 54)
- React Native: 0.83.4
- Hermes: enabled
- drizzle-orm: 1.0.0-rc.2 (also 0.45.2; `applyMixins` source byte-identical across both)
- Pattern: any consumer SDK that re-exports drizzle classes from a sub-path entry (e.g. `import { sqliteTable } from "drizzle-orm/sqlite-core"`)
- Trigger: cold app boot on Hermes (does NOT reproduce on Bun/Node/web)
### Stack trace (representative)
```
TypeError: Cannot read property 'prototype' of undefined
at applyMixins (drizzle-orm/utils.js)
at : SQLiteSelectBase
```
Bundle inspection of the failing call site:
```js
(0, _utilsJs2.applyMixins)(SQLiteSelectBase, [_queryPromiseJs.QueryPromise]);
```
Per the canonical `applyMixins(baseClass, extendedClasses)` signature in `drizzle-orm/src/utils.ts:181`: `SQLiteSelectBase` is the baseClass, `[QueryPromise]` is the extendedClasses array. `_queryPromiseJs.QueryPromise` is direct member-access (NOT a lazy `Object.defineProperty` getter), so the value resolves at module-init time before `query-promise.js` has finished evaluating — `extendedClass` is `undefined`, `.prototype` access throws.
### Root cause
Expo SDK 53+ default-flipped Metro's `unstable_enablePackageExports = true`. Metro picks drizzle's ESM `import` condition (which uses eager named imports). Eager imports cause `applyMixins(SQLiteSelectBase, [QueryPromise])` to run at the importing module's init, which on Hermes happens before `QueryPromise`'s own module has finished `module.exports` assignment.
### Suggested defensive patch
Additive defensive coding on top of canon `drizzle-orm/src/utils.ts:181`. Preserves the `if (name === 'constructor') continue;` skip from #1594. Matches canon's `||` fallback (no `??` drift):
```ts
export function applyMixins(baseClass: any, extendedClasses: any[]) {
if (!baseClass?.prototype) {
throw new Error(
`applyMixins: baseClass undefined or missing prototype — likely circular import or eager-resolution race.`,
);
}
for (const extendedClass of extendedClasses) {
if (!extendedClass?.prototype) {
throw new Error(
`applyMixins: extendedClass at index ${extendedClasses.indexOf(extendedClass)} undefined or missing prototype — likely circular import or eager-resolution race.`,
);
}
for (const name of Object.getOwnPropertyNames(extendedClass.prototype)) {
if (name === 'constructor') continue;
Object.defineProperty(
baseClass.prototype,
name,
Object.getOwnPropertyDescriptor(extendedClass.prototype, name) || Object.create(null),
);
}
}
}
```
This converts the silent prototype-undefined crash into an explicit, diagnostic error message that points the developer at the correct substrate boundary (circular import or eager-resolution race). The actual fix (avoiding the eager resolution) needs deeper RCA in drizzle's source, but the diagnostic is independent of that work and immediately useful.
### Workarounds known to consumers
1. Per-package Metro `resolveRequest` override forcing CJS condition (NO-OP if the package's exports map is condition-symmetric — empirically the case for drizzle-orm@1.0.0-rc.2 in some bun monorepo layouts).
2. Global `unstable_enablePackageExports = false` (sledgehammer; breaks any package that requires the new exports semantics).
3. Vendor drizzle into the consumer's SDK bundle at SDK-build-time (eliminates runtime resolution variance entirely; this is what we shipped in `@kaosmaps/nano-mobile@0.0.50-rc.5`).
### References
- expo/expo#36551 (canonical class for ESM-resolved-package-on-Hermes crashes)
- expo/expo#36588 (same class, different package)
- firebase/firebase-js-sdk#9020 (Firebase Auth, same class)
- drizzle-team/drizzle-orm#1594 (precedent: Dec 2023 Hermes-targeted fix, commit `a4d758d`)
### What we'd like
- A defensive guard would help future consumers hit a diagnostic error instead of a silent crash.
- Optional: deeper investigation of the eager-resolution path (lazy `Object.defineProperty` getters in CJS variant?).
- Happy to PR the defensive guard if it would land faster — let us know your preference for additive-diff vs full-replacement shape.
Contributor guide
Assessment
This issue has not been assessed yet.