Skip to content

feat: Add SSR support#12

Draft
xusd320 wants to merge 3 commits into
mainfrom
xusd320/add-ssr-support
Draft

feat: Add SSR support#12
xusd320 wants to merge 3 commits into
mainfrom
xusd320/add-ssr-support

Conversation

@xusd320
Copy link
Copy Markdown
Contributor

@xusd320 xusd320 commented May 18, 2026

Summary

  • Add first-class SSR configuration and runtime APIs under @evjs/server/ssr, including route-tree helpers, document rendering, asset tags, and transport request-context forwarding.
  • Wire client hydration and TanStack Router SSR integration through @evjs/client, while keeping RSC hidden from public docs and exports.
  • Update Utoopack config for SSR document fallback, TanStack isServer aliasing, SSR defines, plus add SSR example and e2e coverage.

Validation

  • npm test -w @evjs/client
  • npm test -w @evjs/server
  • npm test -w @evjs/ev
  • npm test -w @evjs/bundler-utoopack
  • npm run check-types
  • npm run test:e2e -- e2e/cases/ssr.ts --project=utoopack
  • npm run lint

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces comprehensive Server-Side Rendering (SSR) support across the framework, including document rendering on the server and hydration logic on the client. Key updates include SSR-specific bundler configurations, a new createSsrHandler for managing TanStack Router SSR, and enhanced transport logic to forward request context. Feedback identifies a critical issue where the AsyncLocalStorage instance needs to be a singleton to prevent context loss across multiple bundles and suggests a more robust method for handling multi-value headers during record conversion.

Comment thread packages/server/src/ssr.ts Outdated
Comment on lines +180 to +186
const transportRequestContext = new AsyncLocalStorage<{
baseUrl?: string;
headers?: Record<string, string>;
}>();

(globalThis as Record<symbol, unknown>)[REQUEST_CONTEXT_KEY] =
transportRequestContext;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The AsyncLocalStorage instance should be a singleton across the entire process to ensure that request context is correctly shared even if multiple versions or bundles of @evjs/server are loaded. Currently, every time this module is evaluated, it creates a new instance and overwrites the global symbol, which will cause getStore() to return undefined for code running in a previously established context from a different instance.

const transportRequestContext = (function () {
  const globalAny = globalThis as any;
  if (!globalAny[REQUEST_CONTEXT_KEY]) {
    globalAny[REQUEST_CONTEXT_KEY] = new AsyncLocalStorage<{
      baseUrl?: string;
      headers?: Record<string, string>;
    }>();
  }
  return globalAny[REQUEST_CONTEXT_KEY] as AsyncLocalStorage<{
    baseUrl?: string;
    headers?: Record<string, string>;
  }>;
})();

Comment thread packages/server/src/ssr.ts Outdated
Comment on lines +468 to +476
function headersToRecord(headers: Headers): Record<string, string> {
const result: Record<string, string> = {};
headers.forEach((value, key) => {
if (!FORWARDED_HEADER_DENYLIST.has(key.toLowerCase())) {
result[key] = value;
}
});
return result;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using headers.forEach to build the record might not correctly handle headers with multiple values, as it may overwrite previous values or use an incorrect separator depending on the environment's Headers implementation. Using headers.get(key) for each unique key is generally safer as it returns the correctly combined value according to the Fetch spec.

function headersToRecord(headers: Headers): Record<string, string> {
  const result: Record<string, string> = {};
  const keys = new Set(headers.keys());
  for (const key of keys) {
    if (!FORWARDED_HEADER_DENYLIST.has(key.toLowerCase())) {
      const value = headers.get(key);
      if (value !== null) {
        result[key] = value;
      }
    }
  }
  return result;
}

@xusd320 xusd320 changed the title [codex] Add SSR support feat: Add SSR support May 18, 2026
xusd320 added 2 commits May 19, 2026 11:52
# Conflicts:
#	packages/client/src/app.tsx
#	packages/client/src/transport.ts
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