@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.
@@ -130,7 +130,9 @@ const generateCode = async (appContext, config, entrypoints, api)=>{
130
130
  entryName: entrypoint.entryName,
131
131
  internalDirectory,
132
132
  splitRouteChunks: config?.output?.splitRouteChunks,
133
- isRscClient: (0, utils_namespaceObject.isUseRsc)(config)
133
+ isRscClientBundle: (0, utils_namespaceObject.isUseRsc)(config),
134
+ srcDirectory,
135
+ internalSrcAlias: appContext.internalSrcAlias
134
136
  })
135
137
  });
136
138
  if (entrypoint.nestedRoutesEntry && ((0, utils_namespaceObject.isUseSSRBundle)(config) || (0, utils_namespaceObject.isUseRsc)(config))) {
@@ -150,7 +152,9 @@ const generateCode = async (appContext, config, entrypoints, api)=>{
150
152
  entryName: entrypoint.entryName,
151
153
  internalDirectory,
152
154
  splitRouteChunks: config?.output?.splitRouteChunks,
153
- isRscClient: false
155
+ isRscClientBundle: false,
156
+ srcDirectory,
157
+ internalSrcAlias: appContext.internalSrcAlias
154
158
  });
155
159
  await utils_namespaceObject.fs.outputFile(external_path_default().resolve(internalDirectory, `./${entryName}/routes.server.js`), serverRoutesCode, 'utf8');
156
160
  }
@@ -38,6 +38,7 @@ __webpack_require__.d(__webpack_exports__, {
38
38
  routesForServer: ()=>routesForServer,
39
39
  fileSystemRoutes: ()=>fileSystemRoutes
40
40
  });
41
+ const promises_namespaceObject = require("node:fs/promises");
41
42
  const external_path_namespaceObject = require("path");
42
43
  var external_path_default = /*#__PURE__*/ __webpack_require__.n(external_path_namespaceObject);
43
44
  const utils_namespaceObject = require("@modern-js/utils");
@@ -111,7 +112,25 @@ const routesForServer = ({ routesForServerLoaderMatches })=>{
111
112
  `;
112
113
  };
113
114
  const createMatchReg = (keyword)=>new RegExp(`("${keyword}":\\s)"([^\n]+)"`, 'g');
114
- const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry, entryName, internalDirectory, splitRouteChunks = true, isRscClient = false })=>{
115
+ async function hasUseClientDirective(componentPath, srcDirectory, internalSrcAlias) {
116
+ let realPath = componentPath;
117
+ if (internalSrcAlias && srcDirectory && realPath.startsWith(internalSrcAlias)) realPath = external_path_default().join(srcDirectory, realPath.slice(internalSrcAlias.length));
118
+ const filePath = (0, utils_namespaceObject.findExists)(utils_namespaceObject.JS_EXTENSIONS.map((ext)=>`${realPath}${ext}`));
119
+ if (!filePath) return false;
120
+ let fh;
121
+ try {
122
+ fh = await (0, promises_namespaceObject.open)(filePath, 'r');
123
+ const buf = Buffer.alloc(64);
124
+ const { bytesRead } = await fh.read(buf, 0, 64, 0);
125
+ const content = buf.toString('utf-8', 0, bytesRead).trimStart();
126
+ return content.startsWith("'use client'") || content.startsWith('"use client"');
127
+ } catch {
128
+ return false;
129
+ } finally{
130
+ await fh?.close();
131
+ }
132
+ }
133
+ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry, entryName, internalDirectory, splitRouteChunks = true, isRscClientBundle = false, srcDirectory, internalSrcAlias })=>{
115
134
  const components = [];
116
135
  const loadings = [];
117
136
  const errors = [];
@@ -136,9 +155,9 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
136
155
  const importOptions = webpackChunkName ? `/* webpackChunkName: "${routeId}" */ ` : eager ? '/* webpackMode: "eager" */ ' : '';
137
156
  return `() => import(${importOptions}'${componentPath}').then(routeModule => handleRouteModule(routeModule, "${routeId}")).catch(handleRouteModuleError)`;
138
157
  };
139
- const traverseRouteTree = (route, isRscClient)=>{
158
+ const traverseRouteTree = async (route, isRscClientBundle)=>{
140
159
  let children;
141
- if ('children' in route && route.children) children = route?.children?.map((child)=>traverseRouteTree(child, isRscClient));
160
+ if ('children' in route && route.children) children = await Promise.all(route.children.map((child)=>traverseRouteTree(child, isRscClientBundle)));
142
161
  let loading;
143
162
  let error;
144
163
  let loader;
@@ -210,6 +229,8 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
210
229
  components.push(route._component);
211
230
  component = `component_${components.length - 1}`;
212
231
  }
232
+ const isClientComponent = 'nested' === route.type && Boolean(route._component) && Boolean(srcDirectory) && Boolean(internalSrcAlias) && await hasUseClientDirective(route._component, srcDirectory, internalSrcAlias);
233
+ const shouldIncludeClientBundle = !isRscClientBundle || isClientComponent;
213
234
  const finalRoute = {
214
235
  ...route,
215
236
  loading,
@@ -217,10 +238,17 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
217
238
  action,
218
239
  config,
219
240
  error,
220
- children
241
+ children,
242
+ ...isClientComponent && {
243
+ isClientComponent: true
244
+ },
245
+ ...shouldIncludeClientBundle && {
246
+ lazyImport
247
+ },
248
+ ...shouldIncludeClientBundle && route._component && {
249
+ component
250
+ }
221
251
  };
222
- if (!isRscClient) finalRoute.lazyImport = lazyImport;
223
- if (route._component && !isRscClient) finalRoute.component = component;
224
252
  if ('nested' === route.type && route._component && (route.loader || route.data)) finalRoute.shouldRevalidate = `createShouldRevalidate("${route.id}")`;
225
253
  return finalRoute;
226
254
  };
@@ -228,7 +256,7 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
228
256
  export const routes = [
229
257
  `;
230
258
  for (const route of routes)if ('type' in route) {
231
- const newRoute = traverseRouteTree(route, isRscClient);
259
+ const newRoute = await traverseRouteTree(route, isRscClientBundle);
232
260
  const routeStr = JSON.stringify(newRoute, null, 2);
233
261
  const keywords = [
234
262
  'component',
@@ -303,9 +331,9 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
303
331
  `;
304
332
  return `
305
333
  ${importLazyCode}
306
- ${!isRscClient ? importComponentsCode : ''}
334
+ ${!isRscClientBundle ? importComponentsCode : ''}
307
335
  ${importRuntimeRouterCode}
308
- ${!isRscClient ? rootLayoutCode : ''}
336
+ ${!isRscClientBundle ? rootLayoutCode : ''}
309
337
  ${importLoadingCode}
310
338
  ${importErrorComponentsCode}
311
339
  ${importLoadersCode}
@@ -45,8 +45,9 @@ const router_namespaceObject = require("@modern-js/runtime-utils/router");
45
45
  const external_react_namespaceObject = require("react");
46
46
  var external_react_default = /*#__PURE__*/ __webpack_require__.n(external_react_namespaceObject);
47
47
  const external_CSSLinks_js_namespaceObject = require("./CSSLinks.js");
48
- const safeUse = (promise)=>{
49
- if ('function' == typeof external_react_default().use) return external_react_default().use(promise);
48
+ const safeUse = (value)=>{
49
+ const reactUse = external_react_default().use;
50
+ if ('function' == typeof reactUse) return reactUse(value);
50
51
  return null;
51
52
  };
52
53
  function collectCssFilesFromRoutes(matches, routes) {
@@ -64,6 +65,17 @@ function collectCssFilesFromRoutes(matches, routes) {
64
65
  }
65
66
  const createServerPayload = (routerContext, routes)=>{
66
67
  const cssFiles = collectCssFilesFromRoutes(routerContext.matches, routes);
68
+ let cssInjectionIndex = -1;
69
+ if (cssFiles.length > 0) {
70
+ for(let i = routerContext.matches.length - 1; i >= 0; i--){
71
+ const matchRoute = findRouteInTree(routes, routerContext.matches[i].route.id);
72
+ if (matchRoute && !matchRoute.isClientComponent) {
73
+ cssInjectionIndex = i;
74
+ break;
75
+ }
76
+ }
77
+ if (-1 === cssInjectionIndex) cssInjectionIndex = routerContext.matches.length - 1;
78
+ }
67
79
  return {
68
80
  type: 'render',
69
81
  actionData: routerContext.actionData,
@@ -71,45 +83,42 @@ const createServerPayload = (routerContext, routes)=>{
71
83
  loaderData: routerContext.loaderData,
72
84
  location: routerContext.location,
73
85
  routes: routerContext.matches.map((match, index, matches)=>{
74
- const element = match.route.element;
86
+ const route = match.route;
87
+ const element = route.element;
75
88
  const parentMatch = index > 0 ? matches[index - 1] : void 0;
76
89
  let processedElement;
77
90
  if (element) {
78
91
  const ElementComponent = element.type;
79
92
  const elementProps = {
80
- loaderData: routerContext?.loaderData?.[match.route.id],
81
- actionData: routerContext?.actionData?.[match.route.id],
93
+ loaderData: routerContext?.loaderData?.[route.id],
94
+ actionData: routerContext?.actionData?.[route.id],
82
95
  params: match.params,
83
- matches: routerContext.matches.map((m)=>{
84
- const { route, pathname, params } = m;
85
- return {
86
- id: route.id,
87
- pathname,
88
- params,
89
- data: routerContext?.loaderData?.[route.id],
90
- handle: route.handle
91
- };
92
- })
96
+ matches: routerContext.matches.map((m)=>({
97
+ id: m.route.id,
98
+ pathname: m.pathname,
99
+ params: m.params,
100
+ data: routerContext?.loaderData?.[m.route.id],
101
+ handle: m.route.handle
102
+ }))
93
103
  };
94
104
  const routeElement = /*#__PURE__*/ external_react_default().createElement(ElementComponent, elementProps);
95
- const isLeafRoute = index === routerContext.matches.length - 1;
96
- processedElement = isLeafRoute && cssFiles.length > 0 ? /*#__PURE__*/ external_react_default().createElement(external_react_default().Fragment, null, /*#__PURE__*/ external_react_default().createElement(external_CSSLinks_js_namespaceObject.CSSLinks, {
105
+ processedElement = index === cssInjectionIndex ? /*#__PURE__*/ external_react_default().createElement(external_react_default().Fragment, null, /*#__PURE__*/ external_react_default().createElement(external_CSSLinks_js_namespaceObject.CSSLinks, {
97
106
  cssFiles
98
107
  }), routeElement) : routeElement;
99
108
  }
100
109
  return {
101
110
  element: processedElement,
102
- errorElement: match.route.errorElement,
103
- handle: match.route.handle,
104
- hasAction: !!match.route.action,
105
- hasErrorBoundary: !!match.route.hasErrorBoundary,
106
- hasLoader: !!match.route.loader,
107
- hasClientLoader: !!match.route.hasClientLoader,
108
- id: match.route.id,
109
- index: match.route.index,
111
+ errorElement: route.errorElement,
112
+ handle: route.handle,
113
+ hasAction: !!route.action,
114
+ hasErrorBoundary: !!route.hasErrorBoundary,
115
+ hasLoader: !!route.loader,
116
+ hasClientLoader: !!route.hasClientLoader,
117
+ id: route.id,
118
+ index: route.index,
110
119
  params: match.params,
111
- parentId: parentMatch?.route.id || match.route.parentId,
112
- path: match.route.path,
120
+ parentId: parentMatch?.route.id || route.parentId,
121
+ path: route.path,
113
122
  pathname: match.pathname,
114
123
  pathnameBase: match.pathnameBase
115
124
  };
@@ -129,10 +138,11 @@ const handleRSCRedirect = (headers, basename, status)=>{
129
138
  });
130
139
  };
131
140
  const prepareRSCRoutes = async (routes)=>{
132
- const isLazyComponent = (component)=>component && 'object' == typeof component && void 0 !== component._init && void 0 !== component._payload;
141
+ const isLazyComponent = (component)=>null != component && 'object' == typeof component && '_init' in component && '_payload' in component;
133
142
  const processRoutes = async (routesList)=>{
134
143
  await Promise.all(routesList.map(async (route)=>{
135
- if ('lazyImport' in route && isLazyComponent(route.component)) route.component = (await route.lazyImport()).default;
144
+ const modernRoute = route;
145
+ if ('lazyImport' in modernRoute && isLazyComponent(modernRoute.component)) modernRoute.component = (await modernRoute.lazyImport()).default;
136
146
  if (route.children && Array.isArray(route.children)) await processRoutes(route.children);
137
147
  }));
138
148
  };
@@ -149,12 +159,14 @@ const mergeRoutes = (routes, originalRoutes)=>{
149
159
  };
150
160
  buildRoutesMap(routes);
151
161
  const mergeRoutesRecursive = (origRoutes)=>origRoutes.map((origRoute)=>{
152
- if (origRoute.id && routesMap.has(origRoute.id)) {
153
- const matchedRoute = routesMap.get(origRoute.id);
162
+ const modernOrig = origRoute;
163
+ if (modernOrig.id && routesMap.has(modernOrig.id)) {
164
+ const matchedRoute = routesMap.get(modernOrig.id);
154
165
  const result = {
155
- loader: origRoute.hasClientLoader ? origRoute.loader : void 0,
166
+ loader: modernOrig.hasClientLoader ? modernOrig.loader : void 0,
156
167
  ...matchedRoute
157
168
  };
169
+ if (modernOrig.isClientComponent) result.isClientComponent = true;
158
170
  if (origRoute.children && Array.isArray(origRoute.children)) result.children = mergeRoutesRecursive(origRoute.children);
159
171
  return result;
160
172
  }
@@ -172,6 +184,68 @@ const findRouteInTree = (routes, routeId)=>{
172
184
  }
173
185
  return null;
174
186
  };
187
+ function getChangedMatches(matches, currentMatches) {
188
+ const currentById = new Map();
189
+ for (const m of currentMatches)if (m.route?.id) currentById.set(m.route.id, m);
190
+ return matches.filter((match)=>{
191
+ const current = currentById.get(match.route?.id);
192
+ return !current || JSON.stringify(current.params) !== JSON.stringify(match.params);
193
+ });
194
+ }
195
+ function canSkipRscFetch(matches, routerState) {
196
+ const changedMatches = getChangedMatches(matches, routerState?.matches || []);
197
+ return changedMatches.length > 0 && changedMatches.every((m)=>{
198
+ const route = m.route;
199
+ return route.isClientComponent && !(route.hasLoader && !route.hasClientLoader);
200
+ });
201
+ }
202
+ function injectRouteCss(routeId) {
203
+ if ("u" < typeof window) return;
204
+ const cssAssets = window._MODERNJS_ROUTE_MANIFEST?.routeAssets?.[routeId]?.referenceCssAssets;
205
+ if (!cssAssets) return;
206
+ const publicPath = window.__webpack_public_path__ || '/';
207
+ for (const css of cssAssets){
208
+ const href = css.startsWith('http') || css.startsWith('/') ? css : publicPath + css;
209
+ if (!document.querySelector(`link[href="${CSS.escape(href)}"]`)) {
210
+ const link = document.createElement('link');
211
+ link.rel = 'stylesheet';
212
+ link.href = href;
213
+ document.head.appendChild(link);
214
+ }
215
+ }
216
+ }
217
+ function ensureClientComponent(route, originalRoutes) {
218
+ if (route.isClientComponent && !route.Component) {
219
+ const origRoute = findRouteInTree(originalRoutes, route.id);
220
+ if (origRoute?.Component) {
221
+ route.Component = origRoute.Component;
222
+ delete route.element;
223
+ }
224
+ }
225
+ }
226
+ function resolveClientLoaders(matches, originalRoutes) {
227
+ const clientMatches = matches.filter((m)=>m.route.hasClientLoader);
228
+ if (0 === clientMatches.length) return Promise.resolve([]);
229
+ return Promise.all(clientMatches.map(async (clientMatch)=>{
230
+ const origRoute = findRouteInTree(originalRoutes, clientMatch.route.id);
231
+ clientMatch.route.loader = origRoute?.loader;
232
+ const result = await clientMatch.resolve();
233
+ return {
234
+ routeId: clientMatch.route.id,
235
+ result
236
+ };
237
+ }));
238
+ }
239
+ function applyLoaderResults(results, loaderResults) {
240
+ for (const { routeId, result } of loaderResults)results[routeId] = result;
241
+ }
242
+ async function resolveClientOnlyNavigation(matches, results, originalRoutes, routerState) {
243
+ const changedMatches = getChangedMatches(matches, routerState?.matches || []);
244
+ for (const match of changedMatches)ensureClientComponent(match.route, originalRoutes);
245
+ applyLoaderResults(results, await resolveClientLoaders(changedMatches, originalRoutes));
246
+ for (const match of changedMatches)if (match.route.id) injectRouteCss(match.route.id);
247
+ return results;
248
+ }
175
249
  const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>{
176
250
  const processedRoutes = payload.routes.reduceRight((previous, route)=>{
177
251
  if (previous.length > 0) return [
@@ -188,42 +262,28 @@ const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>
188
262
  const router = (0, router_namespaceObject.createBrowserRouter)(mergedRoutes, {
189
263
  hydrationData: payload,
190
264
  basename: basename,
191
- dataStrategy: async (context)=>{
192
- const { request, matches } = context;
265
+ dataStrategy: async ({ request, matches })=>{
193
266
  const results = {};
194
- const clientMatches = matches.filter((match)=>match.route.hasClientLoader);
267
+ if (canSkipRscFetch(matches, router.state)) return resolveClientOnlyNavigation(matches, results, originalRoutes, router.state);
195
268
  const fetchPromise = fetch(request.url, {
196
269
  headers: {
197
270
  'x-rsc-tree': 'true'
198
271
  }
199
272
  });
200
- const clientLoadersPromise = clientMatches.length > 0 ? Promise.all(clientMatches.map(async (clientMatch)=>{
201
- const foundRoute = findRouteInTree(originalRoutes, clientMatch.route.id);
202
- clientMatch.route.loader = foundRoute?.loader;
203
- const res = await clientMatch.resolve();
204
- return {
205
- routeId: clientMatch.route.id,
206
- result: res
207
- };
208
- })) : Promise.resolve([]);
273
+ const clientLoadersPromise = resolveClientLoaders(matches, originalRoutes);
209
274
  const res = await fetchPromise;
210
275
  const redirectLocation = res.headers.get('X-Modernjs-Redirect');
211
276
  if (redirectLocation) {
212
277
  matches.forEach((match)=>{
213
278
  const routeId = match.route.id;
214
279
  if (routeId) results[routeId] = {
215
- type: 'redirect',
280
+ type: 'data',
216
281
  result: (0, router_namespaceObject.redirect)(redirectLocation)
217
282
  };
218
283
  });
219
284
  return results;
220
285
  }
221
- const [clientLoaderResults] = await Promise.all([
222
- clientLoadersPromise
223
- ]);
224
- clientLoaderResults.forEach(({ routeId, result })=>{
225
- results[routeId] = result;
226
- });
286
+ applyLoaderResults(results, await clientLoadersPromise);
227
287
  if (!res.body) throw new Error('Response body is null');
228
288
  const payload = await (0, client_namespaceObject.createFromReadableStream)(res.body);
229
289
  if ('object' != typeof payload || null === payload || void 0 === payload.type || 'render' !== payload.type) throw new Error('Unexpected payload type');
@@ -231,10 +291,14 @@ const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>
231
291
  matches.forEach((match)=>{
232
292
  const routeId = match.route.id;
233
293
  const matchedRoute = serverPayload.routes.find((route)=>route.id === routeId);
234
- if (matchedRoute) router.patchRoutes(matchedRoute.parentId, [
235
- matchedRoute
236
- ], true);
237
- if (serverPayload.loaderData?.[routeId]) results[routeId] = {
294
+ if (matchedRoute) {
295
+ const modernMatch = match.route;
296
+ router.patchRoutes(matchedRoute.parentId ?? null, [
297
+ matchedRoute
298
+ ], true);
299
+ if (modernMatch.isClientComponent && modernMatch.Component) delete modernMatch.Component;
300
+ }
301
+ if (serverPayload.loaderData && routeId in serverPayload.loaderData) results[routeId] = {
238
302
  type: 'data',
239
303
  result: serverPayload.loaderData[routeId]
240
304
  };
@@ -114,7 +114,12 @@ function getRouteObjects(routes, { globalApp, ssrMode, props }) {
114
114
  ...'object' == typeof route.config ? route.config?.handle : {}
115
115
  },
116
116
  index: route.index,
117
+ hasLoader: !!route.loader,
117
118
  hasClientLoader: !!route.clientData,
119
+ hasAction: !!route.action,
120
+ ...route.isClientComponent ? {
121
+ isClientComponent: true
122
+ } : {},
118
123
  Component: route.component ? route.component : void 0,
119
124
  errorElement: route.error ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(route.error, {}) : void 0,
120
125
  children: route.children ? route.children.map((child)=>getRouteObjects([
@@ -89,7 +89,9 @@ const generateCode = async (appContext, config, entrypoints, api)=>{
89
89
  entryName: entrypoint.entryName,
90
90
  internalDirectory,
91
91
  splitRouteChunks: config?.output?.splitRouteChunks,
92
- isRscClient: isUseRsc(config)
92
+ isRscClientBundle: isUseRsc(config),
93
+ srcDirectory,
94
+ internalSrcAlias: appContext.internalSrcAlias
93
95
  })
94
96
  });
95
97
  if (entrypoint.nestedRoutesEntry && (isUseSSRBundle(config) || isUseRsc(config))) {
@@ -109,7 +111,9 @@ const generateCode = async (appContext, config, entrypoints, api)=>{
109
111
  entryName: entrypoint.entryName,
110
112
  internalDirectory,
111
113
  splitRouteChunks: config?.output?.splitRouteChunks,
112
- isRscClient: false
114
+ isRscClientBundle: false,
115
+ srcDirectory,
116
+ internalSrcAlias: appContext.internalSrcAlias
113
117
  });
114
118
  await fs.outputFile(path.resolve(internalDirectory, `./${entryName}/routes.server.js`), serverRoutesCode, 'utf8');
115
119
  }
@@ -1,5 +1,6 @@
1
+ import { open as promises_open } from "node:fs/promises";
1
2
  import path from "path";
2
- import { findExists, formatImportPath, fs, getEntryOptions, isSSGEntry, slash } from "@modern-js/utils";
3
+ import { JS_EXTENSIONS, findExists, formatImportPath, fs, getEntryOptions, isSSGEntry, slash } from "@modern-js/utils";
3
4
  import { ROUTE_MODULES } from "@modern-js/utils/universal/constants";
4
5
  import { APP_INIT_EXPORTED, TEMP_LOADERS_DIR } from "../constants.mjs";
5
6
  import { getPathWithoutExt, getServerLoadersFile, parseModule, replaceWithAlias } from "./utils.mjs";
@@ -70,7 +71,25 @@ const routesForServer = ({ routesForServerLoaderMatches })=>{
70
71
  `;
71
72
  };
72
73
  const createMatchReg = (keyword)=>new RegExp(`("${keyword}":\\s)"([^\n]+)"`, 'g');
73
- const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry, entryName, internalDirectory, splitRouteChunks = true, isRscClient = false })=>{
74
+ async function hasUseClientDirective(componentPath, srcDirectory, internalSrcAlias) {
75
+ let realPath = componentPath;
76
+ if (internalSrcAlias && srcDirectory && realPath.startsWith(internalSrcAlias)) realPath = path.join(srcDirectory, realPath.slice(internalSrcAlias.length));
77
+ const filePath = findExists(JS_EXTENSIONS.map((ext)=>`${realPath}${ext}`));
78
+ if (!filePath) return false;
79
+ let fh;
80
+ try {
81
+ fh = await promises_open(filePath, 'r');
82
+ const buf = Buffer.alloc(64);
83
+ const { bytesRead } = await fh.read(buf, 0, 64, 0);
84
+ const content = buf.toString('utf-8', 0, bytesRead).trimStart();
85
+ return content.startsWith("'use client'") || content.startsWith('"use client"');
86
+ } catch {
87
+ return false;
88
+ } finally{
89
+ await fh?.close();
90
+ }
91
+ }
92
+ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry, entryName, internalDirectory, splitRouteChunks = true, isRscClientBundle = false, srcDirectory, internalSrcAlias })=>{
74
93
  const components = [];
75
94
  const loadings = [];
76
95
  const errors = [];
@@ -95,9 +114,9 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
95
114
  const importOptions = webpackChunkName ? `/* webpackChunkName: "${routeId}" */ ` : eager ? '/* webpackMode: "eager" */ ' : '';
96
115
  return `() => import(${importOptions}'${componentPath}').then(routeModule => handleRouteModule(routeModule, "${routeId}")).catch(handleRouteModuleError)`;
97
116
  };
98
- const traverseRouteTree = (route, isRscClient)=>{
117
+ const traverseRouteTree = async (route, isRscClientBundle)=>{
99
118
  let children;
100
- if ('children' in route && route.children) children = route?.children?.map((child)=>traverseRouteTree(child, isRscClient));
119
+ if ('children' in route && route.children) children = await Promise.all(route.children.map((child)=>traverseRouteTree(child, isRscClientBundle)));
101
120
  let loading;
102
121
  let error;
103
122
  let loader;
@@ -169,6 +188,8 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
169
188
  components.push(route._component);
170
189
  component = `component_${components.length - 1}`;
171
190
  }
191
+ const isClientComponent = 'nested' === route.type && Boolean(route._component) && Boolean(srcDirectory) && Boolean(internalSrcAlias) && await hasUseClientDirective(route._component, srcDirectory, internalSrcAlias);
192
+ const shouldIncludeClientBundle = !isRscClientBundle || isClientComponent;
172
193
  const finalRoute = {
173
194
  ...route,
174
195
  loading,
@@ -176,10 +197,17 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
176
197
  action,
177
198
  config,
178
199
  error,
179
- children
200
+ children,
201
+ ...isClientComponent && {
202
+ isClientComponent: true
203
+ },
204
+ ...shouldIncludeClientBundle && {
205
+ lazyImport
206
+ },
207
+ ...shouldIncludeClientBundle && route._component && {
208
+ component
209
+ }
180
210
  };
181
- if (!isRscClient) finalRoute.lazyImport = lazyImport;
182
- if (route._component && !isRscClient) finalRoute.component = component;
183
211
  if ('nested' === route.type && route._component && (route.loader || route.data)) finalRoute.shouldRevalidate = `createShouldRevalidate("${route.id}")`;
184
212
  return finalRoute;
185
213
  };
@@ -187,7 +215,7 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
187
215
  export const routes = [
188
216
  `;
189
217
  for (const route of routes)if ('type' in route) {
190
- const newRoute = traverseRouteTree(route, isRscClient);
218
+ const newRoute = await traverseRouteTree(route, isRscClientBundle);
191
219
  const routeStr = JSON.stringify(newRoute, null, 2);
192
220
  const keywords = [
193
221
  'component',
@@ -262,9 +290,9 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
262
290
  `;
263
291
  return `
264
292
  ${importLazyCode}
265
- ${!isRscClient ? importComponentsCode : ''}
293
+ ${!isRscClientBundle ? importComponentsCode : ''}
266
294
  ${importRuntimeRouterCode}
267
- ${!isRscClient ? rootLayoutCode : ''}
295
+ ${!isRscClientBundle ? rootLayoutCode : ''}
268
296
  ${importLoadingCode}
269
297
  ${importErrorComponentsCode}
270
298
  ${importLoadersCode}