feat: add loadServerModule API for SSR-only TypeScript modules#34
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
vite.loadServerModule(entry, opts?)— a first-class API for loading server-side TypeScript modules through Vite in both dev and build mode.serverEntrypointsplugin option that bundles declared entries into the SSR environment (one bundle per entry, hash-free, with manifest).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
ModuleRunnerthemselves.Design
src/server_modules/split intoServerModuleLoader(orchestrator),DevModuleRunner(runner host with restart-detect), andBundledModuleResolver(manifest read + cached prod imports).loadServerModuleresolvesentry → bundle filethrough it. No string normalization, no extension stripping — entry strings are passed through verbatim, just like cliententrypoints.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.ServerModuleMapinterface to associate entrypoint paths with their module shapes.freshopt-in.loadServerModule(entry, { fresh: true })clears the runner cache before importing. Default is to trust HMR for invalidation — no per-renderclearCachewaste.Build hook changes
NODE_ENV=productionbeforecreateBuilderso framework plugins emit production paths under programmatic builds (see import_jsx_dev_runtime.jsxDEV is not a function after upgrade to 1.7.0 in Docker Container remix-run/remix#4081).useLegacyBuilder = false) so Vite's defaultbuildAppbuilds every declared environment when no plugin contributes its own.Migration path for Inertia (separate PR)
ServerRenderer.render()→vite.loadServerModule(config.ssr.entrypoint).clearCachecall (or passfresh: trueif a regression is observed).@adonisjs/inertia/src/client/vite.ts(the Inertia Vite plugin) — its responsibilities are now covered byserverEntrypointsand the build hook.inertia()plugin invite.config.tswithserverEntrypoints: ['inertia/ssr.tsx']onadonisjs().Test plan
tests/backend/server_modules.spec.tscovering:fresh: truere-evaluatesconfigHook: ssr environment absent whenserverEntrypointsis empty, present when non-empty, defers to user-supplied fieldstsc --noEmitclean