remote-module
host-app-architecture
Remote Esm Component Loading Architecture

Remote ESM Component Loading Architecture

This document explains the complete lifecycle of a remote UI component in XSite.

It covers what happens after the host receives a remote ESM URL such as:

https://esm.xsite.live/@xsite/[email protected]?target=es2022

The flow starts on the server, where the remote module is downloaded and compiled, and ends in the browser, where the compiled module is imported and rendered by React or Puck.


Architecture Summary

The remote loading process has two distinct phases:

1. Server compilation phase
   Remote ESM URL
   → download source
   → resolve imports
   → replace host dependencies
   → bundle output
   → return browser-compatible ESM

2. Browser execution phase
   Compiled ESM URL
   → initialize host runtime
   → dynamic import
   → resolve default export
   → render React component

The server does not render or execute the remote React component during compilation.

The browser does not resolve or download the full remote dependency graph directly.


Main Components

The architecture is composed of four main parts:

ComponentResponsibility
compileRemoteComponentCoordinates esbuild compilation
createEsmHttpPluginDownloads and resolves remote ESM modules
runtime-peers.tsMaps host dependencies to virtual runtime module IDs
createHostRuntimePluginGenerates adapters that read from globalThis.__XSITE_RUNTIME__

Additional supporting functions include:

FunctionResponsibility
fetchRemoteSourceFetches remote JavaScript and follows CDN stubs
ensureDefaultExportAdds a default export when only a named component export exists
initializeRemoteRuntimeInstalls host React, Next.js components, and XSite utilities in the browser
mapCdnPathToRuntimeModuleRecognizes CDN-form peer dependency URLs

Complete Request Flow

Example Input

Assume the following source object is passed to the compiler:

const source = {
  url: "https://esm.xsite.live/@xsite/[email protected]?target=es2022",
};

The caller executes:

const result = await compileRemoteComponent(source);

The returned result has this shape:

export type CompileResult = {
  code: string;
  resolvedUrl: string;
};

Example:

{
  code: "/* browser-compatible bundled ESM */",
  resolvedUrl:
    "https://esm.xsite.live/@xsite/[email protected]?target=es2022"
}

At this stage, the component has been compiled but has not yet been rendered.


Phase 1: Server Compilation

1. compileRemoteComponent Receives the Remote Source

export async function compileRemoteComponent(
  source: ResolvedSource,
): Promise<CompileResult> {

The ResolvedSource must contain the final remote URL to compile.

The compiler is responsible for:

  • Starting esbuild
  • Registering the required plugins
  • Loading the remote entry
  • Resolving all imports
  • Replacing host dependencies
  • Bundling remote implementation files
  • Ensuring a default component export
  • Returning browser-compatible ESM code

2. esbuild Is Dynamically Imported

const { build } = await import("esbuild");

The dynamic import prevents Next.js or Turbopack from trying to include the native esbuild binary in the application bundle.

This code must execute in a server environment.

Next.js server
    ↓
Dynamic import of esbuild
    ↓
Native esbuild build process

3. esbuild Starts From a Virtual Entry

const ENTRY_ID = "__xsite_remote_entry__";

The build uses:

entryPoints: [ENTRY_ID],

__xsite_remote_entry__ is not a physical file.

It is a virtual identifier that allows a plugin to replace the entry with the actual remote URL.


4. The Virtual Entry Resolves to the Remote URL

The xsite-remote-entry plugin intercepts the virtual entry:

{
  name: "xsite-remote-entry",
 
  setup(buildApi) {
    buildApi.onResolve(
      { filter: new RegExp(`^${ENTRY_ID}$`) },
      () => ({
        path: source.url,
        namespace: "esm-http",
      }),
    );
  },
}

For the About1 example, esbuild resolves:

__xsite_remote_entry__

into:

{
  path:
    "https://esm.xsite.live/@xsite/[email protected]?target=es2022",
  namespace: "esm-http",
}

The namespace tells esbuild not to treat the path as a local filesystem file.

Instead, the esm-http plugin becomes responsible for loading it.


5. createEsmHttpPlugin Loads the Entry

The plugin registers an onLoad handler for the esm-http namespace:

buildApi.onLoad(
  {
    filter: /.*/,
    namespace: "esm-http",
  },
  async (args) => {
    const { source } = await cachedFetch(args.path);
 
    return {
      contents: source,
      loader: "js",
    };
  },
);

The handler calls:

cachedFetch(args.path);

For About1:

cachedFetch(
  "https://esm.xsite.live/@xsite/[email protected]?target=es2022"
)

cachedFetch eventually calls:

fetchRemoteSource(url);

The remote source is returned to esbuild as JavaScript.


6. Remote Fetch Cache

The HTTP plugin creates a compilation-level cache:

const fetchCache = new Map<
  string,
  Promise<FetchedSource>
>();

The cache stores promises rather than only completed responses.

This prevents duplicate requests when several modules request the same dependency at the same time.

Module A requests dependency X
Module B requests dependency X
              ↓
Both receive the same pending promise
              ↓
Only one network request is made

This cache is:

  • In memory
  • Scoped to the plugin instance
  • Scoped to the current compilation
  • Not a permanent disk cache
  • Not a browser cache
  • Not a Next.js data cache

7. CDN Stub Resolution

An ESM CDN may return a thin re-export module:

export * from "/@xsite/[email protected]/es2022/ui-about1.mjs";
export { default } from "/@xsite/[email protected]/es2022/ui-about1.mjs";

fetchRemoteSource may follow this wrapper and return the actual leaf module:

{
  source: leafModuleSource,
  resolvedUrl:
    "https://esm.xsite.live/@xsite/[email protected]/es2022/ui-about1.mjs"
}

The plugin then aliases the resolved URL:

if (fetched.resolvedUrl !== url) {
  fetchCache.set(
    fetched.resolvedUrl,
    Promise.resolve({
      source: fetched.source,
      resolvedUrl: fetched.resolvedUrl,
    }),
  );
}

This is important for correct relative import resolution.

For example:

import "./AboutCard.mjs";

must resolve relative to:

https://esm.xsite.live/@xsite/[email protected]/es2022/ui-about1.mjs

and not relative to the initial package wrapper URL.


8. esbuild Parses the Remote Imports

Assume the downloaded About1 module contains:

import React from "react";
import { jsx } from "react/jsx-runtime";
import Image from "next/image";
import Link from "next/link";
import { getImage } from "@xsite/common";
import { BlockRenderer } from "@xsite/core/block-renderer";
import AboutCard from "./AboutCard.mjs";

esbuild sends each import through the registered onResolve handlers.

Each import follows one of two paths:

Host dependency
→ map to xsite-runtime

Remote implementation dependency
→ resolve URL
→ fetch source
→ bundle into output

Host Dependency Resolution

9. Bare Imports Are Mapped by createHostRuntimePlugin

The host runtime plugin loops through the peer definitions:

for (const peer of BARE_PEER_RUNTIME_MODULES) {
  buildApi.onResolve(
    { filter: peer.filter },
    () => ({
      path: peer.path,
      namespace: XSITE_RUNTIME_NAMESPACE,
    }),
  );
}

The runtime namespace is:

export const XSITE_RUNTIME_NAMESPACE =
  "xsite-runtime";

Example mappings:

react
→ xsite-runtime:react

react/jsx-runtime
→ xsite-runtime:react-jsx-runtime

next/image
→ xsite-runtime:next-image

next/link
→ xsite-runtime:next-link

@xsite/common
→ xsite-runtime:xsite-common

@xsite/core/block-renderer
→ xsite-runtime:xsite-core-block-renderer

These dependencies are not fetched as normal remote modules.

They are replaced with virtual adapter modules.


10. CDN-Expanded Imports Are Also Mapped

An ESM CDN may convert a bare import:

import React from "react";

into:

import React from "/[email protected]/es2022/react.mjs";

The bare-import patterns in BARE_PEER_RUNTIME_MODULES do not match this URL.

For that reason, createEsmHttpPlugin also calls:

mapCdnPathToRuntimeModule(args.path);

Example:

mapCdnPathToRuntimeModule(
  "/[email protected]/es2022/react.mjs?target=es2022",
);

returns:

react

The HTTP plugin then returns:

{
  path: "react",
  namespace: "xsite-runtime",
}

Both of the following forms therefore use the same host runtime adapter:

react
/[email protected]/es2022/react.mjs

11. Runtime Peer Mapping

runtime-peers.ts is the single source of truth for peer dependency mappings.

Bare imports are mapped through BARE_PEER_RUNTIME_MODULES.

CDN URL imports are mapped through mapCdnPathToRuntimeModule.

Bare peer examples

export const BARE_PEER_RUNTIME_MODULES = [
  { filter: /^react$/, path: "react" },
  {
    filter: /^react\/jsx-runtime$/,
    path: "react-jsx-runtime",
  },
  {
    filter: /^react\/jsx-dev-runtime$/,
    path: "react-jsx-dev-runtime",
  },
  { filter: /^react-dom$/, path: "react-dom" },
  { filter: /^next\/image$/, path: "next-image" },
  { filter: /^next\/link$/, path: "next-link" },
  {
    filter: /^next\/navigation$/,
    path: "next-navigation",
  },
  {
    filter: /^@xsite\/common$/,
    path: "xsite-common",
  },
];

CDN path examples

/react@19/.../jsx-runtime
→ react-jsx-runtime

/react-dom@19/...
→ react-dom

/next@15/.../image
→ next-image

/@xsite/[email protected]/client
→ xsite-common-client

/@xsite/[email protected]/block-renderer
→ xsite-core-block-renderer

/lucide-react@...
→ lucide-react

Runtime Adapter Generation

12. The Host Runtime Plugin Loads Virtual Modules

The host plugin registers an onLoad handler:

buildApi.onLoad(
  {
    filter: /.*/,
    namespace: XSITE_RUNTIME_NAMESPACE,
  },
  ({ path: runtimeModulePath }) => {
    switch (runtimeModulePath) {
      // Generate adapter source
    }
  },
);

Each virtual module exports values from:

globalThis.__XSITE_RUNTIME__

The plugin does not import React, Next.js, or XSite libraries directly into the final remote bundle.

It generates bridge code that reads them from the host application at runtime.


13. React Adapter

A remote import such as:

import React, { useState } from "react";

is mapped to the virtual runtime module:

xsite-runtime:react

The generated adapter is conceptually:

const runtime =
  globalThis.__XSITE_RUNTIME__;
 
if (!runtime?.React) {
  throw new Error(
    "XSITE runtime is missing React"
  );
}
 
const React = runtime.React;
 
export default React;
export const useState = React.useState;
export const useEffect = React.useEffect;
export const createElement = React.createElement;

The remote component uses the host React instance.

This avoids loading a second React copy and prevents common hook and context problems.

Host React instance
        ↓
globalThis.__XSITE_RUNTIME__.React
        ↓
Remote component

14. JSX Runtime Adapter

Compiled JSX often imports:

import {
  jsx,
  jsxs,
  Fragment,
} from "react/jsx-runtime";

This is mapped to:

xsite-runtime:react-jsx-runtime

The adapter reads:

globalThis.__XSITE_RUNTIME__.jsxRuntime

and exports:

export const jsx = runtime.jsx;
export const jsxs = runtime.jsxs;
export const Fragment = runtime.Fragment;

Development JSX is handled separately through:

react/jsx-dev-runtime
→ xsite-runtime:react-jsx-dev-runtime

15. Next.js Image Adapter

Original import:

import Image from "next/image";

Runtime mapping:

next/image
→ xsite-runtime:next-image

Generated adapter:

const Image =
  globalThis.__XSITE_RUNTIME__
    ?.components
    ?.Image;
 
if (!Image) {
  throw new Error(
    "XSITE runtime is missing components.Image"
  );
}
 
export default Image;

The browser host must initialize:

globalThis.__XSITE_RUNTIME__.components.Image

before importing the compiled remote module.


16. Next.js Link Adapter

Original import:

import Link from "next/link";

Runtime mapping:

next/link
→ xsite-runtime:next-link

Generated adapter:

const Link =
  globalThis.__XSITE_RUNTIME__
    ?.components
    ?.Link;
 
if (!Link) {
  throw new Error(
    "XSITE runtime is missing components.Link"
  );
}
 
export default Link;

17. Next.js Navigation Adapter

Original imports may include:

import {
  useRouter,
  usePathname,
  useSearchParams,
} from "next/navigation";

These are mapped to:

xsite-runtime:next-navigation

The generated adapter reads:

globalThis.__XSITE_RUNTIME__.navigation

and exports host navigation functions.

The host runtime is expected to provide:

navigation: {
  useSearchParams,
  usePathname,
  useRouter,
  useParams,
  redirect,
  notFound,
}

18. @xsite/common Adapter

Original import:

import {
  getImage,
  textHighlighter,
} from "@xsite/common";

Runtime mapping:

@xsite/common
→ xsite-runtime:xsite-common

The adapter source is generated through:

createCommonRuntimeModule();

Related variants are generated through:

createCommonClientRuntimeModule();
createCommonServerRuntimeModule();

These adapters expose selected functions from the host runtime instead of bundling another copy of @xsite/common.


19. XSite Core Adapters

Supported XSite core modules include:

@xsite/core/dynamicZoneManager.client
@xsite/core/dynamicZoneManager
@xsite/core/localized-link
@xsite/core/language-switcher
@xsite/core/mediaPreviewer
@xsite/core/block-renderer
@xsite/core/fetchContentType.remote
@xsite/core/fetchContentType

Examples:

@xsite/core/dynamicZoneManager.client
→ core.dynamicZoneManagerClient

@xsite/core/dynamicZoneManager
→ core.dynamicZoneManager

@xsite/core/mediaPreviewer
→ core.mediaPreviewer

@xsite/core/fetchContentType.remote
→ core.fetchContentTypeRemote

The generic adapter helpers include:

createDefaultRuntimeModule(runtimePath);
createNamedRuntimeModule(runtimePath, exports);

20. Block Renderer Adapter

Original import:

import {
  BlockRenderer,
} from "@xsite/core/block-renderer";

Generated adapter:

const runtime =
  globalThis.__XSITE_RUNTIME__;
 
if (!runtime?.core?.BlockRenderer) {
  throw new Error(
    "XSITE runtime is missing core.BlockRenderer"
  );
}
 
export const BlockRenderer =
  runtime.core.BlockRenderer;
 
export default runtime.core.BlockRenderer;

The browser host must provide:

globalThis.__XSITE_RUNTIME__
  .core
  .BlockRenderer

21. ReactDOM Adapter

Original import:

import ReactDOM from "react-dom";

Runtime mapping:

react-dom
→ xsite-runtime:react-dom

The generated adapter reads:

globalThis.__XSITE_RUNTIME__.ReactDOM

and exposes:

createPortal
flushSync
version

22. Lucide Adapter

Original import:

import {
  ArrowRight,
  Menu,
} from "lucide-react";

Runtime mapping:

lucide-react
→ xsite-runtime:lucide-react

The adapter is generated by:

createLucideRuntimeModule();

The generated implementation must expose the icon exports expected by the remote package.


23. Process Polyfill

A CDN dependency may import:

/node/process.mjs

The mapper converts it to:

xsite-runtime:node-process

The host plugin injects selected environment values during server compilation:

const apiUrl =
  process.env.NEXT_PUBLIC_API_URL ?? "";
 
const nodeEnv =
  process.env.NODE_ENV ?? "development";

Generated output:

const env = {
  NEXT_PUBLIC_API_URL: "...",
  NODE_ENV: "production",
};
 
const proc = {
  env,
  browser: true,
  version: "",
};
 
export default proc;
export { env };

Unlike React and Next.js adapters, these values are embedded into the compiled output during the server phase.


Remote Dependency Resolution

24. Relative Imports Are Converted to Absolute URLs

Assume the About1 module contains:

import AboutCard from "./AboutCard.mjs";

The HTTP plugin calls:

toAbsoluteEsmUrl(
  "./AboutCard.mjs",
  args.importer,
);

If the importer is:

https://esm.xsite.live/@xsite/[email protected]/es2022/ui-about1.mjs

the resolved URL becomes:

https://esm.xsite.live/@xsite/[email protected]/es2022/AboutCard.mjs

The dependency is then:

  1. Checked against the allowed hosts
  2. Fetched through cachedFetch
  3. Loaded into esbuild
  4. Included in the final bundle

25. Root-Relative CDN Imports

An import such as:

import dependency from "/[email protected]/es2022/dependency.mjs";

is converted into an absolute URL through:

`${getEsmShOrigin()}${spec}`

Example:

/[email protected]/es2022/embla-carousel-react.mjs

becomes:

https://esm.xsite.live/[email protected]/es2022/embla-carousel-react.mjs

26. Absolute HTTP Imports

Absolute HTTP imports are accepted when their host is allowed:

if (/^https?:\/\//.test(spec)) {
  return normalizeEsmUrl(spec);
}

Examples:

https://esm.xsite.live/...
http://localhost/...
http://127.0.0.1/...

27. Blocked URL Types

The HTTP plugin does not process:

blob:
data:

The function returns null:

if (/^(blob:|data:)/.test(spec)) {
  return null;
}

28. Allowed Host Validation

Remote HTTP modules are restricted by:

function isAllowedEsmHost(
  abs: string,
): boolean {

The allowed hosts are:

  • The configured ESM origin
  • localhost
  • 127.0.0.1

Example:

https://esm.xsite.live/...
→ allowed

http://localhost/...
→ allowed

https://unknown-cdn.example.com/...
→ not handled by the plugin

This protects the compiler from fetching arbitrary remote origins.


29. Version URL Normalization

The HTTP plugin strips npm range operators from URLs.

Examples:

/pkg@^1.2.3
→ /[email protected]

/pkg@~1.2.3
→ /[email protected]

/pkg@>=1.2.3
→ /[email protected]

Implementation:

u.pathname = u.pathname.replace(
  /@[\^~>=<]+(?=\d)/g,
  "@",
);

This normalizes URLs but does not perform a full semantic-version range resolution.

The CDN must still understand the resulting version path.


Bundling

30. esbuild Builds the Module Graph

For About1, the dependency graph may look like:

about1 entry
├── ./AboutCard.mjs
│   ├── ./Icon.mjs
│   └── xsite-runtime:react-jsx-runtime
├── ./utils.mjs
├── xsite-runtime:react
├── xsite-runtime:react-jsx-runtime
├── xsite-runtime:next-image
├── xsite-runtime:next-link
├── xsite-runtime:xsite-common
└── xsite-runtime:xsite-core-block-renderer

Private implementation modules are fetched and bundled.

Host-provided dependencies are represented by generated runtime adapters.


31. What Is Bundled

The final output usually includes:

About1 implementation
AboutCard implementation
Private helper modules
Package-local utility modules
Remote dependencies not mapped as host peers
Generated runtime adapter code

32. What Is Not Bundled as a Full Library

The following are provided by the host runtime:

react
react-dom
react/jsx-runtime
react/jsx-dev-runtime
next/image
next/link
next/navigation
@xsite/common
@xsite/common/client
@xsite/common/server
Selected @xsite/core modules
lucide-react

Adapter code is bundled, but the actual package implementation is not duplicated.


33. Build Configuration

The compiler uses:

bundle: true,
write: false,
format: "esm",
platform: "browser",
target: "es2022",
treeShaking: true,
minify:
  process.env.NODE_ENV === "production",
sourcemap: false,

Result

  • One browser-compatible ESM output
  • No file written by esbuild
  • ES2022 browser target
  • Unused code removed where possible
  • Production output minified
  • Development output more readable

Default Export Handling

34. Why a Default Export Is Required

The remote loader or Puck integration usually expects:

remoteModule.default

However, a package may only export a named component:

export { About1 };

ensureDefaultExport adds a default export when possible.


35. Existing Default Exports Are Preserved

The helper returns immediately when it finds:

export default About1;

or:

export { About1 as default };

Detection:

if (/\bexport\s+default\b/.test(code)) {
  return code;
}
 
if (/\bas\s+default\b/.test(code)) {
  return code;
}

36. Named PascalCase Export Selection

If no default export exists, the helper reads the final export block.

Example:

export {
  helper,
  aboutData,
  About1,
};

It selects the first likely component export based on PascalCase naming:

About1

Then it appends:

export { About1 as default };

Final output:

export {
  helper,
  aboutData,
  About1,
};
 
export { About1 as default };

37. Compiler Return Value

After compilation:

return {
  code,
  resolvedUrl: source.url,
};

The output contains JavaScript source, not a React component instance.

The compiled code must still be served to and imported by the browser.


Phase 2: Browser Execution

38. Serving the Compiled Module

A server route typically calls:

const result =
  await compileRemoteComponent(source);

and returns:

return new Response(result.code, {
  headers: {
    "Content-Type":
      "text/javascript; charset=utf-8",
  },
});

The browser receives a URL such as:

/api/remote-component?url=<encoded-remote-url>

The response body is the compiled ESM module.


39. Host Runtime Initialization

Before importing the compiled module, the browser must initialize:

globalThis.__XSITE_RUNTIME__

Example shape:

globalThis.__XSITE_RUNTIME__ = {
  React,
  ReactDOM,
  jsxRuntime,
  jsxDevRuntime,
 
  components: {
    Image,
    Link,
  },
 
  navigation: {
    useSearchParams,
    usePathname,
    useRouter,
    useParams,
    redirect,
    notFound,
  },
 
  common: {
    getImage,
    textHighlighter,
  },
 
  core: {
    dynamicZoneManagerClient,
    dynamicZoneManager,
    mediaPreviewer,
    fetchContentTypeRemote,
    BlockRenderer,
 
    localizedLink: {
      LocalizedLink,
    },
 
    languageSwitcher: {
      LocaleSwitcher,
    },
  },
};

The exact runtime shape must match the paths expected by the generated adapters.


40. Runtime Must Be Initialized Before Import

Correct order:

initializeRemoteRuntime();
 
const remoteModule =
  await import(compiledModuleUrl);

Incorrect order:

const remoteModule =
  await import(compiledModuleUrl);
 
initializeRemoteRuntime();

The second order fails because the generated adapters execute as soon as the module is imported.

Possible errors include:

XSITE runtime is missing React
XSITE runtime is missing jsxRuntime
XSITE runtime is missing components.Image
XSITE runtime is missing components.Link
XSITE runtime is missing navigation
XSITE runtime is missing core.BlockRenderer

41. Dynamic Import

The browser loads the compiled ESM:

const remoteModule = await import(
  /* webpackIgnore: true */
  compiledModuleUrl
);

At import time:

  1. The compiled JavaScript executes
  2. Runtime adapters access globalThis.__XSITE_RUNTIME__
  3. Bundled component functions are created
  4. ESM exports are registered
  5. module.default becomes available

42. Reading the Component Export

const About1 =
  remoteModule.default;

Because ensureDefaultExport ran during compilation, the loader can consistently use the default export.


43. Rendering Through React or Puck

Direct React rendering:

<About1
  title="About Us"
  image={image}
  blocks={blocks}
/>

Lazy React rendering:

const LazyAbout1 = React.lazy(
  async () => {
    const module =
      await import(compiledModuleUrl);
 
    return {
      default: module.default,
    };
  },
);

Puck registration may look like:

config.components.About1 = {
  render: About1,
  fields: descriptor.fields,
  defaultProps: defaults,
};

Full About1 Walkthrough

Assume the original remote component is:

import Image from "next/image";
import Link from "next/link";
 
import {
  getImage,
} from "@xsite/common";
 
import {
  BlockRenderer,
} from "@xsite/core/block-renderer";
 
export function About1({
  title,
  image,
  blocks,
}) {
  return (
    <section>
      <Image
        src={getImage(image)}
        alt={title}
      />
 
      <h2>{title}</h2>
 
      <BlockRenderer
        blocks={blocks}
      />
 
      <Link href="/about">
        Read more
      </Link>
    </section>
  );
}

The full flow is:

1. The host receives the About1 ESM URL.

2. The host calls compileRemoteComponent.

3. esbuild starts from __xsite_remote_entry__.

4. xsite-remote-entry maps the virtual entry to the About1 URL.

5. esm-http downloads the About1 module.

6. fetchRemoteSource may follow an ESM CDN stub to the leaf module.

7. esbuild parses About1 imports.

8. react/jsx-runtime maps to:
   xsite-runtime:react-jsx-runtime

9. next/image maps to:
   xsite-runtime:next-image

10. next/link maps to:
    xsite-runtime:next-link

11. @xsite/common maps to:
    xsite-runtime:xsite-common

12. @xsite/core/block-renderer maps to:
    xsite-runtime:xsite-core-block-renderer

13. Relative private files are resolved against the leaf module URL.

14. Relative private files are downloaded and bundled.

15. Host runtime adapter source is generated.

16. esbuild creates one ESM output.

17. ensureDefaultExport sees export { About1 }.

18. ensureDefaultExport appends:
    export { About1 as default }.

19. The server returns the compiled JavaScript.

20. The browser initializes globalThis.__XSITE_RUNTIME__.

21. The browser dynamically imports the compiled module.

22. The generated adapters read:
    runtime.jsxRuntime
    runtime.components.Image
    runtime.components.Link
    runtime.common.getImage
    runtime.core.BlockRenderer

23. remoteModule.default resolves to About1.

24. React or Puck renders About1.

Server and Browser Responsibilities

OperationServerBrowser
Resolve remote package URLYesNo
Fetch remote ESM sourceYesNo
Follow ESM CDN stubsYesNo
Resolve relative importsYesNo
Map peer dependenciesYesNo
Generate runtime adaptersYesNo
Bundle remote modulesYesNo
Ensure default exportYesNo
Initialize __XSITE_RUNTIME__NoYes
Execute compiled moduleNoYes
Read host React and Next.js componentsNoYes
Render the componentNoYes

Resolution Time vs Execution Time

This distinction is critical.

Server Compilation Time

The host runtime plugin generates code such as:

const React =
  globalThis.__XSITE_RUNTIME__.React;

It does not access the browser runtime while compiling.

The server is only generating JavaScript text.

Browser Execution Time

When the browser imports the compiled module, that generated JavaScript executes.

Only then does it read:

globalThis.__XSITE_RUNTIME__

Mental model:

Server:
"Generate code that will read the runtime later."

Browser:
"Execute that code and read the runtime now."

Simplified Transformation

The original remote imports:

import React from "react";
import Image from "next/image";
import {
  getImage,
} from "@xsite/common";

are conceptually transformed into:

const React =
  globalThis.__XSITE_RUNTIME__
    .React;
 
const Image =
  globalThis.__XSITE_RUNTIME__
    .components
    .Image;
 
const getImage =
  globalThis.__XSITE_RUNTIME__
    .common
    .getImage;

A private relative import:

import AboutCard from "./AboutCard.mjs";

is instead fetched and bundled into the final module.

The core rule is:

Host dependency
→ runtime adapter

Remote implementation dependency
→ fetch and bundle

Error Points

Remote Fetch Errors

Possible causes:

  • Remote URL unavailable
  • ESM CDN unavailable
  • Authentication failure
  • Invalid JavaScript response
  • Disallowed remote host
  • Broken relative dependency path

Typical location:

fetchRemoteSource(url);

Compilation Errors

Possible causes:

  • Unsupported syntax
  • Missing export
  • Unresolved import
  • Invalid source map comment
  • Dependency not handled by any plugin
  • esbuild native binary unavailable

Typical location:

await build({...});

Missing Compilation Output

The compiler explicitly checks:

if (!code) {
  throw Object.assign(
    new Error(
      "Compilation produced no output",
    ),
    {
      status: 500,
    },
  );
}

Missing Host Runtime Values

Examples:

XSITE runtime is missing React
XSITE runtime is missing ReactDOM
XSITE runtime is missing jsxRuntime
XSITE runtime is missing components.Image
XSITE runtime is missing components.Link
XSITE runtime is missing navigation
XSITE runtime is missing core.BlockRenderer

These indicate that the module compiled successfully but the browser runtime contract was incomplete.


Missing Default Export

If the final module has no default export and no named PascalCase export, ensureDefaultExport cannot determine which export is the component.

The caller may later receive:

remoteModule.default === undefined

For reliable loading, component packages should ideally provide an explicit default export.


Caching Layers

The architecture may use several different caches.

HTTP Plugin Fetch Cache

Map<string, Promise<FetchedSource>>

Purpose:

  • Prevent repeated fetches during one compilation
  • Deduplicate concurrent dependency requests

Scope:

  • In memory
  • Per plugin instance
  • Per compilation

Server Compilation Cache

This is not shown in the provided functions but may exist around compileRemoteComponent.

Possible cache key:

[
  "remote-component-compile",
  packageId,
  packageVersion,
  artifactRevision,
]

Purpose:

  • Avoid recompiling unchanged remote packages
  • Invalidate when package code or artifact metadata changes

Browser HTTP Cache

The compiled route response may be cached depending on response headers.

Example strategy:

Cache-Control: public, max-age=31536000, immutable

This is appropriate only when the compiled URL is versioned and immutable.


Browser Module Cache

Dynamic import() caches modules by URL.

Importing the exact same URL twice usually returns the same module instance.

To force a new browser module load, the URL must change.

Example:

/api/remote-component?...&revision=15

Runtime Contract

The host and remote compiler must agree on the runtime contract.

Example TypeScript model:

type XSiteRuntime = {
  React: typeof import("react");
 
  ReactDOM:
    typeof import("react-dom");
 
  jsxRuntime:
    typeof import("react/jsx-runtime");
 
  jsxDevRuntime?:
    typeof import(
      "react/jsx-dev-runtime"
    );
 
  components: {
    Image: React.ComponentType<any>;
    Link: React.ComponentType<any>;
  };
 
  navigation: {
    useSearchParams: Function;
    usePathname: Function;
    useRouter: Function;
    useParams: Function;
    redirect: Function;
    notFound: Function;
  };
 
  common: Record<
    string,
    unknown
  >;
 
  core: {
    dynamicZoneManagerClient?: unknown;
    dynamicZoneManager?: unknown;
    mediaPreviewer?: unknown;
    fetchContentTypeRemote?: unknown;
    BlockRenderer?: unknown;
    localizedLink?: {
      LocalizedLink: unknown;
    };
    languageSwitcher?: {
      LocaleSwitcher: unknown;
    };
  };
};

The runtime adapter mappings and runtime initialization must evolve together.

Adding a peer mapping without adding the corresponding runtime value causes browser execution failure.


Recommended Package Contract

A remote UI package should preferably:

  1. Export its React component as the default export
  2. Use supported peer imports
  3. Avoid Node-only APIs
  4. Avoid importing arbitrary external hosts
  5. Keep relative imports valid after CDN transformation
  6. Use browser-compatible ESM
  7. Declare React and host frameworks as peers
  8. Avoid bundling its own React instance
  9. Avoid depending on unregistered runtime exports
  10. Use immutable versioned URLs

Example:

import type {
  FC,
} from "react";
 
export type About1Props = {
  title: string;
};
 
const About1: FC<
  About1Props
> = ({
  title,
}) => {
  return (
    <section>
      <h2>{title}</h2>
    </section>
  );
};
 
export default About1;

Current Code Observations

Duplicate Bare Peer Mapping

The current peer list includes the same block-renderer mapping more than once:

{
  filter:
    /^@xsite\/core\/block-renderer$/,
  path:
    "xsite-core-block-renderer",
}

The duplicate does not normally change behavior because both entries return the same runtime module.

It should still be removed to keep the mapping contract clear.


Broad CDN Block Renderer Match

The CDN mapper contains:

if (
  /block-renderer|blockRenderer/
    .test(pathOnly)
) {
  return "xsite-core-block-renderer";
}

This check is broad.

Any allowed CDN module whose path contains block-renderer may be mapped to the XSite core runtime adapter.

A safer match would verify that the path belongs to @xsite/core.

Example:

if (
  (
    /\/@xsite\/core@/.test(pathOnly) ||
    /\/@xsite\/core\//.test(pathOnly)
  ) &&
  /block-renderer|blockRenderer/
    .test(pathOnly)
) {
  return "xsite-core-block-renderer";
}

Version Normalization Limitation

The version normalizer removes semver range symbols:

@^1.2.3
→ @1.2.3

It does not calculate the latest package version satisfying the original range.

This is safe only when the source CDN already generated a concrete compatible URL or accepts the normalized version.


Default Export Heuristic

ensureDefaultExport chooses a component based on PascalCase naming.

This works for common React component packages but is heuristic.

For example:

export {
  About1,
  AboutCard,
};

The selected component depends on export order.

The strongest package contract is still:

export default About1;

Debugging Checklist

When a remote component does not load, verify the flow in this order.

1. Source Resolution

Confirm the compiler receives the expected URL:

console.log(source.url);

2. Entry Fetch

Confirm fetchRemoteSource successfully downloads the entry module.

Log:

console.log({
  requestedUrl: url,
  resolvedUrl:
    fetched.resolvedUrl,
});

3. Import Resolution

Log each import inside onResolve:

console.log({
  path: args.path,
  importer: args.importer,
  namespace: args.namespace,
});

4. Runtime Mapping

Confirm host dependencies map to the expected virtual modules:

next/image
→ next-image

@xsite/common
→ xsite-common

5. Remote Dependency URLs

Confirm relative imports resolve against the final leaf URL.


6. Allowed Host

Confirm the dependency hostname matches:

new URL(
  getEsmShOrigin(),
).hostname

7. Build Output

Confirm esbuild creates:

result.outputFiles?.[0]?.text

8. Default Export

Inspect the final output for:

export {
  About1 as default,
};

or:

export default About1;

9. Runtime Initialization

Before dynamic import, inspect:

console.log(
  globalThis.__XSITE_RUNTIME__,
);

10. Browser Import

Confirm the browser receives JavaScript with the correct content type:

Content-Type: text/javascript

11. Final Module

Inspect:

const module =
  await import(compiledUrl);
 
console.log(module);

Expected:

{
  default: About1,
  About1: About1,
}

Final Mental Model

The remote loader is not simply doing:

import(remoteUrl);

Instead, it performs a controlled compilation pipeline:

Remote package URL
        ↓
Download package source
        ↓
Follow ESM CDN redirects or stubs
        ↓
Parse all imports
        ↓
Replace host framework dependencies
        ↓
Download private implementation dependencies
        ↓
Bundle everything into one browser ESM
        ↓
Ensure a default React component export
        ↓
Serve compiled ESM to the browser
        ↓
Initialize the host runtime
        ↓
Dynamically import the compiled module
        ↓
Render the remote component

The key architectural rule is:

Remote package owns:
- Its component implementation
- Its private helper modules
- Its package-specific behavior

Host application owns:
- React
- ReactDOM
- JSX runtime
- Next.js Image and Link
- Navigation APIs
- Shared XSite utilities
- Shared XSite core services

This creates a controlled remote-component system where component implementations can be delivered remotely while still sharing the host application's framework runtime and platform services.