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 = ` - window._SSR_DATA = ${serializeJson(ssrData)} - `; + const inlineScript = + typeof ssrConfig === 'boolean' ? true : ssrConfig?.inlineScript !== false; + const useInlineScript = inlineScript !== false; + const serializeSSRData = serializeJson(ssrData); + + const ssrDataScript = useInlineScript + ? ` + window._SSR_DATA = ${serializeSSRData} + ` + : ``; 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 && ( -