Skip to content

feat: add loadServerModule API for SSR-only TypeScript modules#34

Merged
thetutlage merged 4 commits into
6.xfrom
feat/load-server-module
May 10, 2026
Merged

feat: add loadServerModule API for SSR-only TypeScript modules#34
thetutlage merged 4 commits into
6.xfrom
feat/load-server-module

Conversation

@thetutlage
Copy link
Copy Markdown
Member

Summary

  • Adds vite.loadServerModule(entry, opts?) — a first-class API for loading server-side TypeScript modules through Vite in both dev and build mode.
  • Adds serverEntrypoints plugin option that bundles declared entries into the SSR environment (one bundle per entry, hash-free, with manifest).
  • Moves process.env.NODE_ENV = 'production' from the Inertia Vite plugin to @adonisjs/vite's build hook — it's a generic concern, not Inertia-specific.

Why

Today, packages that need to execute TypeScript on the server (e.g. Inertia SSR, React-email templates, MDX renderers) each roll their own Vite bundling + module-runner plumbing. This PR consolidates that into a single API so any consumer can load Vite-processed modules from Node without owning a ModuleRunner themselves.

Design

  • Roles, not helpers. Logic lives in src/server_modules/ split into ServerModuleLoader (orchestrator), DevModuleRunner (runner host with restart-detect), and BundledModuleResolver (manifest read + cached prod imports).
  • Manifest as source of truth in prod. The SSR environment emits a manifest mirroring the client pattern; loadServerModule resolves entry → bundle file through it. No string normalization, no extension stripping — entry strings are passed through verbatim, just like client entrypoints.
  • Override-safe config. Every environments.ssr.build.* field falls back to user/plugin-supplied values via ?? guards, so other plugins contributing to the SSR environment cooperate rather than collide.
  • Type-safe entries. Apps and packages augment the exported ServerModuleMap interface to associate entrypoint paths with their module shapes.
  • fresh opt-in. loadServerModule(entry, { fresh: true }) clears the runner cache before importing. Default is to trust HMR for invalidation — no per-render clearCache waste.

Build hook changes

Migration path for Inertia (separate PR)

  1. ServerRenderer.render()vite.loadServerModule(config.ssr.entrypoint).
  2. Drop the clearCache call (or pass fresh: true if a regression is observed).
  3. Delete @adonisjs/inertia/src/client/vite.ts (the Inertia Vite plugin) — its responsibilities are now covered by serverEntrypoints and the build hook.
  4. Document upgrade: replace inertia() plugin in vite.config.ts with serverEntrypoints: ['inertia/ssr.tsx'] on adonisjs().

Test plan

  • 11 new tests in tests/backend/server_modules.spec.ts covering:
    • dev path: TS/JSX import via runner, runner reuse across calls, fresh: true re-evaluates
    • prod path: manifest-driven import, per-entry import cache, missing-entry error, missing-manifest error
    • configHook: ssr environment absent when serverEntrypoints is empty, present when non-empty, defers to user-supplied fields
  • Full test suite green (113 passed, 1 skipped)
  • tsc --noEmit clean

Introduce a first-class API for loading server-side TypeScript modules
processed by Vite, usable in both dev and build mode. Enables Inertia
SSR, React-email templates, and any other module that must run in Node
but cannot be imported directly by AdonisJS.

- New `serverEntrypoints` plugin option declares entries to bundle into
  the SSR environment. Bundles are emitted under
  `<buildDirectory>/server` with a manifest, mirroring the client
  pattern. SSR build config defers to user/plugin-supplied values via
  ?? guards so other plugins can cooperate.
- New `vite.loadServerModule(entry, opts?)` runtime API. In dev,
  evaluates through a shared `ModuleRunner` with restart-detect on SSR
  environment swaps. In prod, reads the SSR manifest and imports the
  bundled file with per-entry import caching.
- `ServerModuleMap` interface exposed for declaration merging so apps
  can type entry strings against their module shapes.
- `LoadServerModuleOptions.fresh` opt-in to clear the runner cache
  before importing. Default is to trust HMR for invalidation.
- Move `process.env.NODE_ENV = 'production'` into the build hook —
  it's a generic concern for any framework plugin under programmatic
  `createBuilder`, not Inertia-specific.
- Force multi-environment builder mode so Vite's default `buildApp`
  builds every declared environment.

Logic is split into small role classes under `src/server_modules`:
`ServerModuleLoader` (dispatch), `DevModuleRunner` (runner host +
restart-detect), and `BundledModuleResolver` (manifest read + cached
prod imports).
Three simplifications that incidentally fix the Windows path-separator
test failures:

1. `client/config.ts`: drop `join(root, entrypoint)` for both client
   and ssr `rolldownOptions.input`. Vite resolves relative input paths
   against `config.root` automatically, so prefixing with the root only
   introduced platform-native separators (`\` on Windows) for no benefit.
   Pass user entrypoints through verbatim, matching Laravel's plugin.

2. `client/resolve_assets.ts`: adopt Rolldown's `originalFileName` field
   on `emitFile({ type: 'asset' })`. Vite uses it directly as the
   manifest key, eliminating the second `:manifest` plugin and the
   `assetRefToSource` map / `manifestEnabled` / `manifestFileName`
   state that scanned for the synthetic `_<hash>.<ext>` key after
   the build. Cuts ~50 lines and removes a fragile post-build rewrite.

3. `client/reload.ts`: switch from `@poppinss/utils/string.toUnixSlash`
   to Vite's exported `normalizePath`. Same behavior (POSIX separators
   on all platforms) using the framework's own helper, applied to the
   project root, the user-supplied glob patterns, and the chokidar-
   emitted file paths so picomatch matches consistently on Windows.
@thetutlage thetutlage merged commit dc8b200 into 6.x May 10, 2026
8 checks 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.

1 participant