Skip to content

fix(better-auth): keep schema-generator import lazy in CJS output#2658

Merged
ymc9 merged 2 commits into
devfrom
fix/2646-better-auth-eager-langium-load
May 8, 2026
Merged

fix(better-auth): keep schema-generator import lazy in CJS output#2658
ymc9 merged 2 commits into
devfrom
fix/2646-better-auth-eager-langium-load

Conversation

@ymc9
Copy link
Copy Markdown
Member

@ymc9 ymc9 commented May 8, 2026

Summary

  • Fixes [BUG] ZenStack V3: better-auth integration leads to startup errors #2646ERR_PACKAGE_PATH_NOT_EXPORTED for langium/package.json on CJS apps (e.g. NestJS) when constructing the better-auth adapter.
  • Root cause: the bundled index.cjs had a top-level require("./schema-generator.cjs") that eagerly pulled @zenstackhq/language (Langium) at adapter load time. Langium's package.json lacks an exports main, so resolution from CJS failed.
  • The dynamic await import('./schema-generator') inside createSchema was supposed to keep that branch lazy, but Rolldown was hoisting a sibling-chunk register into the file top because both entries were built in a single tsdown invocation.

Fix

  • Split tsdown.config.ts into two separate tsdown invocations (one per entry) so Rolldown no longer treats them as siblings and stops injecting the eager top-level require.
  • Switched the dynamic import to a package self-subpath (@zenstackhq/better-auth/schema-generator) and added it to deps.neverBundle so Rolldown leaves it as an external reference resolved by Node at first call.
  • Added comments in both files explaining why this setup is required.

After the change, dist/index.cjs no longer has any top-level reference to schema-generator; only the lazy await import("@zenstackhq/better-auth/schema-generator") inside createSchema remains. Langium is therefore only loaded when schema generation is actually invoked (e.g. during @better-auth/cli generate), not on adapter construction.

Test plan

  • pnpm test in packages/auth-adapters/better-auth passes (6/6, including cli-generate.test.ts which exercises the dynamic import path)
  • Verified dist/index.cjs has no top-level require("./schema-generator.cjs") after build
  • Verified dist/index.mjs still uses lazy await import(...) only inside createSchema
  • Manual repro from [BUG] ZenStack V3: better-auth integration leads to startup errors #2646 (NestJS + better-auth CJS startup) confirmed working

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Better-Auth adapter now loads its schema generator from a dedicated package entry, preserving lazy/dynamic loading so adapter startup remains lightweight.
    • Build and TypeScript configs updated to support the modular entry, improving packaging and maintainability.

)

When the index entry and schema-generator entry were built in the same
tsdown invocation, Rolldown injected a top-level `require("./schema-generator.cjs")`
into `index.cjs`. This eagerly loaded `@zenstackhq/language` (Langium) at
adapter construction time, which fails on Node CJS hosts because Langium's
package.json has no `exports` main, producing ERR_PACKAGE_PATH_NOT_EXPORTED.

Split the build into two tsdown invocations and import schema-generator via
the package self-subpath (marked `deps.neverBundle`) so the dynamic import
stays truly lazy in both CJS and ESM output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 879f5dd9-4484-410d-979e-b15140345e73

📥 Commits

Reviewing files that changed from the base of the PR and between 4c2721f and fc36d3e.

📒 Files selected for processing (1)
  • packages/auth-adapters/better-auth/tsconfig.json
✅ Files skipped from review due to trivial changes (1)
  • packages/auth-adapters/better-auth/tsconfig.json

📝 Walkthrough

Walkthrough

The Better-Auth adapter build configuration is split into two separate entries: one for the main index module and one for the schema-generator subpath. The index build marks @zenstackhq/better-auth/schema-generator as neverBundle, enabling the adapter to dynamically import the schema-generator via package subpath instead of local path, preserving lazy-loading behavior and preventing transitive dependency bundling issues.

Changes

Schema Generator Lazy-Loading

Layer / File(s) Summary
Build Configuration Setup
packages/auth-adapters/better-auth/tsdown.config.ts
Export refactored from single createConfig to array of two builds. The index build marks @zenstackhq/better-auth/schema-generator as neverBundle to preserve dynamic import behavior; schema-generator build is added as separate entry.
Runtime Dynamic Import
packages/auth-adapters/better-auth/src/adapter.ts
The createSchema method's schema-generator import path changes from relative ./schema-generator to package subpath @zenstackhq/better-auth/schema-generator, with comments clarifying bundling and lazy-loading intent.
TSConfig Paths
packages/auth-adapters/better-auth/tsconfig.json
Adds compilerOptions.paths mapping for @zenstackhq/better-auth/schema-generator -> ./src/schema-generator.ts and includes Node types.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • #2646: The split schema-generator subpath with neverBundle configuration directly addresses the ERR_PACKAGE_PATH_NOT_EXPORTED error caused by transitive langium dependency bundling in the better-auth adapter.

Poem

🐰 I hopped through builds with nimble feet,
Split the paths so imports meet,
Schema-gen waits until called near,
Bundles stay light, no errors here,
A carrot for lazy-load cheer! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: preventing eager loading of schema-generator in the CJS build output, which is the core fix for issue #2646.
Linked Issues check ✅ Passed The PR directly addresses all three coding requirements from issue #2646: preventing eager Langium loading during adapter construction, ensuring lazy schema-generator imports, and preserving functionality in both CJS and ESM outputs.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the lazy import issue: split build configurations, dynamic import via package subpath, and tsconfig path mapping for type resolution.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/2646-better-auth-eager-langium-load

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 8, 2026

Claude finished @ymc9's task —— View job


Code Review for PR #2658

Reviewing the fix for better-auth eager Langium loading issue.

Todo List:

  • Read and analyze the changed files
  • Review code quality and implementation approach
  • Check for potential bugs or issues
  • Assess performance considerations
  • Evaluate security concerns
  • Review test coverage adequacy
  • Provide comprehensive feedback

Review Summary

This is a well-executed fix for a complex CJS module loading issue. The PR successfully addresses the root cause of eager Langium imports in CommonJS environments.

Code Quality & Implementation ✅

Excellent approach: The solution correctly identifies that the bundling strategy was causing sibling chunk registration, leading to unintended eager imports. The fix is surgical and well-reasoned:

  1. Split build strategy (tsdown.config.ts:10-18): Splitting into two separate tsdown invocations prevents Rolldown from treating the modules as siblings
  2. External package reference (adapter.ts:192): Using @zenstackhq/better-auth/schema-generator instead of relative imports ensures bundler treats it as external
  3. Clear documentation: Both files have excellent comments explaining the rationale

Technical Soundness ✅

Dynamic import strategy: The change from './schema-generator' to '@zenstackhq/better-auth/schema-generator' is clever because:

  • It leverages the package's own export map (package.json:42-51)
  • The neverBundle configuration (tsdown.config.ts:13) ensures it stays external
  • Maintains lazy loading semantics while avoiding bundler optimization issues

Performance Considerations ✅

Improved lazy loading: The fix actually enhances performance by:

  • Eliminating eager loading of the heavy Langium dependency
  • Only loading schema generation code when createSchema() is actually called
  • Reducing initial bundle size and startup time for CJS applications

Security Concerns ✅

No security implications: The changes are purely build/bundling related with no security impact.

Potential Issues & Suggestions

Minor: Build Configuration Complexity

The split build approach, while necessary, adds complexity. Consider:

  • Adding a comment in the main README or package documentation explaining why this unusual build setup exists
  • Ensuring both entries are tested in CI to prevent drift

Minor: Error Handling

adapter.ts:192 could benefit from error handling for the dynamic import:

try {
    const generateSchema = (await import('@zenstackhq/better-auth/schema-generator')).generateSchema;
    return generateSchema(file, tables, config, options);
} catch (error) {
    throw new BetterAuthError('Failed to load schema generator', { cause: error });
}

Test Coverage ✅

Adequate testing: The PR mentions:

  • 6/6 tests passing including cli-generate.test.ts which exercises the dynamic import path
  • Verification of build outputs (no top-level requires in CJS)
  • Manual testing planned for the original reproduction case

Architecture Impact ✅

Future-proof solution: The approach:

  • Maintains backward compatibility
  • Doesn't change the public API
  • Scales well if more conditional imports are needed
  • Properly leverages package.json exports

Overall Assessment

Excellent fix that demonstrates deep understanding of:

  • JavaScript module systems (CJS vs ESM)
  • Bundler behavior (Rolldown's sibling chunk optimization)
  • Package export strategies
  • Lazy loading patterns

The solution is minimal, targeted, and well-documented. The split build approach is unusual but justified and necessary for this specific use case.

Recommendation: ✅ Approve - This is a high-quality fix that solves a real compatibility issue without compromising functionality.


The package self-import `@zenstackhq/better-auth/schema-generator` resolves
through the published `exports` map to `./dist/schema-generator.d.mts`,
which doesn't exist on a clean CI build (tsc --noEmit runs before tsdown).
Add a tsconfig `paths` mapping so TypeScript resolves the type lookup
against the local source file at typecheck time. Runtime resolution
remains unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 8, 2026

Claude finished @ymc9's task —— View job


Code Review for PR #2658

Reviewing the fix for better-auth eager Langium loading issue.

Todo List:

  • Read and analyze the changed files
  • Review code quality and implementation approach
  • Check for potential bugs or issues
  • Assess performance considerations
  • Evaluate security concerns
  • Review test coverage adequacy
  • Provide comprehensive feedback

Review Summary

This is a well-executed fix for a complex CJS module loading issue. The PR successfully addresses the root cause of eager Langium imports in CommonJS environments.

Code Quality & Implementation ✅

Excellent approach: The solution correctly identifies that the bundling strategy was causing sibling chunk registration, leading to unintended eager imports. The fix is surgical and well-reasoned:

  1. Split build strategy (tsdown.config.ts:10-18): Splitting into two separate tsdown invocations prevents Rolldown from treating the modules as siblings
  2. External package reference (adapter.ts:192): Using @zenstackhq/better-auth/schema-generator instead of relative imports ensures bundler treats it as external
  3. Clear documentation: Both files have excellent comments explaining the rationale

Technical Soundness ✅

Dynamic import strategy: The change from './schema-generator' to '@zenstackhq/better-auth/schema-generator' is clever because:

  • It leverages the package's own export map (package.json:42-51)
  • The neverBundle configuration (tsdown.config.ts:13) ensures it stays external
  • Maintains lazy loading semantics while avoiding bundler optimization issues

Performance Considerations ✅

Improved lazy loading: The fix actually enhances performance by:

  • Eliminating eager loading of the heavy Langium dependency
  • Only loading schema generation code when createSchema() is actually called
  • Reducing initial bundle size and startup time for CJS applications

Security Concerns ✅

No security implications: The changes are purely build/bundling related with no security impact.

Potential Issues & Suggestions

Minor: Build Configuration Complexity

The split build approach, while necessary, adds complexity. Consider:

  • Adding a comment in the main README or package documentation explaining why this unusual build setup exists
  • Ensuring both entries are tested in CI to prevent drift

Minor: Error Handling

adapter.ts:192 could benefit from error handling for the dynamic import:

try {
    const generateSchema = (await import('@zenstackhq/better-auth/schema-generator')).generateSchema;
    return generateSchema(file, tables, config, options);
} catch (error) {
    throw new BetterAuthError('Failed to load schema generator', { cause: error });
}

Test Coverage ✅

Adequate testing: The PR mentions:

  • 6/6 tests passing including cli-generate.test.ts which exercises the dynamic import path
  • Verification of build outputs (no top-level requires in CJS)
  • Manual testing planned for the original reproduction case

Architecture Impact ✅

Future-proof solution: The approach:

  • Maintains backward compatibility
  • Doesn't change the public API
  • Scales well if more conditional imports are needed
  • Properly leverages package.json exports

Overall Assessment

Excellent fix that demonstrates deep understanding of:

  • JavaScript module systems (CJS vs ESM)
  • Bundler behavior (Rolldown's sibling chunk optimization)
  • Package export strategies
  • Lazy loading patterns

The solution is minimal, targeted, and well-documented. The split build approach is unusual but justified and necessary for this specific use case.

Recommendation: ✅ Approve - This is a high-quality fix that solves a real compatibility issue without compromising functionality.


@ymc9 ymc9 merged commit 08c11e7 into dev May 8, 2026
8 checks passed
@ymc9 ymc9 deleted the fix/2646-better-auth-eager-langium-load branch May 8, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] ZenStack V3: better-auth integration leads to startup errors

1 participant