@modern-js/runtime 3.0.4 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,9 @@ import { ElementsContext, createFromReadableStream } from "@modern-js/render/cli
4
4
  import { StaticRouterProvider, createBrowserRouter, createStaticRouter, redirect } from "@modern-js/runtime-utils/router";
5
5
  import react from "react";
6
6
  import { CSSLinks } from "./CSSLinks.mjs";
7
- const safeUse = (promise)=>{
8
- if ('function' == typeof react.use) return react.use(promise);
7
+ const safeUse = (value)=>{
8
+ const reactUse = react.use;
9
+ if ('function' == typeof reactUse) return reactUse(value);
9
10
  return null;
10
11
  };
11
12
  function collectCssFilesFromRoutes(matches, routes) {
@@ -23,6 +24,17 @@ function collectCssFilesFromRoutes(matches, routes) {
23
24
  }
24
25
  const createServerPayload = (routerContext, routes)=>{
25
26
  const cssFiles = collectCssFilesFromRoutes(routerContext.matches, routes);
27
+ let cssInjectionIndex = -1;
28
+ if (cssFiles.length > 0) {
29
+ for(let i = routerContext.matches.length - 1; i >= 0; i--){
30
+ const matchRoute = findRouteInTree(routes, routerContext.matches[i].route.id);
31
+ if (matchRoute && !matchRoute.isClientComponent) {
32
+ cssInjectionIndex = i;
33
+ break;
34
+ }
35
+ }
36
+ if (-1 === cssInjectionIndex) cssInjectionIndex = routerContext.matches.length - 1;
37
+ }
26
38
  return {
27
39
  type: 'render',
28
40
  actionData: routerContext.actionData,
@@ -30,45 +42,42 @@ const createServerPayload = (routerContext, routes)=>{
30
42
  loaderData: routerContext.loaderData,
31
43
  location: routerContext.location,
32
44
  routes: routerContext.matches.map((match, index, matches)=>{
33
- const element = match.route.element;
45
+ const route = match.route;
46
+ const element = route.element;
34
47
  const parentMatch = index > 0 ? matches[index - 1] : void 0;
35
48
  let processedElement;
36
49
  if (element) {
37
50
  const ElementComponent = element.type;
38
51
  const elementProps = {
39
- loaderData: routerContext?.loaderData?.[match.route.id],
40
- actionData: routerContext?.actionData?.[match.route.id],
52
+ loaderData: routerContext?.loaderData?.[route.id],
53
+ actionData: routerContext?.actionData?.[route.id],
41
54
  params: match.params,
42
- matches: routerContext.matches.map((m)=>{
43
- const { route, pathname, params } = m;
44
- return {
45
- id: route.id,
46
- pathname,
47
- params,
48
- data: routerContext?.loaderData?.[route.id],
49
- handle: route.handle
50
- };
51
- })
55
+ matches: routerContext.matches.map((m)=>({
56
+ id: m.route.id,
57
+ pathname: m.pathname,
58
+ params: m.params,
59
+ data: routerContext?.loaderData?.[m.route.id],
60
+ handle: m.route.handle
61
+ }))
52
62
  };
53
63
  const routeElement = /*#__PURE__*/ react.createElement(ElementComponent, elementProps);
54
- const isLeafRoute = index === routerContext.matches.length - 1;
55
- processedElement = isLeafRoute && cssFiles.length > 0 ? /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(CSSLinks, {
64
+ processedElement = index === cssInjectionIndex ? /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(CSSLinks, {
56
65
  cssFiles
57
66
  }), routeElement) : routeElement;
58
67
  }
59
68
  return {
60
69
  element: processedElement,
61
- errorElement: match.route.errorElement,
62
- handle: match.route.handle,
63
- hasAction: !!match.route.action,
64
- hasErrorBoundary: !!match.route.hasErrorBoundary,
65
- hasLoader: !!match.route.loader,
66
- hasClientLoader: !!match.route.hasClientLoader,
67
- id: match.route.id,
68
- index: match.route.index,
70
+ errorElement: route.errorElement,
71
+ handle: route.handle,
72
+ hasAction: !!route.action,
73
+ hasErrorBoundary: !!route.hasErrorBoundary,
74
+ hasLoader: !!route.loader,
75
+ hasClientLoader: !!route.hasClientLoader,
76
+ id: route.id,
77
+ index: route.index,
69
78
  params: match.params,
70
- parentId: parentMatch?.route.id || match.route.parentId,
71
- path: match.route.path,
79
+ parentId: parentMatch?.route.id || route.parentId,
80
+ path: route.path,
72
81
  pathname: match.pathname,
73
82
  pathnameBase: match.pathnameBase
74
83
  };
@@ -88,10 +97,11 @@ const handleRSCRedirect = (headers, basename, status)=>{
88
97
  });
89
98
  };
90
99
  const prepareRSCRoutes = async (routes)=>{
91
- const isLazyComponent = (component)=>component && 'object' == typeof component && void 0 !== component._init && void 0 !== component._payload;
100
+ const isLazyComponent = (component)=>null != component && 'object' == typeof component && '_init' in component && '_payload' in component;
92
101
  const processRoutes = async (routesList)=>{
93
102
  await Promise.all(routesList.map(async (route)=>{
94
- if ('lazyImport' in route && isLazyComponent(route.component)) route.component = (await route.lazyImport()).default;
103
+ const modernRoute = route;
104
+ if ('lazyImport' in modernRoute && isLazyComponent(modernRoute.component)) modernRoute.component = (await modernRoute.lazyImport()).default;
95
105
  if (route.children && Array.isArray(route.children)) await processRoutes(route.children);
96
106
  }));
97
107
  };
@@ -108,12 +118,14 @@ const mergeRoutes = (routes, originalRoutes)=>{
108
118
  };
109
119
  buildRoutesMap(routes);
110
120
  const mergeRoutesRecursive = (origRoutes)=>origRoutes.map((origRoute)=>{
111
- if (origRoute.id && routesMap.has(origRoute.id)) {
112
- const matchedRoute = routesMap.get(origRoute.id);
121
+ const modernOrig = origRoute;
122
+ if (modernOrig.id && routesMap.has(modernOrig.id)) {
123
+ const matchedRoute = routesMap.get(modernOrig.id);
113
124
  const result = {
114
- loader: origRoute.hasClientLoader ? origRoute.loader : void 0,
125
+ loader: modernOrig.hasClientLoader ? modernOrig.loader : void 0,
115
126
  ...matchedRoute
116
127
  };
128
+ if (modernOrig.isClientComponent) result.isClientComponent = true;
117
129
  if (origRoute.children && Array.isArray(origRoute.children)) result.children = mergeRoutesRecursive(origRoute.children);
118
130
  return result;
119
131
  }
@@ -131,6 +143,68 @@ const findRouteInTree = (routes, routeId)=>{
131
143
  }
132
144
  return null;
133
145
  };
146
+ function getChangedMatches(matches, currentMatches) {
147
+ const currentById = new Map();
148
+ for (const m of currentMatches)if (m.route?.id) currentById.set(m.route.id, m);
149
+ return matches.filter((match)=>{
150
+ const current = currentById.get(match.route?.id);
151
+ return !current || JSON.stringify(current.params) !== JSON.stringify(match.params);
152
+ });
153
+ }
154
+ function canSkipRscFetch(matches, routerState) {
155
+ const changedMatches = getChangedMatches(matches, routerState?.matches || []);
156
+ return changedMatches.length > 0 && changedMatches.every((m)=>{
157
+ const route = m.route;
158
+ return route.isClientComponent && !(route.hasLoader && !route.hasClientLoader);
159
+ });
160
+ }
161
+ function injectRouteCss(routeId) {
162
+ if ("u" < typeof window) return;
163
+ const cssAssets = window._MODERNJS_ROUTE_MANIFEST?.routeAssets?.[routeId]?.referenceCssAssets;
164
+ if (!cssAssets) return;
165
+ const publicPath = window.__webpack_public_path__ || '/';
166
+ for (const css of cssAssets){
167
+ const href = css.startsWith('http') || css.startsWith('/') ? css : publicPath + css;
168
+ if (!document.querySelector(`link[href="${CSS.escape(href)}"]`)) {
169
+ const link = document.createElement('link');
170
+ link.rel = 'stylesheet';
171
+ link.href = href;
172
+ document.head.appendChild(link);
173
+ }
174
+ }
175
+ }
176
+ function ensureClientComponent(route, originalRoutes) {
177
+ if (route.isClientComponent && !route.Component) {
178
+ const origRoute = findRouteInTree(originalRoutes, route.id);
179
+ if (origRoute?.Component) {
180
+ route.Component = origRoute.Component;
181
+ delete route.element;
182
+ }
183
+ }
184
+ }
185
+ function resolveClientLoaders(matches, originalRoutes) {
186
+ const clientMatches = matches.filter((m)=>m.route.hasClientLoader);
187
+ if (0 === clientMatches.length) return Promise.resolve([]);
188
+ return Promise.all(clientMatches.map(async (clientMatch)=>{
189
+ const origRoute = findRouteInTree(originalRoutes, clientMatch.route.id);
190
+ clientMatch.route.loader = origRoute?.loader;
191
+ const result = await clientMatch.resolve();
192
+ return {
193
+ routeId: clientMatch.route.id,
194
+ result
195
+ };
196
+ }));
197
+ }
198
+ function applyLoaderResults(results, loaderResults) {
199
+ for (const { routeId, result } of loaderResults)results[routeId] = result;
200
+ }
201
+ async function resolveClientOnlyNavigation(matches, results, originalRoutes, routerState) {
202
+ const changedMatches = getChangedMatches(matches, routerState?.matches || []);
203
+ for (const match of changedMatches)ensureClientComponent(match.route, originalRoutes);
204
+ applyLoaderResults(results, await resolveClientLoaders(changedMatches, originalRoutes));
205
+ for (const match of changedMatches)if (match.route.id) injectRouteCss(match.route.id);
206
+ return results;
207
+ }
134
208
  const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>{
135
209
  const processedRoutes = payload.routes.reduceRight((previous, route)=>{
136
210
  if (previous.length > 0) return [
@@ -147,42 +221,28 @@ const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>
147
221
  const router = createBrowserRouter(mergedRoutes, {
148
222
  hydrationData: payload,
149
223
  basename: basename,
150
- dataStrategy: async (context)=>{
151
- const { request, matches } = context;
224
+ dataStrategy: async ({ request, matches })=>{
152
225
  const results = {};
153
- const clientMatches = matches.filter((match)=>match.route.hasClientLoader);
226
+ if (canSkipRscFetch(matches, router.state)) return resolveClientOnlyNavigation(matches, results, originalRoutes, router.state);
154
227
  const fetchPromise = fetch(request.url, {
155
228
  headers: {
156
229
  'x-rsc-tree': 'true'
157
230
  }
158
231
  });
159
- const clientLoadersPromise = clientMatches.length > 0 ? Promise.all(clientMatches.map(async (clientMatch)=>{
160
- const foundRoute = findRouteInTree(originalRoutes, clientMatch.route.id);
161
- clientMatch.route.loader = foundRoute?.loader;
162
- const res = await clientMatch.resolve();
163
- return {
164
- routeId: clientMatch.route.id,
165
- result: res
166
- };
167
- })) : Promise.resolve([]);
232
+ const clientLoadersPromise = resolveClientLoaders(matches, originalRoutes);
168
233
  const res = await fetchPromise;
169
234
  const redirectLocation = res.headers.get('X-Modernjs-Redirect');
170
235
  if (redirectLocation) {
171
236
  matches.forEach((match)=>{
172
237
  const routeId = match.route.id;
173
238
  if (routeId) results[routeId] = {
174
- type: 'redirect',
239
+ type: 'data',
175
240
  result: redirect(redirectLocation)
176
241
  };
177
242
  });
178
243
  return results;
179
244
  }
180
- const [clientLoaderResults] = await Promise.all([
181
- clientLoadersPromise
182
- ]);
183
- clientLoaderResults.forEach(({ routeId, result })=>{
184
- results[routeId] = result;
185
- });
245
+ applyLoaderResults(results, await clientLoadersPromise);
186
246
  if (!res.body) throw new Error('Response body is null');
187
247
  const payload = await createFromReadableStream(res.body);
188
248
  if ('object' != typeof payload || null === payload || void 0 === payload.type || 'render' !== payload.type) throw new Error('Unexpected payload type');
@@ -190,10 +250,14 @@ const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>
190
250
  matches.forEach((match)=>{
191
251
  const routeId = match.route.id;
192
252
  const matchedRoute = serverPayload.routes.find((route)=>route.id === routeId);
193
- if (matchedRoute) router.patchRoutes(matchedRoute.parentId, [
194
- matchedRoute
195
- ], true);
196
- if (serverPayload.loaderData?.[routeId]) results[routeId] = {
253
+ if (matchedRoute) {
254
+ const modernMatch = match.route;
255
+ router.patchRoutes(matchedRoute.parentId ?? null, [
256
+ matchedRoute
257
+ ], true);
258
+ if (modernMatch.isClientComponent && modernMatch.Component) delete modernMatch.Component;
259
+ }
260
+ if (serverPayload.loaderData && routeId in serverPayload.loaderData) results[routeId] = {
197
261
  type: 'data',
198
262
  result: serverPayload.loaderData[routeId]
199
263
  };
@@ -68,7 +68,12 @@ function getRouteObjects(routes, { globalApp, ssrMode, props }) {
68
68
  ...'object' == typeof route.config ? route.config?.handle : {}
69
69
  },
70
70
  index: route.index,
71
+ hasLoader: !!route.loader,
71
72
  hasClientLoader: !!route.clientData,
73
+ hasAction: !!route.action,
74
+ ...route.isClientComponent ? {
75
+ isClientComponent: true
76
+ } : {},
72
77
  Component: route.component ? route.component : void 0,
73
78
  errorElement: route.error ? /*#__PURE__*/ jsx(route.error, {}) : void 0,
74
79
  children: route.children ? route.children.map((child)=>getRouteObjects([
@@ -1,10 +1,10 @@
1
- import type { RouterState } from '@modern-js/runtime-utils/router';
1
+ import type { RouterState, ShouldRevalidateFunction } from '@modern-js/runtime-utils/router';
2
2
  export type PayloadRoute = {
3
- clientAction?: any;
4
- clientLoader?: any;
3
+ clientAction?: (...args: unknown[]) => unknown;
4
+ clientLoader?: (...args: unknown[]) => unknown;
5
5
  element?: React.ReactNode;
6
6
  errorElement?: React.ReactNode;
7
- handle?: any;
7
+ handle?: unknown;
8
8
  hasAction: boolean;
9
9
  hasErrorBoundary: boolean;
10
10
  hasLoader: boolean;
@@ -16,7 +16,7 @@ export type PayloadRoute = {
16
16
  path?: string;
17
17
  pathname: string;
18
18
  pathnameBase: string;
19
- shouldRevalidate?: any;
19
+ shouldRevalidate?: ShouldRevalidateFunction;
20
20
  };
21
21
  export type ServerPayload = {
22
22
  type: 'render';
@@ -3,7 +3,7 @@ import type { Entrypoint, NestedRouteForCli, PageRoute, RouteLegacy, SSRMode } f
3
3
  export declare const routesForServer: ({ routesForServerLoaderMatches, }: {
4
4
  routesForServerLoaderMatches: (NestedRouteForCli | PageRoute)[];
5
5
  }) => string;
6
- export declare const fileSystemRoutes: ({ metaName, routes, ssrMode, nestedRoutesEntry, entryName, internalDirectory, splitRouteChunks, isRscClient, }: {
6
+ export declare const fileSystemRoutes: ({ metaName, routes, ssrMode, nestedRoutesEntry, entryName, internalDirectory, splitRouteChunks, isRscClientBundle, srcDirectory, internalSrcAlias, }: {
7
7
  metaName: string;
8
8
  routes: RouteLegacy[] | (NestedRouteForCli | PageRoute)[];
9
9
  ssrMode?: SSRMode;
@@ -11,7 +11,9 @@ export declare const fileSystemRoutes: ({ metaName, routes, ssrMode, nestedRoute
11
11
  entryName: string;
12
12
  internalDirectory: string;
13
13
  splitRouteChunks?: boolean;
14
- isRscClient?: boolean;
14
+ isRscClientBundle?: boolean;
15
+ srcDirectory?: string;
16
+ internalSrcAlias?: string;
15
17
  }) => Promise<string>;
16
18
  export declare function ssrLoaderCombinedModule(entrypoints: Entrypoint[], entrypoint: Entrypoint, config: AppNormalizedConfig, appContext: AppToolsContext): string | null;
17
19
  export declare const runtimeGlobalContext: ({ entryName, metaName, srcDirectory, nestedRoutesEntry, internalSrcAlias, globalApp, rscType, basename, }: {
@@ -1,7 +1,13 @@
1
- import { type StaticHandlerContext } from '@modern-js/runtime-utils/router';
2
- import { type RouteObject } from '@modern-js/runtime-utils/router';
1
+ import { type RouteObject, type StaticHandlerContext } from '@modern-js/runtime-utils/router';
3
2
  import React from 'react';
4
3
  import type { ServerPayload } from '../../core/context';
4
+ import type { RouteManifest } from './types';
5
+ declare global {
6
+ interface Window {
7
+ _MODERNJS_ROUTE_MANIFEST?: RouteManifest;
8
+ __webpack_public_path__?: string;
9
+ }
10
+ }
5
11
  export declare const createServerPayload: (routerContext: StaticHandlerContext, routes: RouteObject[]) => ServerPayload;
6
12
  export declare const handleRSCRedirect: (headers: Headers, basename: string, status: number) => Response;
7
13
  export declare const prepareRSCRoutes: (routes: RouteObject[]) => Promise<void>;
@@ -43,8 +43,25 @@ export interface RouteAssets {
43
43
  [routeId: string]: {
44
44
  chunkIds?: (string | number)[];
45
45
  assets?: string[];
46
+ referenceCssAssets?: string[];
46
47
  };
47
48
  }
49
+ export interface LazyComponentDescriptor {
50
+ _init: unknown;
51
+ _payload: unknown;
52
+ }
53
+ export type ModernRouteObject = RouteObject & {
54
+ isClientComponent?: boolean;
55
+ hasClientLoader?: boolean;
56
+ hasLoader?: boolean;
57
+ hasAction?: boolean;
58
+ parentId?: string;
59
+ lazyImport?: () => Promise<{
60
+ default: React.ComponentType;
61
+ }>;
62
+ component?: React.ComponentType | LazyComponentDescriptor;
63
+ entryCssFiles?: string[];
64
+ };
48
65
  interface DataFunctionArgs<D = any> {
49
66
  request: Request;
50
67
  params: Params;
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "modern",
16
16
  "modern.js"
17
17
  ],
18
- "version": "3.0.4",
18
+ "version": "3.0.5",
19
19
  "engines": {
20
20
  "node": ">=20"
21
21
  },
@@ -215,12 +215,12 @@
215
215
  "isbot": "3.8.0",
216
216
  "react-helmet": "^6.1.0",
217
217
  "react-is": "^18.3.1",
218
- "@modern-js/plugin": "3.0.4",
219
- "@modern-js/plugin-data-loader": "3.0.4",
220
- "@modern-js/render": "3.0.4",
221
- "@modern-js/runtime-utils": "3.0.4",
222
- "@modern-js/types": "3.0.4",
223
- "@modern-js/utils": "3.0.4"
218
+ "@modern-js/plugin": "3.0.5",
219
+ "@modern-js/plugin-data-loader": "3.0.5",
220
+ "@modern-js/render": "3.0.5",
221
+ "@modern-js/types": "3.0.5",
222
+ "@modern-js/runtime-utils": "3.0.5",
223
+ "@modern-js/utils": "3.0.5"
224
224
  },
225
225
  "peerDependencies": {
226
226
  "react": ">=17.0.2",
@@ -240,7 +240,7 @@
240
240
  "react-dom": "^19.2.4",
241
241
  "ts-node": "^10.9.2",
242
242
  "typescript": "^5",
243
- "@modern-js/app-tools": "3.0.4",
243
+ "@modern-js/app-tools": "3.0.5",
244
244
  "@modern-js/rslib": "2.68.10",
245
245
  "@scripts/rstest-config": "2.66.0"
246
246
  },