Skip to content

fix: CJS/ESM named-export interop for js-sha256 & gql-query-builder under Nuxt 4 / Vite 7#18

Merged
NicoKaempf merged 2 commits into
mainfrom
fix/cjs-esm-interop-nuxt4
Jul 8, 2026
Merged

fix: CJS/ESM named-export interop for js-sha256 & gql-query-builder under Nuxt 4 / Vite 7#18
NicoKaempf merged 2 commits into
mainfrom
fix/cjs-esm-interop-nuxt4

Conversation

@NicoKaempf

@NicoKaempf NicoKaempf commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Problem

Under Vite 7 / Nuxt 4, @lenne.tech/nuxt-base broke the client bundle → no hydration (dropdowns/forms/buttons dead). Root cause lived entirely in this module:

  • 5 runtime files import named exports from CJS/UMD packages: { sha256 } from js-sha256, { query, mutation, subscription } from gql-query-builder.
  • module.ts pushed both packages into build.transpile (client-only) — the // TODO: Remove when package fixed with valid ESM exports workaround. Under Vite 7 that serves them raw (no esbuild pre-bundling), so the named imports aren't statically detectable → client bundle crash.

This surfaced during the Nuxt 3→4 migration in a consumer project, which had to work around it with per-project Vite-alias shims. That workaround belongs here instead — fixed at the source, no consumer shims needed.

Fix

  • Remove both build.transpile pushes — the actual root cause. Both deps are pure CJS (no ESM entry point), so pre-bundling via optimizeDeps (dev/esbuild) and Rollup's commonjs plugin (build) produces the proper interop wrapper.
  • Add two interop helpers (runtime/helpers/js-sha256-interop.ts, gql-query-builder-interop.ts) using namespace-import + default-fallback, covering both CJS shapes (__esModule and module.exports = {}).
  • Point the 5 runtime import sites at the helpers.

Review hardening

A high-effort review caught follow-ups, all addressed:

  • Auto-import leak — the helpers deliberately live outside the auto-imported runtime dirs, so generic names (query/mutation/subscription/sha256) don't leak into consumers' global Nuxt auto-import namespace.
  • Decoupling — split into two files so the password-hashing (sha256) path never pulls gql-query-builder into the graph.
  • disableGraphqlgql-query-builder pre-bundling is gated behind !disableGraphql; js-sha256 stays unconditional (backs password hashing).

Validation

  • Lint green on all changed files.
  • Runtime check against the real installed packages: sha256/query/mutation/subscription resolve to functions, sha256("abc") correct, query builder works.

Open / before release

Full production client build couldn't be exercised in the dev environment (pre-existing nuxi prepare failure, unrelated to this change). Definitive test: production build + hydration/login check in a consumer (regio-match) with this nuxt-base and the MR !467 shims removed. Once released, that project's alias-shim workaround can be dropped.

🤖 Generated with Claude Code

…uilder under Nuxt 4 / Vite 7

Removes the client-only build.transpile pushes for js-sha256 and gql-query-builder
(the root cause: under Vite 7 / Nuxt 4 they were served raw, breaking named imports
and the client bundle → no hydration). Both are now read through dedicated interop
helpers (namespace import + default fallback) and pre-bundled via optimizeDeps.

- add runtime/helpers/js-sha256-interop.ts and gql-query-builder-interop.ts
  (kept out of the auto-imported runtime dirs so their generic exports don't leak
  into the global Nuxt auto-import namespace)
- point the 5 runtime import sites at the interop helpers
- keep the two deps decoupled so the password-hashing path never pulls gql-query-builder
- gate gql-query-builder pre-bundling behind !disableGraphql

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Nuxt 4 / Vite 7 client hydration breakage caused by importing named exports from CJS/UMD dependencies (js-sha256, gql-query-builder) when those deps are served “raw” due to build.transpile.

Changes:

  • Introduces explicit CJS/UMD interop helpers that resolve exports via namespace import + default fallback.
  • Updates GraphQL composables and metadata hashing code to import from the interop helpers instead of named-importing the packages directly.
  • Removes the build.transpile workaround and relies on Vite dep optimization (and Rollup CJS handling in production) to generate correct interop.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/runtime/helpers/js-sha256-interop.ts Adds a js-sha256 interop wrapper to safely access sha256 under CJS/UMD.
src/runtime/helpers/gql-query-builder-interop.ts Adds a gql-query-builder interop wrapper for query/mutation/subscription.
src/runtime/functions/graphql-meta.ts Switches hashing import to the js-sha256 interop helper.
src/runtime/composables/gql-subscription.ts Switches subscription import to the interop helper.
src/runtime/composables/gql-query.ts Switches query import to the interop helper.
src/runtime/composables/gql-mutation.ts Switches mutation import to the interop helper.
src/runtime/composables/gql-async-query.ts Switches query import to the interop helper.
src/module.ts Stops transpiling the deps and instead ensures dev pre-bundling via optimizeDeps.include (with GraphQL gating).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

The exclude array was initialized but never pushed to. Removed as part of the
CJS/ESM interop cleanup (flagged by review). No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NicoKaempf
NicoKaempf merged commit 0eb978d into main Jul 8, 2026
1 check passed
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.

2 participants