Skip to content
Open
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
41 changes: 21 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,27 @@
"read-yaml-file>js-yaml": "3.14.2",
"@remix-run/router": ">=1.23.2",
"h3": ">=1.15.5",
"@rspack/core@2.0.0-beta.2": "npm:@rspack-canary/core@2.0.0-canary-032bd1ff-20260212021235",
"@module-federation/bridge-react": "https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/bridge-react-webpack-plugin": "https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/bridge-shared": "https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/cli": "https://pkg.pr.new/module-federation/core/@module-federation/cli@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/data-prefetch": "https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/dts-plugin": "https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/enhanced": "https://pkg.pr.new/module-federation/core/@module-federation/enhanced@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/error-codes": "https://pkg.pr.new/module-federation/core/@module-federation/error-codes@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/inject-external-runtime-core-plugin": "https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/managers": "https://pkg.pr.new/module-federation/core/@module-federation/managers@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/manifest": "https://pkg.pr.new/module-federation/core/@module-federation/manifest@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/rsbuild-plugin": "https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/rspack": "https://pkg.pr.new/module-federation/core/@module-federation/rspack@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/runtime": "https://pkg.pr.new/module-federation/core/@module-federation/runtime@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/runtime-core": "https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/runtime-tools": "https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/sdk": "https://pkg.pr.new/module-federation/core/@module-federation/sdk@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/third-party-dts-extractor": "https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@module-federation/webpack-bundler-runtime": "https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@c160c7ae3246c65fa50f19d966b8acd32448580a",
"@rspack/core@2.0.0-beta.2": "npm:@rspack-canary/core@2.0.0-canary-35cc3a16-20260303212939",
"@module-federation/bridge-react": "https://pkg.pr.new/module-federation/core/@module-federation/bridge-react@a3dcf69",
"@module-federation/bridge-react-webpack-plugin": "https://pkg.pr.new/module-federation/core/@module-federation/bridge-react-webpack-plugin@a3dcf69",
"@module-federation/bridge-shared": "https://pkg.pr.new/module-federation/core/@module-federation/bridge-shared@a3dcf69",
"@module-federation/cli": "https://pkg.pr.new/module-federation/core/@module-federation/cli@a3dcf69",
"@module-federation/data-prefetch": "https://pkg.pr.new/module-federation/core/@module-federation/data-prefetch@a3dcf69",
"@module-federation/dts-plugin": "https://pkg.pr.new/module-federation/core/@module-federation/dts-plugin@a3dcf69",
"@module-federation/enhanced": "https://pkg.pr.new/module-federation/core/@module-federation/enhanced@a3dcf69",
"@module-federation/error-codes": "https://pkg.pr.new/module-federation/core/@module-federation/error-codes@a3dcf69",
"@module-federation/inject-external-runtime-core-plugin": "https://pkg.pr.new/module-federation/core/@module-federation/inject-external-runtime-core-plugin@a3dcf69",
"@module-federation/managers": "https://pkg.pr.new/module-federation/core/@module-federation/managers@a3dcf69",
"@module-federation/manifest": "https://pkg.pr.new/module-federation/core/@module-federation/manifest@a3dcf69",
"@module-federation/node": "https://pkg.pr.new/module-federation/core/@module-federation/node@a3dcf69",
"@module-federation/rsbuild-plugin": "https://pkg.pr.new/module-federation/core/@module-federation/rsbuild-plugin@a3dcf69",
"@module-federation/rspack": "https://pkg.pr.new/module-federation/core/@module-federation/rspack@a3dcf69",
"@module-federation/runtime": "https://pkg.pr.new/module-federation/core/@module-federation/runtime@a3dcf69",
"@module-federation/runtime-core": "https://pkg.pr.new/module-federation/core/@module-federation/runtime-core@a3dcf69",
"@module-federation/runtime-tools": "https://pkg.pr.new/module-federation/core/@module-federation/runtime-tools@a3dcf69",
"@module-federation/sdk": "https://pkg.pr.new/module-federation/core/@module-federation/sdk@a3dcf69",
"@module-federation/third-party-dts-extractor": "https://pkg.pr.new/module-federation/core/@module-federation/third-party-dts-extractor@a3dcf69",
"@module-federation/webpack-bundler-runtime": "https://pkg.pr.new/module-federation/core/@module-federation/webpack-bundler-runtime@a3dcf69",
"tar": ">=7.5.4",
"diff": ">=4.0.4",
"debug": ">=4.4.3",
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/builder/src/plugins/rscConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type { RsbuildPlugin, Rspack } from '@rsbuild/core';
const ASYNC_STORAGE_PATTERN = /universal[/\\]async_storage/;
const RSC_COMMON_LAYER = 'rsc-common';
const ENTRY_NAME_VAR = '__MODERN_JS_ENTRY_NAME';
export const ROUTE_SERVER_ENTRY_FILE_PATTERN =
/(?:^|[/\\])routes(?:[/\\].*)?[/\\](layout|page|\$)\.[tj]sx?$/;
const APP_SERVER_ENTRY_FILE_PATTERN = /[/\\]App\.[tj]sx?$/;

const createVirtualModule = (content: string) =>
`data:text/javascript,${encodeURIComponent(content)}`;
Expand Down Expand Up @@ -60,14 +63,13 @@ export function pluginRscConfig(): RsbuildPlugin {
// Matches: layout.tsx, layout.ts, layout.jsx, layout.js
// page.tsx, page.ts, page.jsx, page.js
// $.tsx, $.ts, $.jsx, $.js
// Use [/\\] before filename so both Unix (/) and Windows (\) paths match
const routeFilePattern =
/routes[/\\].*[/\\](layout|page|\$)\.[tj]sx?$/;
// Supports both root-level routes files (routes/page.tsx) and nested routes.
const routeFilePattern = ROUTE_SERVER_ENTRY_FILE_PATTERN;

// Pattern 2: Match App.[tj]sx files anywhere (self-controlled routing)
// Matches: App.tsx, App.ts, App.jsx, App.js in any directory
// Note: node_modules is already excluded by the exclude rule
const appFilePattern = /[/\\]App\.[tj]sx?$/;
const appFilePattern = APP_SERVER_ENTRY_FILE_PATTERN;

// Combine both patterns
const combinedPattern = new RegExp(
Expand Down
36 changes: 36 additions & 0 deletions packages/cli/builder/tests/rscConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, test } from '@rstest/core';
import { ROUTE_SERVER_ENTRY_FILE_PATTERN } from '../src/plugins/rscConfig';

describe('rsc server-entry route pattern', () => {
test('matches root routes files for server-entry injection', () => {
expect(
ROUTE_SERVER_ENTRY_FILE_PATTERN.test('/project/src/routes/page.tsx'),
).toBeTruthy();
expect(
ROUTE_SERVER_ENTRY_FILE_PATTERN.test('/project/src/routes/layout.ts'),
).toBeTruthy();
expect(
ROUTE_SERVER_ENTRY_FILE_PATTERN.test('/project/src/routes/$.jsx'),
).toBeTruthy();
});

test('matches nested routes files', () => {
expect(
ROUTE_SERVER_ENTRY_FILE_PATTERN.test('/project/src/routes/user/page.tsx'),
).toBeTruthy();
expect(
ROUTE_SERVER_ENTRY_FILE_PATTERN.test(
'C:\\project\\src\\routes\\user\\layout.tsx',
),
).toBeTruthy();
});

test('does not match non-route component files', () => {
expect(
ROUTE_SERVER_ENTRY_FILE_PATTERN.test('/project/src/myroutes/page.tsx'),
).toBeFalsy();
expect(
ROUTE_SERVER_ENTRY_FILE_PATTERN.test('/project/src/routes/page.data.ts'),
).toBeFalsy();
});
});
29 changes: 23 additions & 6 deletions packages/runtime/plugin-runtime/src/cli/template.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ import {
createRequestHandler,
} from '@#metaName/runtime/ssr/server';
import { RSCServerSlot } from '@#metaName/runtime/rsc/client';
import { renderRsc } from '@#metaName/runtime/rsc/server';
export { handleAction } from '@#metaName/runtime/rsc/server';
import {
renderRsc,
handleAction,
RSC_FLIGHT_CONTENT_TYPE,
} from '@#metaName/runtime/rsc/server';
export { handleAction };

const handleRequest = async (request, ServerRoot, options) => {

Expand Down Expand Up @@ -58,7 +62,11 @@ const handleRSCRequest = async (request, ServerRoot, options) => {
element: options.rscRoot
});

return new Response(stream);
return new Response(stream, {
headers: {
'content-type': RSC_FLIGHT_CONTENT_TYPE
},
});
}


Expand Down Expand Up @@ -92,8 +100,13 @@ export const entryForCSRWithRSC = ({
import {
createRequestHandler,
} from '@${metaName}/runtime/ssr/server';
import { renderCSRWithRSC, renderRsc } from '@${metaName}/runtime/rsc/server';
export { handleAction } from '@${metaName}/runtime/rsc/server';
import {
renderCSRWithRSC,
renderRsc,
handleAction,
RSC_FLIGHT_CONTENT_TYPE,
} from '@${metaName}/runtime/rsc/server';
export { handleAction };

const handleCSRRender = async (request, ServerRoot, options) => {
return renderCSRWithRSC({
Expand All @@ -111,7 +124,11 @@ export const entryForCSRWithRSC = ({
element: options.rscRoot,
});

return new Response(stream);
return new Response(stream, {
headers: {
'content-type': RSC_FLIGHT_CONTENT_TYPE
},
});
}

export const rscPayloadHandler = createRequestHandler(handleRequest, {
Expand Down
36 changes: 18 additions & 18 deletions packages/runtime/plugin-runtime/src/router/runtime/rsc-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@ import {
ElementsContext,
createFromReadableStream,
} from '@modern-js/render/client';
import {
type StaticHandlerContext,
StaticRouterProvider,
createStaticRouter,
} from '@modern-js/runtime-utils/router';
import {
type RouteObject,
createBrowserRouter,
redirect,
import type {
RouteObject,
StaticHandlerContext,
} from '@modern-js/runtime-utils/router';
import * as RouterRuntimeModule from '@modern-js/runtime-utils/router/rsc';
import React from 'react';
import type { PayloadRoute, ServerPayload } from '../../core/context';
import { CSSLinks } from './CSSLinks';

const RouterRuntime = RouterRuntimeModule as any;

// There is no `use` method in the following version of react19.
// In order to avoid errors, it is compatible here.
const safeUse = (promise: any) => {
Expand Down Expand Up @@ -277,15 +274,15 @@ export const createClientRouterFromPayload = (

const mergedRoutes = mergeRoutes(processedRoutes, originalRoutes);

const router = createBrowserRouter(mergedRoutes, {
const router = RouterRuntime.createBrowserRouter(mergedRoutes, {
//@ts-ignore
hydrationData: payload,
basename: basename,
dataStrategy: async context => {
dataStrategy: async (context: any) => {
const { request, matches } = context;
const results: Record<string, any> = {};
const clientMatches = matches.filter(
match => (match.route as any).hasClientLoader,
(match: any) => (match.route as any).hasClientLoader,
);

const fetchPromise = fetch(request.url, {
Expand All @@ -297,7 +294,7 @@ export const createClientRouterFromPayload = (
const clientLoadersPromise =
clientMatches.length > 0
? Promise.all(
clientMatches.map(async clientMatch => {
clientMatches.map(async (clientMatch: any) => {
const foundRoute = findRouteInTree(
originalRoutes,
clientMatch.route.id,
Expand All @@ -314,12 +311,12 @@ export const createClientRouterFromPayload = (
const redirectLocation = res.headers.get('X-Modernjs-Redirect');

if (redirectLocation) {
matches.forEach(match => {
matches.forEach((match: any) => {
const routeId = match.route.id;
if (routeId) {
results[routeId] = {
type: 'redirect',
result: redirect(redirectLocation),
result: RouterRuntime.redirect(redirectLocation),
};
}
});
Expand Down Expand Up @@ -350,7 +347,7 @@ export const createClientRouterFromPayload = (

const serverPayload = payload as ServerPayload;

matches.forEach(match => {
matches.forEach((match: any) => {
const routeId = match.route.id;
const matchedRoute = serverPayload.routes.find(
(route: PayloadRoute) => route.id === routeId,
Expand Down Expand Up @@ -424,10 +421,13 @@ const createRSCStaticRouterComponent = (
[],
);

const router = createStaticRouter(processedRoutes, routerContext);
const router = RouterRuntime.createStaticRouter(
processedRoutes,
routerContext,
);

return (
<StaticRouterProvider
<RouterRuntime.StaticRouterProvider
context={routerContext}
router={router}
hydrate={false}
Expand Down
32 changes: 8 additions & 24 deletions packages/runtime/render/src/client/callServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,21 @@ type ReactServerValue = unknown;
export type ActionIdResolver = (id: string) => string | Promise<string>;
export type ActionRequestUrlResolver = (entryName?: string) => string;

const ACTION_RESOLVER_KEY = '__MODERN_RSC_ACTION_RESOLVER__';
const ACTION_URL_RESOLVER_KEY = '__MODERN_RSC_ACTION_URL_RESOLVER__';
let actionIdResolver: ActionIdResolver | undefined;
let actionRequestUrlResolver: ActionRequestUrlResolver | undefined;

/**
* Register a custom action ID resolver. Plugins (e.g. Module Federation)
* use this to remap raw action IDs before they are sent to the server.
*/
export const setResolveActionId = (resolver: ActionIdResolver): void => {
(
globalThis as typeof globalThis & {
[ACTION_RESOLVER_KEY]?: ActionIdResolver;
}
)[ACTION_RESOLVER_KEY] = resolver;
export const setResolveActionId = (resolver?: ActionIdResolver): void => {
actionIdResolver = resolver;
};

export const setActionIdResolver = setResolveActionId;

const resolveActionId = (id: string): string | Promise<string> => {
const resolver = (
globalThis as typeof globalThis & {
[ACTION_RESOLVER_KEY]?: ActionIdResolver;
}
)[ACTION_RESOLVER_KEY];
const resolver = actionIdResolver;
if (typeof resolver === 'function') {
return resolver(id);
}
Expand All @@ -42,13 +34,9 @@ const resolveActionId = (id: string): string | Promise<string> => {
* to align request URLs with customized route/base configurations.
*/
export const setResolveActionRequestUrl = (
resolver: ActionRequestUrlResolver,
resolver?: ActionRequestUrlResolver,
): void => {
(
globalThis as typeof globalThis & {
[ACTION_URL_RESOLVER_KEY]?: ActionRequestUrlResolver;
}
)[ACTION_URL_RESOLVER_KEY] = resolver;
actionRequestUrlResolver = resolver;
};

export const setActionRequestUrlResolver = setResolveActionRequestUrl;
Expand All @@ -57,11 +45,7 @@ const resolveActionRequestUrl = (): string => {
const entryName =
typeof window !== 'undefined' ? window.__MODERN_JS_ENTRY_NAME : undefined;

const resolver = (
globalThis as typeof globalThis & {
[ACTION_URL_RESOLVER_KEY]?: ActionRequestUrlResolver;
}
)[ACTION_URL_RESOLVER_KEY];
const resolver = actionRequestUrlResolver;
if (typeof resolver === 'function') {
return resolver(entryName);
}
Expand Down
Loading