diff --git a/biome.json b/biome.json
index 3fc465bb33a6..ff9f91e65e2a 100644
--- a/biome.json
+++ b/biome.json
@@ -16,7 +16,8 @@
"packages/generator/sandpack-react/src/templates/**",
"tests/integration/swc/fixtures/minify-css/src/bootstrap.css",
"tests/integration/swc/fixtures/config-function/src/bootstrap.css",
- "packages/runtime/plugin-garfish/type.d.ts"
+ "packages/runtime/plugin-garfish/type.d.ts",
+ "packages/runtime/plugin-runtime/static/**"
]
},
"css": {
@@ -112,7 +113,8 @@
"tests/integration/ssr/fixtures/preload/**/*",
"tests/integration/swc/fixtures/transform-fail/src/App.jsx",
"tests/integration/module/plugins/vue/**/*",
- "packages/module/plugin-module-node-polyfill/src/globals.js"
+ "packages/module/plugin-module-node-polyfill/src/globals.js",
+ "packages/runtime/plugin-runtime/static/**"
]
}
}
diff --git a/packages/runtime/plugin-runtime/package.json b/packages/runtime/plugin-runtime/package.json
index 8cefcaf6c2f2..f4a781a83534 100644
--- a/packages/runtime/plugin-runtime/package.json
+++ b/packages/runtime/plugin-runtime/package.json
@@ -29,6 +29,7 @@
"jsnext:source": "./src/index.ts",
"default": "./dist/esm/index.js"
},
+ "./package.json": "./package.json",
"./types": "./types/index.d.ts",
"./types/index": "./types/index.d.ts",
"./types/router": "./types/router.d.ts",
@@ -178,7 +179,8 @@
"dev": "modern-lib build --watch",
"prepublishOnly": "only-allow-pnpm",
"new": "modern-lib new",
- "build": "modern-lib build",
+ "build": "modern-lib build && pnpm run gen-static",
+ "gen-static": "ts-node ./scripts/gen-static.ts",
"test": "jest"
},
"dependencies": {
@@ -235,6 +237,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"ts-jest": "^29.1.0",
+ "ts-node": "^10.9.1",
"typescript": "^5",
"webpack": "^5.95.0"
},
diff --git a/packages/runtime/plugin-runtime/scripts/gen-static.ts b/packages/runtime/plugin-runtime/scripts/gen-static.ts
new file mode 100644
index 000000000000..6bfd3cca25d2
--- /dev/null
+++ b/packages/runtime/plugin-runtime/scripts/gen-static.ts
@@ -0,0 +1,26 @@
+import path from 'path';
+import { fs } from '@modern-js/utils';
+import {
+ modernInline,
+ runRouterDataFnStr,
+ runWindowFnStr,
+} from '../src/router/runtime/constants';
+
+(async () => {
+ const targetDir = path.join(__dirname, '../static');
+ await fs.ensureDir(targetDir);
+
+ const modernDefineInitPath = path.join(targetDir, 'modern-inline.js');
+ await fs.writeFile(modernDefineInitPath, modernInline, 'utf-8');
+
+ const runRouterDataFilePath = path.join(
+ targetDir,
+ 'modern-run-router-data-fn.js',
+ );
+ await fs.writeFile(runRouterDataFilePath, runRouterDataFnStr, 'utf-8');
+
+ const runWindowFilePath = path.join(targetDir, 'modern-run-window-fn.js');
+ await fs.writeFile(runWindowFilePath, runWindowFnStr, 'utf-8');
+
+ console.info('Generate static files successfully');
+})();
diff --git a/packages/runtime/plugin-runtime/src/core/server/requestHandler.ts b/packages/runtime/plugin-runtime/src/core/server/requestHandler.ts
index 5877834ef46b..2de74c3cfc29 100644
--- a/packages/runtime/plugin-runtime/src/core/server/requestHandler.ts
+++ b/packages/runtime/plugin-runtime/src/core/server/requestHandler.ts
@@ -16,7 +16,7 @@ import { getGlobalRunner } from '../plugin/runner';
import { createRoot } from '../react';
import type { SSRServerContext } from '../types';
import { CHUNK_CSS_PLACEHOLDER } from './constants';
-import { getSSRConfigByEntry, getSSRMode } from './utils';
+import { getSSRConfigByEntry, getSSRInlineScript, getSSRMode } from './utils';
export type { RequestHandlerConfig as HandleRequestConfig } from '@modern-js/app-tools';
@@ -102,6 +102,7 @@ function createSSRContext(
);
const ssrMode = getSSRMode(ssrConfig);
+ const inlineScript = getSSRInlineScript(ssrConfig);
const loaderFailureMode =
typeof ssrConfig === 'object' ? ssrConfig.loaderFailureMode : undefined;
@@ -137,6 +138,7 @@ function createSSRContext(
},
reporter,
mode: ssrMode,
+ inlineScript,
onError,
onTiming,
loaderFailureMode,
diff --git a/packages/runtime/plugin-runtime/src/core/server/stream/afterTemplate.ts b/packages/runtime/plugin-runtime/src/core/server/stream/afterTemplate.ts
index ddd370eb83b5..efe4cb502f2a 100644
--- a/packages/runtime/plugin-runtime/src/core/server/stream/afterTemplate.ts
+++ b/packages/runtime/plugin-runtime/src/core/server/stream/afterTemplate.ts
@@ -1,6 +1,6 @@
import { serializeJson } from '@modern-js/runtime-utils/node';
import type { HeadersData } from '@modern-js/runtime-utils/universal/request';
-import type { RenderLevel } from '../../constants';
+import { type RenderLevel, SSR_DATA_JSON_ID } from '../../constants';
import type { RuntimeContext } from '../../context';
import type { SSRContainer } from '../../types';
import { SSR_DATA_PLACEHOLDER } from '../constants';
@@ -103,9 +103,16 @@ function createReplaceSSRData(options: {
};
const attrsStr = attributesToString({ nonce });
- const ssrDataScript = `
-
- `;
+ const inlineScript =
+ typeof ssrConfig === 'boolean' ? true : ssrConfig?.inlineScript !== false;
+ const useInlineScript = inlineScript !== false;
+ const serializeSSRData = serializeJson(ssrData);
+
+ const ssrDataScript = useInlineScript
+ ? `
+
+ `
+ : ``;
return (template: string) =>
safeReplace(template, SSR_DATA_PLACEHOLDER, ssrDataScript);
diff --git a/packages/runtime/plugin-runtime/src/core/server/utils.ts b/packages/runtime/plugin-runtime/src/core/server/utils.ts
index a2f028aba921..8556d0c241b8 100644
--- a/packages/runtime/plugin-runtime/src/core/server/utils.ts
+++ b/packages/runtime/plugin-runtime/src/core/server/utils.ts
@@ -79,3 +79,11 @@ export function getSSRMode(ssrConfig?: SSRConfig): 'string' | 'stream' | false {
return ssrConfig?.mode === 'stream' ? 'stream' : 'string';
}
+
+export function getSSRInlineScript(ssrConfig?: SSRConfig): boolean {
+ if (typeof ssrConfig === 'object') {
+ return ssrConfig.inlineScript === undefined ? true : ssrConfig.inlineScript;
+ }
+
+ return true;
+}
diff --git a/packages/runtime/plugin-runtime/src/core/types.ts b/packages/runtime/plugin-runtime/src/core/types.ts
index 3cd95901a04d..1ac787f9e40e 100644
--- a/packages/runtime/plugin-runtime/src/core/types.ts
+++ b/packages/runtime/plugin-runtime/src/core/types.ts
@@ -69,4 +69,5 @@ export type SSRServerContext = Pick<
loaderFailureMode?: 'clientRender' | 'errorBoundary';
onError?: (e: unknown) => void;
onTiming?: (name: string, dur: number) => void;
+ inlineScript?: boolean;
};
diff --git a/packages/runtime/plugin-runtime/src/router/runtime/DeferredDataScripts.node.tsx b/packages/runtime/plugin-runtime/src/router/runtime/DeferredDataScripts.node.tsx
index 8fb055741a32..c2dcad2476e1 100644
--- a/packages/runtime/plugin-runtime/src/router/runtime/DeferredDataScripts.node.tsx
+++ b/packages/runtime/plugin-runtime/src/router/runtime/DeferredDataScripts.node.tsx
@@ -5,52 +5,10 @@ import type {
} from '@modern-js/runtime-utils/remix-router';
import { Await, useAsyncError } from '@modern-js/runtime-utils/router';
import { Suspense, useEffect, useMemo, useRef } from 'react';
+import { ROUTER_DATA_JSON_ID } from '../../core/constants';
+import { modernInline, runRouterDataFnStr, runWindowFnStr } from './constants';
import { serializeErrors } from './utils';
-/**
- * setup promises for deferred data
- * original function:
- function setupDeferredPromise(routeId, key) {
- _ROUTER_DATA.r = _ROUTER_DATA.r || {};
- _ROUTER_DATA.r[routeId] = _ROUTER_DATA.r[routeId] || {};
- const promise = new Promise(function (resolve, reject) {
- _ROUTER_DATA.r[routeId][key] = {
- resolve,
- reject,
- };
- });
- return promise;
- };
- *
- */
-const setupFnStr = `function s(r,e){_ROUTER_DATA.r=_ROUTER_DATA.r||{},_ROUTER_DATA.r[r]=_ROUTER_DATA.r[r]||{};return new Promise((function(A,R){_ROUTER_DATA.r[r][e]={resolve:A,reject:R}}))};`;
-
-/**
- * resolve promises for deferred data
- * original function:
- function resolveDeferredPromise(routeId, key, data, error) {
- if (error) {
- _ROUTER_DATA.r[routeId][key].reject(error);
- } else {
- _ROUTER_DATA.r[routeId][key].resolve(data);
- }
- };
- */
-const resolveFnStr = `function r(e,r,o,A){A?_ROUTER_DATA.r[e][r].reject(A):_ROUTER_DATA.r[e][r].resolve(o)};`;
-
-/**
- * update data for pre resolved promises
- * original function:
- * function preResovledDeferredPromise(data, error) {
- if(typeof error !== 'undefined'){
- return Promise.reject(new Error(error.message));
- }else{
- return Promise.resolve(data);
- }
- }
- */
-const preResolvedFnStr = `function p(e,r){return void 0!==r?Promise.reject(new Error(r.message)):Promise.resolve(e)};`;
-
/**
* DeferredDataScripts only renders in server side,
* it doesn't need to be hydrated in client side.
@@ -58,8 +16,10 @@ const preResolvedFnStr = `function p(e,r){return void 0!==r?Promise.reject(new E
const DeferredDataScripts = (props?: {
nonce?: string;
context: StaticHandlerContext;
+ inlineScript?: boolean;
}) => {
const staticContext = props?.context;
+ const inlineScript = props?.inlineScript;
const hydratedRef = useRef(false);
useEffect(() => {
@@ -68,7 +28,14 @@ const DeferredDataScripts = (props?: {
// No need to memo since DeferredDataScripts only renders in server side,
// here we memo it in case DeferredDataScripts renders in client side one day.
- const deferredScripts: [string, JSX.Element[]] | null = useMemo(() => {
+ const deferredScripts:
+ | [
+ string,
+ string,
+ { fnName: string; fnArgs: any[]; fnRun: string; fnScriptSrc: string }[],
+ JSX.Element[],
+ ]
+ | null = useMemo(() => {
if (!staticContext) {
return null;
}
@@ -80,68 +47,80 @@ const DeferredDataScripts = (props?: {
errors: serializeErrors(staticContext.errors),
};
- let initialScripts = [
- `_ROUTER_DATA = ${serializeJson(_ROUTER_DATA)};`,
- `_ROUTER_DATA.s = ${setupFnStr}`,
- `_ROUTER_DATA.r = ${resolveFnStr}`,
- `_ROUTER_DATA.p = ${preResolvedFnStr}`,
- ].join('\n');
- const deferredDataScripts: JSX.Element[] = [];
+ //
+ const initialScript0 = inlineScript ? '' : `${serializeJson(_ROUTER_DATA)}`;
+
+ const initialScript1 = modernInline;
- initialScripts += Object.entries(activeDeferreds)
- .map(([routeId, deferredData]) => {
+ const deferredDataScripts: JSX.Element[] = [];
+ const initialScripts = Object.entries(activeDeferreds).map(
+ ([routeId, deferredData]) => {
const pendingKeys = new Set(deferredData.pendingKeys);
const { deferredKeys } = deferredData;
- const deferredKeyPromiseStr = deferredKeys
- .map(key => {
- if (pendingKeys.has(key)) {
- deferredDataScripts.push(
- ,
- );
-
- return `${JSON.stringify(key)}: _ROUTER_DATA.s(${JSON.stringify(
- routeId,
- )},${JSON.stringify(key)}) `;
+ const deferredKeyPromiseManifests = deferredKeys.map(key => {
+ if (pendingKeys.has(key)) {
+ deferredDataScripts.push(
+ ,
+ );
+
+ return {
+ key,
+ routerDataFnName: 's',
+ routerDataFnArgs: [routeId, key],
+ };
+ } else {
+ const trackedPromise = deferredData.data[key] as TrackedPromise;
+ if (typeof trackedPromise._error !== 'undefined') {
+ const error = {
+ message: trackedPromise._error.message,
+ stack:
+ process.env.NODE_ENV !== 'production'
+ ? trackedPromise._error.stack
+ : undefined,
+ };
+
+ return {
+ key,
+ routerDataFnName: 'p',
+ routerDataFnArgs: [undefined, serializeJson(error)],
+ };
} else {
- const trackedPromise = deferredData.data[key] as TrackedPromise;
- if (typeof trackedPromise._error !== 'undefined') {
- const error = {
- message: trackedPromise._error.message,
- stack:
- process.env.NODE_ENV !== 'production'
- ? trackedPromise._error.stack
- : undefined,
- };
- return `${JSON.stringify(
- key,
- )}: _ROUTER_DATA.p(${undefined}, ${serializeJson(error)})`;
- } else {
- if (typeof trackedPromise._data === 'undefined') {
- throw new Error(
- `The deferred data for ${key} was not resolved, did you forget to return data from a deferred promise`,
- );
- }
- return `${JSON.stringify(key)}: _ROUTER_DATA.p(${serializeJson(
- trackedPromise._data,
- )})`;
+ if (typeof trackedPromise._data === 'undefined') {
+ throw new Error(
+ `The deferred data for ${key} was not resolved, did you forget to return data from a deferred promise`,
+ );
}
- }
- })
- .join(',\n');
- return `Object.assign(_ROUTER_DATA.loaderData[${JSON.stringify(
- routeId,
- )}], {${deferredKeyPromiseStr}});`;
- })
- .join('\n');
-
- return [initialScripts, deferredDataScripts];
+ return {
+ key,
+ routerDataFnName: 'p',
+ routerDataFnArgs: [serializeJson(trackedPromise._data)],
+ };
+ }
+ }
+ });
+ // raw string: `function fn_${fnName} (routeIdJsonStr, deferredKeyPromiseStr) {Object.assign(_ROUTER_DATA.loaderData[routeIdJsonStr], deferredKeyPromiseStr);};fn_${fnName}("${routeId}", {${deferredKeyPromiseStr}});`;
+ return {
+ fnName: `mergeData`,
+ fnRun: runWindowFnStr,
+ fnArgs: [routeId, deferredKeyPromiseManifests],
+ fnScriptSrc: 'modern-run-window-fn',
+ };
+ },
+ );
+
+ return [
+ initialScript0,
+ initialScript1,
+ initialScripts,
+ deferredDataScripts,
+ ];
}, []);
if (!deferredScripts) {
@@ -151,14 +130,41 @@ const DeferredDataScripts = (props?: {
return (
<>
{!hydratedRef.current && (
-
+ <>
+ {/* json or empty string */}
+ {deferredScripts[0].length !== 0 && (
+
+ )}
+
+ {deferredScripts[2].map(({ fnName, fnRun, fnArgs, fnScriptSrc }) => (
+
+ ))}
+ >
)}
- {!hydratedRef.current && deferredScripts[1]}
+ {!hydratedRef.current && deferredScripts[3]}
>
);
};
@@ -191,11 +197,12 @@ const DeferredDataScript = ({
)}
@@ -218,15 +225,21 @@ const ErrorDeferredDataScript = ({
return (
);
diff --git a/packages/runtime/plugin-runtime/src/router/runtime/constants.ts b/packages/runtime/plugin-runtime/src/router/runtime/constants.ts
new file mode 100644
index 000000000000..dd8d10020f20
--- /dev/null
+++ b/packages/runtime/plugin-runtime/src/router/runtime/constants.ts
@@ -0,0 +1,84 @@
+import { ROUTER_DATA_JSON_ID } from '../../core/constants';
+
+/**
+ * setup promises for deferred data
+ * original function:
+ function setupDeferredPromise(routeId, key) {
+ _ROUTER_DATA.r = _ROUTER_DATA.r || {};
+ _ROUTER_DATA.r[routeId] = _ROUTER_DATA.r[routeId] || {};
+ const promise = new Promise(function (resolve, reject) {
+ _ROUTER_DATA.r[routeId][key] = {
+ resolve,
+ reject,
+ };
+ });
+ return promise;
+ };
+ *
+ */
+const setupFnStr = `function s(r,e){_ROUTER_DATA.r=_ROUTER_DATA.r||{},_ROUTER_DATA.r[r]=_ROUTER_DATA.r[r]||{};return new Promise((function(A,R){_ROUTER_DATA.r[r][e]={resolve:A,reject:R}}))};`;
+
+/**
+ * resolve promises for deferred data
+ * original function:
+ function resolveDeferredPromise(routeId, key, data, error) {
+ if (error) {
+ _ROUTER_DATA.r[routeId][key].reject(error);
+ } else {
+ _ROUTER_DATA.r[routeId][key].resolve(data);
+ }
+ };
+ */
+const resolveFnStr = `function r(e,r,o,A){A?_ROUTER_DATA.r[e][r].reject(A):_ROUTER_DATA.r[e][r].resolve(o)};`;
+
+/**
+ * update data for pre resolved promises
+ * original function:
+ * function preResovledDeferredPromise(data, error) {
+ if(typeof error !== 'undefined'){
+ return Promise.reject(new Error(error.message));
+ }else{
+ return Promise.resolve(data);
+ }
+ }
+ */
+const preResolvedFnStr = `function p(e,r){return void 0!==r?Promise.reject(new Error(r.message)):Promise.resolve(e)};`;
+
+/**
+ * This original string is: ${setupFnStr};${resolveFnStr};${preResolvedFnStr};
+ * function mergeData (routeIdJsonStr, deferredKeyPromiseManifests) {
+ const source = deferredKeyPromiseManifests.reduce(function(o, {key, routerDataFnName, routerDataFnArgs }) {
+ return {...o, [key]: _ROUTER_DATA[routerDataFnName](...routerDataFnArgs)}
+ }, {});
+ Object.assign(_ROUTER_DATA.loaderData[routeIdJsonStr], source);
+ };
+ */
+const initRouterDataAttrs = `_ROUTER_DATA.s = ${setupFnStr}_ROUTER_DATA.r = ${resolveFnStr}_ROUTER_DATA.p = ${preResolvedFnStr}function mergeData(a,e){e=e.reduce(function(a,{key:e,routerDataFnName:r,routerDataFnArgs:t}){return{...a,[e]:_ROUTER_DATA[r](...t)}},{});Object.assign(_ROUTER_DATA.loaderData[a],e)}`;
+
+/**
+ function runWindowFn() {
+ window[document.currentScript.getAttribute('data-fn-name')](...JSON.parse(document.currentScript.getAttribute('data-fn-args')))
+ }
+ function runRouterDataFn() {
+ _ROUTER_DATA[document.currentScript.getAttribute('data-fn-name')](...JSON.parse(document.currentScript.getAttribute('data-fn-args')))
+ }
+ function initRouterData(id) {
+ const ele = document.getElementById(id);
+ if (ele) {
+ try {
+ _ROUTER_DATA = JSON.parse(ele.textContent);
+ } catch(e) {
+ console.error("parse ".concat(id, " error"), t);
+ _ROUTER_DATA = {};
+ }
+ }
+
+ initRouterData();
+ ${initRouterDataAttrs}
+ }
+ */
+export const modernInline = `function runWindowFn(){window[document.currentScript.getAttribute("data-fn-name")](...JSON.parse(document.currentScript.getAttribute("data-fn-args")))}function runRouterDataFn(){_ROUTER_DATA[document.currentScript.getAttribute("data-fn-name")](...JSON.parse(document.currentScript.getAttribute("data-fn-args")))}function initRouterData(e){var r=document.getElementById(e);if(r)try{_ROUTER_DATA=JSON.parse(r.textContent)}catch(r){console.error("parse ".concat(e," error"),t),_ROUTER_DATA={}}};initRouterData('${ROUTER_DATA_JSON_ID}');${initRouterDataAttrs}`;
+
+export const runRouterDataFnStr = `runRouterDataFn();`;
+
+export const runWindowFnStr = `runWindowFn();`;
diff --git a/packages/runtime/plugin-runtime/src/router/runtime/plugin.node.tsx b/packages/runtime/plugin-runtime/src/router/runtime/plugin.node.tsx
index 12fddcede5f0..9157e729d3a2 100644
--- a/packages/runtime/plugin-runtime/src/router/runtime/plugin.node.tsx
+++ b/packages/runtime/plugin-runtime/src/router/runtime/plugin.node.tsx
@@ -152,7 +152,7 @@ export const routerPlugin = (
const context = useContext(RuntimeReactContext);
const { remixRouter, routerContext, ssrContext } = context;
- const { nonce, mode } = ssrContext!;
+ const { nonce, mode, inlineScript } = ssrContext!;
const routerWrapper = (
<>
@@ -168,6 +168,7 @@ export const routerPlugin = (
)}
{mode === 'stream' && JSX_SHELL_STREAM_END_MARK}
diff --git a/packages/runtime/plugin-runtime/static/modern-inline.js b/packages/runtime/plugin-runtime/static/modern-inline.js
new file mode 100644
index 000000000000..646bdd653815
--- /dev/null
+++ b/packages/runtime/plugin-runtime/static/modern-inline.js
@@ -0,0 +1 @@
+function runWindowFn(){window[document.currentScript.getAttribute("data-fn-name")](...JSON.parse(document.currentScript.getAttribute("data-fn-args")))}function runRouterDataFn(){_ROUTER_DATA[document.currentScript.getAttribute("data-fn-name")](...JSON.parse(document.currentScript.getAttribute("data-fn-args")))}function initRouterData(e){var r=document.getElementById(e);if(r)try{_ROUTER_DATA=JSON.parse(r.textContent)}catch(r){console.error("parse ".concat(e," error"),t),_ROUTER_DATA={}}};initRouterData('__MODERN_ROUTER_DATA__');_ROUTER_DATA.s = function s(r,e){_ROUTER_DATA.r=_ROUTER_DATA.r||{},_ROUTER_DATA.r[r]=_ROUTER_DATA.r[r]||{};return new Promise((function(A,R){_ROUTER_DATA.r[r][e]={resolve:A,reject:R}}))};_ROUTER_DATA.r = function r(e,r,o,A){A?_ROUTER_DATA.r[e][r].reject(A):_ROUTER_DATA.r[e][r].resolve(o)};_ROUTER_DATA.p = function p(e,r){return void 0!==r?Promise.reject(new Error(r.message)):Promise.resolve(e)};function mergeData(a,e){e=e.reduce(function(a,{key:e,routerDataFnName:r,routerDataFnArgs:t}){return{...a,[e]:_ROUTER_DATA[r](...t)}},{});Object.assign(_ROUTER_DATA.loaderData[a],e)}
\ No newline at end of file
diff --git a/packages/runtime/plugin-runtime/static/modern-run-router-data-fn.js b/packages/runtime/plugin-runtime/static/modern-run-router-data-fn.js
new file mode 100644
index 000000000000..b389eaa6a36f
--- /dev/null
+++ b/packages/runtime/plugin-runtime/static/modern-run-router-data-fn.js
@@ -0,0 +1 @@
+runRouterDataFn();
\ No newline at end of file
diff --git a/packages/runtime/plugin-runtime/static/modern-run-window-fn.js b/packages/runtime/plugin-runtime/static/modern-run-window-fn.js
new file mode 100644
index 000000000000..004bb74ed032
--- /dev/null
+++ b/packages/runtime/plugin-runtime/static/modern-run-window-fn.js
@@ -0,0 +1 @@
+runWindowFn();
\ No newline at end of file
diff --git a/tests/integration/ssr/fixtures/streaming-inline/modern.config.ts b/tests/integration/ssr/fixtures/streaming-inline/modern.config.ts
new file mode 100644
index 000000000000..a3f26d24f2f3
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/modern.config.ts
@@ -0,0 +1,13 @@
+import { applyBaseConfig } from '../../../../utils/applyBaseConfig';
+
+export default applyBaseConfig({
+ runtime: {
+ router: true,
+ },
+ server: {
+ ssr: {
+ mode: 'stream',
+ inlineScript: false,
+ },
+ },
+});
diff --git a/tests/integration/ssr/fixtures/streaming-inline/package.json b/tests/integration/ssr/fixtures/streaming-inline/package.json
new file mode 100644
index 000000000000..4fe16a4872bf
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "ssr-streaming-inline-test",
+ "version": "2.9.0",
+ "private": true,
+ "scripts": {
+ "dev": "modern dev"
+ },
+ "engines": {
+ "node": ">=14.17.6"
+ },
+ "dependencies": {
+ "@modern-js/runtime": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@modern-js/app-tools": "workspace:*",
+ "@modern-js/tsconfig": "workspace:*",
+ "@types/react": "^18.3.11",
+ "@types/react-dom": "^18.3.1",
+ "typescript": "^5"
+ }
+}
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/entry.server.tsx b/tests/integration/ssr/fixtures/streaming-inline/src/entry.server.tsx
new file mode 100644
index 000000000000..5e8fa41c13a2
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/entry.server.tsx
@@ -0,0 +1,17 @@
+import {
+ type HandleRequest,
+ createRequestHandler,
+ renderStreaming,
+} from '@modern-js/runtime/ssr/server';
+
+const handleRequest: HandleRequest = async (request, ServerRoot, options) => {
+ const stream = await renderStreaming(request, , options);
+
+ return new Response(stream, {
+ headers: {
+ 'x-custom-key': '123',
+ },
+ });
+};
+
+export default createRequestHandler(handleRequest);
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/modern-app-env.d.ts b/tests/integration/ssr/fixtures/streaming-inline/src/modern-app-env.d.ts
new file mode 100644
index 000000000000..3f453508cee1
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/modern-app-env.d.ts
@@ -0,0 +1,3 @@
+///
+///
+///
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/about/About.tsx b/tests/integration/ssr/fixtures/streaming-inline/src/routes/about/About.tsx
new file mode 100644
index 000000000000..bb01f07e9e22
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/about/About.tsx
@@ -0,0 +1,9 @@
+import './about.css';
+
+export default () => {
+ return (
+
+ );
+};
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/about/about.css b/tests/integration/ssr/fixtures/streaming-inline/src/routes/about/about.css
new file mode 100644
index 000000000000..3a3751cdb332
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/about/about.css
@@ -0,0 +1,3 @@
+.about {
+ color: blue;
+}
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/about/page.tsx b/tests/integration/ssr/fixtures/streaming-inline/src/routes/about/page.tsx
new file mode 100644
index 000000000000..28e760a768d4
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/about/page.tsx
@@ -0,0 +1,13 @@
+import { Suspense } from 'react';
+import About from './About';
+
+export default function Page() {
+ return (
+
+ About page
+ loading ...
}>
+
+
+
+ );
+}
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/error/page.loader.ts b/tests/integration/ssr/fixtures/streaming-inline/src/routes/error/page.loader.ts
new file mode 100644
index 000000000000..055aabe04a37
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/error/page.loader.ts
@@ -0,0 +1,11 @@
+import { defer } from '@modern-js/runtime/router';
+
+export default () => {
+ const data = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ reject(new Error('error occurs'));
+ }, 200);
+ });
+
+ return defer({ data });
+};
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/error/page.tsx b/tests/integration/ssr/fixtures/streaming-inline/src/routes/error/page.tsx
new file mode 100644
index 000000000000..1571c5199bbc
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/error/page.tsx
@@ -0,0 +1,24 @@
+import { Await, useAsyncError, useLoaderData } from '@modern-js/runtime/router';
+import { Suspense } from 'react';
+
+export default function Page() {
+ const data = useLoaderData() as any;
+
+ return (
+
+ Error page
+ loading ...
}>
+ }>
+ {_ => {
+ return never shown
;
+ }}
+
+
+
+ );
+}
+
+function ErrorElement() {
+ const error = useAsyncError() as Error;
+ return Something went wrong! {error.message}
;
+}
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/layout.tsx b/tests/integration/ssr/fixtures/streaming-inline/src/routes/layout.tsx
new file mode 100644
index 000000000000..a58ecd6e12e2
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/layout.tsx
@@ -0,0 +1,24 @@
+import { Link, Outlet } from '@modern-js/runtime/router';
+
+export default function Layout() {
+ return (
+
+ Root layout
+
+
+ Go User 1
+
+
+ Go About
+
+
+ Go Error
+
+
+ Go Redirect
+
+
+
+
+ );
+}
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/redirect/page.loader.ts b/tests/integration/ssr/fixtures/streaming-inline/src/routes/redirect/page.loader.ts
new file mode 100644
index 000000000000..b848f3b85a21
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/redirect/page.loader.ts
@@ -0,0 +1,19 @@
+import { defer } from '@modern-js/runtime/router';
+
+export default () => {
+ const foo = new Promise(resolve => {
+ setTimeout(() => {
+ resolve('foo');
+ }, 200);
+ });
+
+ return defer(
+ { data: foo },
+ {
+ status: 302,
+ headers: {
+ Location: '/',
+ },
+ },
+ );
+};
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/redirect/page.tsx b/tests/integration/ssr/fixtures/streaming-inline/src/routes/redirect/page.tsx
new file mode 100644
index 000000000000..f99b60bd7ff2
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/redirect/page.tsx
@@ -0,0 +1,5 @@
+const Page = () => {
+ return Redirect page
;
+};
+
+export default Page;
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/[id]/page.loader.ts b/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/[id]/page.loader.ts
new file mode 100644
index 000000000000..9c587a4e29cd
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/[id]/page.loader.ts
@@ -0,0 +1,17 @@
+import { type LoaderFunctionArgs, defer } from '@modern-js/runtime/router';
+import type { User } from './page';
+
+export default ({ params }: LoaderFunctionArgs) => {
+ const userId = params.id;
+
+ const user = new Promise(resolve => {
+ setTimeout(() => {
+ resolve({
+ name: `user${userId}`,
+ age: 18,
+ });
+ }, 200);
+ });
+
+ return defer({ data: user });
+};
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/[id]/page.tsx b/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/[id]/page.tsx
new file mode 100644
index 000000000000..f52bccfde515
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/[id]/page.tsx
@@ -0,0 +1,34 @@
+import { Await, useLoaderData } from '@modern-js/runtime/router';
+import { Suspense } from 'react';
+
+export interface User {
+ name: string;
+ age: number;
+}
+
+interface Data {
+ data: User;
+}
+
+const Page = () => {
+ const data = useLoaderData() as Data;
+
+ return (
+
+ user info:
+ loading user data ...
}>
+
+ {(user: User) => {
+ return (
+
+ {user.name}-{user.age}
+
+ );
+ }}
+
+
+
+ );
+};
+
+export default Page;
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/layout.tsx b/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/layout.tsx
new file mode 100644
index 000000000000..a7f9b773abbc
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/layout.tsx
@@ -0,0 +1,10 @@
+import { Outlet } from '@modern-js/runtime/router';
+
+export default function Layout() {
+ return (
+
+ User layout
+ {}
+
+ );
+}
diff --git a/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/page.tsx b/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/page.tsx
new file mode 100644
index 000000000000..2c2f4d9ac4e2
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/src/routes/user/page.tsx
@@ -0,0 +1,18 @@
+import { Outlet, useNavigate } from '@modern-js/runtime/router';
+
+export default function Page() {
+ const nav = useNavigate();
+ return (
+
+ User page
+
+
+
+ );
+}
diff --git a/tests/integration/ssr/fixtures/streaming-inline/tsconfig.json b/tests/integration/ssr/fixtures/streaming-inline/tsconfig.json
new file mode 100644
index 000000000000..aea750a5f660
--- /dev/null
+++ b/tests/integration/ssr/fixtures/streaming-inline/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "@modern-js/tsconfig/base",
+ "compilerOptions": {
+ "declaration": false,
+ "jsx": "preserve",
+ "baseUrl": "./",
+ "paths": {
+ "@/*": ["./src/*"],
+ "@shared/*": ["./shared/*"],
+ "@api/*": ["./api/*"]
+ }
+ },
+ "include": ["src", "shared", "config", "modern.config.ts", "api"]
+}
diff --git a/tests/integration/ssr/tests/streaming-inline.test.ts b/tests/integration/ssr/tests/streaming-inline.test.ts
new file mode 100644
index 000000000000..236f62997509
--- /dev/null
+++ b/tests/integration/ssr/tests/streaming-inline.test.ts
@@ -0,0 +1,46 @@
+import path, { join } from 'path';
+import puppeteer, { type Browser, type Page } from 'puppeteer';
+import {
+ getPort,
+ killApp,
+ launchApp,
+ launchOptions,
+} from '../../../utils/modernTestUtils';
+
+const fixtureDir = path.resolve(__dirname, '../fixtures');
+
+describe('Streaming SSR with ssr.inlineScript', () => {
+ let app: any;
+ let appPort: number;
+ let page: Page;
+ let browser: Browser;
+
+ beforeAll(async () => {
+ const appDir = join(fixtureDir, 'streaming-inline');
+ appPort = await getPort();
+ app = await launchApp(appDir, appPort, {});
+
+ browser = await puppeteer.launch(launchOptions as any);
+ page = await browser.newPage();
+ });
+
+ afterAll(async () => {
+ if (browser) {
+ browser.close();
+ }
+ if (app) {
+ await killApp(app);
+ }
+ });
+
+ test('window._SSR_DATA is json', async () => {
+ const res = await page.goto(`http://localhost:${appPort}`, {
+ waitUntil: ['networkidle0'],
+ });
+
+ const body = await res!.text();
+ expect(body).toMatch(
+ /