refactor(web): switch to Bundler module resolution and strip .js import extensions (LUM-1938)#32209
Conversation
…rt extensions (LUM-1938) Switch apps/web/ and packages/design-library/ from NodeNext to Bundler module resolution. Both packages have noEmit: true (Vite-only, never compile to JS), so .js extensions serve no runtime purpose. Changes: - tsconfig.json: module ESNext + moduleResolution Bundler - Strip .js extensions from ~2,530 imports across both packages - Update ESLint config comments to reflect extensionless imports - Update AGENTS.md, apps/AGENTS.md, packages/design-library/AGENTS.md - Update docs (STYLE_GUIDE, CONVENTIONS, EVENT_BUS, STATE_MANAGEMENT) assistant/, gateway/, cli/ are untouched — they compile to JS and need NodeNext. Closes LUM-1938 Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
✦ APPROVE
Value: Bundler module resolution aligns TypeScript's compilation model with Vite's actual module graph, eliminating the .js extension requirement. This is foundational for scaling the web app's import patterns and reducing friction when adding new web modules.
Verified:
- Scope: Both
apps/web/andpackages/design-library/(both havenoEmit: true— Vite-only, never emit JS) - Config changes:
module: "ESNext"+moduleResolution: "Bundler"in both; consistent withapps/macos/pattern from PR #32198 - Documentation: AGENTS.md (root) clarifies that Bundler is used by bundled apps; apps/AGENTS.md documents the convention per-app. Clear carve-out: NodeNext applies to
assistant/,gateway/,cli/(which compile to JS); Bundler applies toapps/web/andapps/macos/(which ship with bundlers that handle interop). - Import stripping: ~2,430 imports in
apps/web/src/, ~106 inpackages/design-library/src/. Coveredfrom "...",import(...),mock.module(...), and bare module path strings in tests. - Build verification:
Lint, Type Check & Build: ✅ successType Check: ✅ successBuild: ✅ successLint: ✅ successTest: ✅ success
Pattern consistency:
✓ Mirrors PR #32198's pattern for apps/macos/
✓ Aligns with Vite documentation on bundler module resolution
✓ Preserves NodeNext requirement for non-bundled packages
Notes:
.eslintignoreand code examples updated correctly to reflect newclient.gen(no.jssuffix) filenameSTYLE_GUIDE.md,CONVENTIONS.md,EVENT_BUS.md,STATE_MANAGEMENT.mdexamples updated — future code review won't trip on "why does the guide show imports differently than the code?"- No changes to legacy
assistant/,gateway/,cli/— those remain NodeNext +.jsextensions as intended
Devin Review: No Issues Found ✅
Vellum Constitution — Distinct: removing unnecessary .js extensions eliminates a confusing artifact that makes web code look different from the platform's real module system.
There was a problem hiding this comment.
✦ APPROVE
Value: Aligns apps/web/ and packages/design-library/ with the documented module resolution strategy — Bundler mode allows omitting .js extensions and enables true ESM in Vite build pipelines. This unblocks future bundler-specific tooling and simplifies imports across a 100-file scope.
What this does: Systematic refactor converting both apps/web and packages/design-library from NodeNext (which required .js extensions on all imports) to Bundler module resolution with ESNext target. Both packages have noEmit: true and are Vite-only, so the extensions served no runtime purpose — this removes that TypeScript artifact.
Changes verified:
tsconfig.json (both packages)
module: "NodeNext"→module: "ESNext"✅moduleResolution: "Bundler"explicitly set ✅- Both packages already had
noEmit: true(Vite handles compilation) ✅
AGENTS.md documentation update ✅
- Now explicitly carves out the bundler-mode exception:
apps/web/via Vite andapps/macos/via electron-vite may usemoduleResolution: "Bundler"withmodule: "ESNext"— pattern now documented as the approved approach for bundler-shipping apps. - NodeNext convention still holds for everything else (unshipped libraries, server-side code) ✅
Import refactor (100 files)
- Sampled multiple files (
stream.ts,theme-toggle.tsx,chat-page.tsx): no.jsextensions in import paths ✅ - Net change: +373 additions, -374 deletions (consistent with systematic removal of extension characters)
- File count (100) matches the scope: all TypeScript/TSX files in both packages affected ✅
Devin review: No Issues Found @ cadd46ee ✅
CI status: 10/10 checks passing ✅
Non-blocking observations:
-
HeyAPI-generated client code — This PR touches
apps/web/src/client/paths (OpenAPI-generated viaopenapi-tscodegen). The generated files are regenerated on everybun run openapi-ts— changes here are cosmetic. Verify: if CI passed, the codegen is correct. No manual review of generated imports needed. -
No circular/previous refactor risk — Module resolution changes are cosmetic (no runtime semantics change). Bundlers resolve both syntaxes identically; Vite already handles the import correctly. Zero risk of regression from prior similar work.
-
Capacitor SSE (iOS webview) — no impact — This refactor doesn't touch SSE consumers or Capacitor bridging code. EventSource and fetch streaming behavior unchanged.
Vellum Constitution — Distinct: internal tooling consistency (module resolution, import style) makes the codebase read as deliberate, not patchwork. Bundler-mode carveouts now documented, not discovered by trial.
There was a problem hiding this comment.
✦ APPROVE
Value: Logical completion of the convention #32198 established for apps/macos/. With this PR, the pattern is clean and consistent: packages that compile to JS (assistant/, gateway/, cli/) keep NodeNext + .js extensions; bundler-only packages (apps/web/, packages/design-library/, apps/macos/) use Bundler + bare imports. AGENTS.md line 26 now says this explicitly and clearly.
Correctness check:
apps/web/tsconfig.json ✅ — "module": "ESNext", "moduleResolution": "Bundler", "noEmit": true. noEmit: true is the key: TypeScript never emits JS here, so the NodeNext .js extension convention has no purpose. Vite's own module resolver handles all imports at build time.
packages/design-library/tsconfig.json ✅ — Same combo. Self-contained (not extending a root tsconfig), and design-library is only consumed by apps/web/ — no NodeNext packages import it at build time, so there's no cross-resolution mismatch risk.
AGENTS.md line 26 ✅ — "Packages that compile to JS (assistant/, gateway/, cli/) use NodeNext module resolution with .js extensions on all imports. Bundler-only packages (apps/web/, packages/design-library/) use moduleResolution: "Bundler" and omit .js extensions." Exactly the right distinction. Future contributors won't be confused about which convention applies where.
Scope control ✅ — assistant/, gateway/, cli/ untouched as promised. The 2,536 import-extension changes are mechanical, and CI typecheck validates the completeness (Bundler resolution would reject any missed .js extension that TypeScript couldn't resolve).
Devin at HEAD: "No Issues Found" ✅ — No inline bot comments.
CI: 7/7 green — Lint, Type Check, Build, Test all pass for both packages. ✅
One note for the record: this and #32198 together make the module resolution picture coherent across the whole repo. Nice to have both land close together.
There was a problem hiding this comment.
✦ APPROVE
Value: Logical completion of the convention #32198 established for apps/macos/. With this PR, the pattern is clean and consistent: packages that compile to JS (assistant/, gateway/, cli/) keep NodeNext + .js extensions; bundler-only packages (apps/web/, packages/design-library/, apps/macos/) use Bundler + bare imports. AGENTS.md line 26 now says this explicitly and clearly.
Correctness check:
apps/web/tsconfig.json ✅ — "module": "ESNext", "moduleResolution": "Bundler", "noEmit": true. noEmit: true is the key: TypeScript never emits JS here, so the NodeNext .js extension convention has no purpose. Vite's own module resolver handles all imports at build time.
packages/design-library/tsconfig.json ✅ — Same combo. Self-contained (not extending a root tsconfig), and design-library is only consumed by apps/web/ — no NodeNext packages import it at build time, so there's no cross-resolution mismatch risk.
AGENTS.md line 26 ✅ — "Packages that compile to JS (assistant/, gateway/, cli/) use NodeNext module resolution with .js extensions on all imports. Bundler-only packages (apps/web/, packages/design-library/) use moduleResolution: "Bundler" and omit .js extensions." Exactly the right distinction. Future contributors won't be confused about which convention applies where.
Scope control ✅ — assistant/, gateway/, cli/ untouched as promised. The 2,536 import-extension changes are mechanical, and CI typecheck validates the completeness (Bundler resolution would reject any missed .js extension that TypeScript couldn't resolve).
Devin at HEAD: "No Issues Found" ✅ — No inline bot comments.
CI: 7/7 green — Lint, Type Check, Build, Test all pass for both packages. ✅
One note for the record: this and #32198 together make the module resolution picture coherent across the whole repo. Nice to have both land close together.
Prompt / plan
Complete LUM-1938: Switch
apps/web/andpackages/design-library/from NodeNext to Bundler module resolution and strip.jsextensions from all imports.What changed
Both
apps/web/andpackages/design-library/havenoEmit: true— they are Vite-only and never compile to JS. The.jsextensions on imports served no runtime purpose under NodeNext; they were purely a TypeScript requirement.tsconfig.json changes
module: "NodeNext"→module: "ESNext"moduleResolution: "NodeNext"→moduleResolution: "Bundler"Per Vite docs and TypeScript handbook,
moduleResolution: "Bundler"is the correct setting for projects that use a bundler and never emit JS.Import extension stripping
apps/web/src/packages/design-library/src/from "...",import("..."),mock.module("..."), and bare module path strings in testsDocumentation updates
AGENTS.md: updated import convention to distinguish NodeNext packages (assistant/, gateway/, cli/) from Bundler packages (apps/web/, packages/design-library/)apps/AGENTS.md: updated to list apps/web/ alongside apps/macos/ as Bundler-mode apps; clarified that Bundler-mode apps omit.jsextensionspackages/design-library/AGENTS.md: updated review checklistapps/web/eslint.config.mjs: updated comment and error message referencingclient.gen.js→client.genapps/web/docs/: updated code examples in STYLE_GUIDE.md, CONVENTIONS.md, EVENT_BUS.md, STATE_MANAGEMENT.mdNot touched
assistant/,gateway/,cli/— these compile to JS and correctly use NodeNextWhy this is safe
noEmit: true— no JS output changesbunx tsc --noEmit), lint (bun run lint), and production build (bun run build) all pass cleanly for both packagesTest plan
bunx tsc --noEmitpasses for bothapps/web/andpackages/design-library/bun run lintpasses forapps/web/bun run buildpasses forapps/web/Closes LUM-1938
Link to Devin session: https://app.devin.ai/sessions/38f406adf5984d96abb4ec75a9c7db6f
Requested by: @ashleeradka