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=es2022The 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 componentThe 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:
| Component | Responsibility |
|---|---|
compileRemoteComponent | Coordinates esbuild compilation |
createEsmHttpPlugin | Downloads and resolves remote ESM modules |
runtime-peers.ts | Maps host dependencies to virtual runtime module IDs |
createHostRuntimePlugin | Generates adapters that read from globalThis.__XSITE_RUNTIME__ |
Additional supporting functions include:
| Function | Responsibility |
|---|---|
fetchRemoteSource | Fetches remote JavaScript and follows CDN stubs |
ensureDefaultExport | Adds a default export when only a named component export exists |
initializeRemoteRuntime | Installs host React, Next.js components, and XSite utilities in the browser |
mapCdnPathToRuntimeModule | Recognizes 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 process3. 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 madeThis 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.mjsand 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 outputHost 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-rendererThese 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:
reactThe 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.mjs11. 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-reactRuntime 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:reactThe 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 component14. JSX Runtime Adapter
Compiled JSX often imports:
import {
jsx,
jsxs,
Fragment,
} from "react/jsx-runtime";This is mapped to:
xsite-runtime:react-jsx-runtimeThe adapter reads:
globalThis.__XSITE_RUNTIME__.jsxRuntimeand 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-runtime15. Next.js Image Adapter
Original import:
import Image from "next/image";Runtime mapping:
next/image
→ xsite-runtime:next-imageGenerated 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.Imagebefore importing the compiled remote module.
16. Next.js Link Adapter
Original import:
import Link from "next/link";Runtime mapping:
next/link
→ xsite-runtime:next-linkGenerated 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-navigationThe generated adapter reads:
globalThis.__XSITE_RUNTIME__.navigationand 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-commonThe 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/fetchContentTypeExamples:
@xsite/core/dynamicZoneManager.client
→ core.dynamicZoneManagerClient
@xsite/core/dynamicZoneManager
→ core.dynamicZoneManager
@xsite/core/mediaPreviewer
→ core.mediaPreviewer
@xsite/core/fetchContentType.remote
→ core.fetchContentTypeRemoteThe 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
.BlockRenderer21. ReactDOM Adapter
Original import:
import ReactDOM from "react-dom";Runtime mapping:
react-dom
→ xsite-runtime:react-domThe generated adapter reads:
globalThis.__XSITE_RUNTIME__.ReactDOMand exposes:
createPortal
flushSync
version22. Lucide Adapter
Original import:
import {
ArrowRight,
Menu,
} from "lucide-react";Runtime mapping:
lucide-react
→ xsite-runtime:lucide-reactThe 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.mjsThe mapper converts it to:
xsite-runtime:node-processThe 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.mjsthe resolved URL becomes:
https://esm.xsite.live/@xsite/[email protected]/es2022/AboutCard.mjsThe dependency is then:
- Checked against the allowed hosts
- Fetched through
cachedFetch - Loaded into esbuild
- 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.mjsbecomes:
https://esm.xsite.live/[email protected]/es2022/embla-carousel-react.mjs26. 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
localhost127.0.0.1
Example:
https://esm.xsite.live/...
→ allowed
http://localhost/...
→ allowed
https://unknown-cdn.example.com/...
→ not handled by the pluginThis 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-rendererPrivate 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 code32. 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-reactAdapter 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.defaultHowever, 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:
About1Then 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.BlockRenderer41. Dynamic Import
The browser loads the compiled ESM:
const remoteModule = await import(
/* webpackIgnore: true */
compiledModuleUrl
);At import time:
- The compiled JavaScript executes
- Runtime adapters access
globalThis.__XSITE_RUNTIME__ - Bundled component functions are created
- ESM exports are registered
module.defaultbecomes 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
| Operation | Server | Browser |
|---|---|---|
| Resolve remote package URL | Yes | No |
| Fetch remote ESM source | Yes | No |
| Follow ESM CDN stubs | Yes | No |
| Resolve relative imports | Yes | No |
| Map peer dependencies | Yes | No |
| Generate runtime adapters | Yes | No |
| Bundle remote modules | Yes | No |
| Ensure default export | Yes | No |
Initialize __XSITE_RUNTIME__ | No | Yes |
| Execute compiled module | No | Yes |
| Read host React and Next.js components | No | Yes |
| Render the component | No | Yes |
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 bundleError 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.BlockRendererThese 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 === undefinedFor 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, immutableThis 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=15Runtime 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:
- Export its React component as the default export
- Use supported peer imports
- Avoid Node-only APIs
- Avoid importing arbitrary external hosts
- Keep relative imports valid after CDN transformation
- Use browser-compatible ESM
- Declare React and host frameworks as peers
- Avoid bundling its own React instance
- Avoid depending on unregistered runtime exports
- 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.3It 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-common5. Remote Dependency URLs
Confirm relative imports resolve against the final leaf URL.
6. Allowed Host
Confirm the dependency hostname matches:
new URL(
getEsmShOrigin(),
).hostname7. Build Output
Confirm esbuild creates:
result.outputFiles?.[0]?.text8. 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/javascript11. 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 componentThe 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 servicesThis 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.