Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Plugin build: `tsdown` (`packages/astro/tsdown.config.ts`).
Module Federation plugin: `@module-federation/vite`.
Astro bridge package in this repo: `@module-federation/astro`.
Published package source: `packages/astro`.
Example apps: `apps/example` (see `apps/example/README.md`).
Example apps: `apps/example` (see `apps/example/README.md`) and `apps/react`.

## Package usage

Expand Down Expand Up @@ -36,6 +36,7 @@ export default defineConfig({
- SSR remote imports in Astro frontmatter are handled by an SSR transform path in `@module-federation/astro`.
- That SSR path supports remote Astro components as well as plain server functions, including `await import('remote/Component')` followed by `<Component />`.
- Dev target defaults to runtime inference (`ENV_TARGET = undefined`) so client/server contexts can coexist.
- A host consuming React remotes should configure `@astrojs/react`, `react`, and `react-dom` explicitly on the host.

## Build checks

Expand Down
18 changes: 18 additions & 0 deletions apps/react/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# React federation example

Scenario:

- host Astro app consumes a federated React component
- host owns React setup explicitly with `@astrojs/react`
- federation handles the remote module loading only

Apps:

- host: `apps/react/host` -> `http://localhost:4331`
- remote: `apps/react/remote` -> `http://localhost:4332`

Run both:

```sh
pnpm dev:react
```
34 changes: 34 additions & 0 deletions apps/react/host/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @ts-check
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import react from '@astrojs/react';
import { moduleFederation } from '@module-federation/astro';

export default defineConfig({
output: 'server',
server: {
port: 4331,
strictPort: true,
},
preview: {
port: 4331,
strictPort: true,
},
adapter: node({
mode: 'standalone',
}),
integrations: [
react(),
moduleFederation({
name: 'react_host',
remotes: {
react_remote: 'react_remote@http://localhost:4332/mf-manifest.json',
},
ssr: {
localRemotes: {
react_remote: '../remote',
},
},
}),
],
});
23 changes: 23 additions & 0 deletions apps/react/host/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "react-host",
"private": true,
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev --host localhost --port 4331",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/node": "^10.0.4",
"@astrojs/react": "^5.0.2",
"@module-federation/astro": "workspace:*",
"astro": "^6.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"vite": "^6.4.1"
}
}
78 changes: 78 additions & 0 deletions apps/react/host/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
import CounterCard from 'react_remote/components/CounterCard';

const ssrCount = 0;
const ssrTitle = 'Server-rendered federated React card';
---

<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>React host</title>
</head>
<body style="background:#f3efe3;color:#1c1b18;font-family:Georgia,serif;margin:0;min-height:100vh;">
<main style="display:grid;gap:2rem;max-width:72rem;margin:0 auto;padding:4rem 1.5rem;">
<div style="display:grid;gap:0.6rem;max-width:42rem;">
<p style="color:#8a4b08;letter-spacing:0.18em;margin:0;text-transform:uppercase;">host app</p>
<h1 style="font-size:clamp(3rem,8vw,5.5rem);line-height:0.92;margin:0;">Astro host; React explicit, remote federated.</h1>
<p style="font-size:1.08rem;line-height:1.65;margin:0;opacity:0.8;">
The host owns its React setup through <code>@astrojs/react</code>, <code>react</code>, and <code>react-dom</code>.
Federation only loads the remote module boundary.
</p>
</div>

<section style="display:grid;gap:1.25rem;">
<h2 style="font-size:1.1rem;letter-spacing:0.08em;margin:0;text-transform:uppercase;">SSR render</h2>
<div id="ssr-remote-counter">
<CounterCard title={ssrTitle} count={ssrCount} tone="sun" />
</div>
</section>

<section style="display:grid;gap:1.25rem;">
<h2 style="font-size:1.1rem;letter-spacing:0.08em;margin:0;text-transform:uppercase;">Client-only render</h2>
<div id="client-only-remote-counter"></div>
</section>
</main>

<script>
import React, { useState } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import RemoteCounterCard from 'react_remote/components/CounterCard';

const ssrMountNode = document.querySelector('#ssr-remote-counter');
const mountNode = document.querySelector('#client-only-remote-counter');

if (ssrMountNode) {
function HydratedRemoteCounter() {
const [count, setCount] = useState(0);

return React.createElement(RemoteCounterCard, {
title: 'Server-rendered federated React card',
count,
onIncrement: () => setCount((value) => value + 1),
tone: 'sun',
});
}

hydrateRoot(ssrMountNode, React.createElement(HydratedRemoteCounter));
}

if (mountNode) {
function ClientOnlyRemoteCounter() {
const [count, setCount] = useState(0);

return React.createElement(RemoteCounterCard, {
title: 'Client-rendered federated React card',
count,
onIncrement: () => setCount((value) => value + 1),
tone: 'ink',
});
}

createRoot(mountNode).render(React.createElement(ClientOnlyRemoteCounter));
}
</script>
</body>
</html>
5 changes: 5 additions & 0 deletions apps/react/host/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
44 changes: 44 additions & 0 deletions apps/react/remote/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// @ts-check
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import react from '@astrojs/react';
import { moduleFederation } from '@module-federation/astro';

export default defineConfig({
output: 'server',
server: {
port: 4332,
strictPort: true,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
},
},
preview: {
port: 4332,
strictPort: true,
},
vite: {
server: {
cors: true,
origin: 'http://localhost:4332',
},
},
adapter: node({
mode: 'standalone',
}),
integrations: [
react(),
moduleFederation({
name: 'react_remote',
filename: 'remoteEntry.js',
publicPath: 'http://localhost:4332/',
varFilename: 'remoteEntry.global.js',
manifest: true,
dts: false,
exposes: {
'./components/CounterCard': './src/components/CounterCard.tsx',
},
}),
],
});
25 changes: 25 additions & 0 deletions apps/react/remote/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "react-remote",
"private": true,
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "pnpm run build && astro preview --host localhost --port 4332",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/node": "^10.0.4",
"@astrojs/react": "^5.0.2",
"@module-federation/astro": "workspace:*",
"astro": "^6.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"vite": "^6.4.1"
}
}
100 changes: 100 additions & 0 deletions apps/react/remote/src/components/CounterCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';

type CounterCardProps = {
title: string;
count: number;
onIncrement?: () => void;
tone?: 'sun' | 'ink';
};

const THEMES = {
sun: {
background: 'linear-gradient(135deg, rgba(107,57,18,0.96), rgba(55,29,8,0.96))',
border: '#c97a1d',
label: '#ffbf47',
buttonBackground: '#ffbf47',
buttonText: '#18181b',
},
ink: {
background: 'linear-gradient(135deg, rgba(9,24,41,0.98), rgba(24,71,99,0.96))',
border: '#38bdf8',
label: '#7dd3fc',
buttonBackground: '#38bdf8',
buttonText: '#082f49',
},
} as const;

export default function CounterCard({
title,
count,
onIncrement,
tone = 'sun',
}: CounterCardProps) {
const theme = THEMES[tone];

return (
<section
style={{
background: theme.background,
border: `1px solid ${theme.border}`,
borderRadius: '24px',
boxShadow: '0 24px 60px rgba(15, 23, 42, 0.24)',
color: '#fff7ea',
display: 'grid',
gap: '1rem',
maxWidth: '26rem',
padding: '1.25rem',
}}
>
<div style={{ display: 'grid', gap: '0.35rem' }}>
<p
style={{
color: theme.label,
fontSize: '0.72rem',
letterSpacing: '0.14em',
margin: 0,
textTransform: 'uppercase',
}}
>
remote: react
</p>
<h2 style={{ fontSize: '1.5rem', lineHeight: 1.05, margin: 0 }}>{title}</h2>
</div>

<div
style={{
alignItems: 'center',
display: 'flex',
gap: '0.9rem',
justifyContent: 'space-between',
}}
>
<div>
<p style={{ fontSize: '0.75rem', margin: 0, opacity: 0.72, textTransform: 'uppercase' }}>
count
</p>
<strong style={{ fontSize: '3rem', lineHeight: 0.9 }}>{count}</strong>
</div>

<button
type="button"
onClick={onIncrement}
disabled={!onIncrement}
style={{
background: theme.buttonBackground,
border: 0,
borderRadius: '999px',
color: theme.buttonText,
cursor: onIncrement ? 'pointer' : 'default',
fontSize: '0.95rem',
fontWeight: 700,
opacity: onIncrement ? 1 : 0.72,
padding: '0.8rem 1.1rem',
}}
>
increment
</button>
</div>
</section>
);
}
10 changes: 10 additions & 0 deletions apps/react/remote/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<main style="font-family:Georgia,serif;max-width:52rem;margin:0 auto;padding:3rem 1.5rem;display:grid;gap:1rem;">
<h1 style="margin:0;font-size:3rem;line-height:0.95;">React remote</h1>
<p style="margin:0;line-height:1.7;max-width:40rem;">
This app exposes <code>react_remote/components/CounterCard</code> through Module Federation.
Use the host on <code>http://localhost:4331</code> to verify SSR plus client rendering.
</p>
<p style="margin:0;">
Manifest: <a href="/mf-manifest.json">/mf-manifest.json</a>
</p>
</main>
5 changes: 5 additions & 0 deletions apps/react/remote/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
1 change: 1 addition & 0 deletions docs/astro-source-integration-points.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,6 @@ Implemented in this PoC package:
## Remaining Constraints

- SSR path depends on SSR-safe remote modules (no DOM usage).
- React remotes are not zero-dependency on the host. Astro still needs a local renderer package plus `react`/`react-dom` available to resolve `clientEntrypoint` and `serverEntrypoint`; host config should own that setup explicitly.
- For non-Astro providers, server manifests need `ssrRemoteEntry`; MF runtime will prefer that entry in Node/server execution.
- `mf-vite` still emits serve-time warnings around `plugin:add-entry` (`emitFile()` in serve mode).
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
"dev": "pnpm --filter @module-federation/astro build && turbo run dev --filter=example-remote --filter=example-host",
"dev:host": "pnpm --filter @module-federation/astro build && turbo run dev --filter=example-host",
"dev:remote": "pnpm --filter @module-federation/astro build && turbo run dev --filter=example-remote",
"dev:react": "node ./scripts/dev-react.mjs && pnpm --filter @module-federation/astro build && turbo run dev --filter=react-remote --filter=react-host",
"dev:react:host": "pnpm --filter @module-federation/astro build && turbo run dev --filter=react-host",
"dev:react:remote": "pnpm --filter @module-federation/astro build && turbo run dev --filter=react-remote",
"build:package": "pnpm --filter @module-federation/astro build",
"build": "turbo run build --filter=@module-federation/astro",
"build:host": "turbo run build --filter=example-host",
"build:remote": "turbo run build --filter=example-remote",
"build:react:host": "turbo run build --filter=react-host",
"build:react:remote": "turbo run build --filter=react-remote",
"lint": "turbo run lint --filter=@module-federation/astro",
"fmt": "turbo run fmt --filter=@module-federation/astro",
"fmt.check": "turbo run fmt.check --filter=@module-federation/astro",
Expand Down
1 change: 1 addition & 0 deletions packages/astro/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Notes:
- In `astro dev`, `ENV_TARGET` defaults to `undefined` so runtime infers browser vs node context.
- `mode: 'client' | 'server'` maps to MF target `'web' | 'node'` if you want to force one side.
- SSR `.astro` remote imports are transformed through an Astro SSR runtime path.
- React remotes still need explicit host setup for `@astrojs/react`, `react`, and `react-dom`.

## `.astro` usage

Expand Down
Loading
Loading