@modern-js/runtime 3.0.3 → 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.
@@ -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}
@@ -3,8 +3,9 @@ import { ElementsContext, createFromReadableStream } from "@modern-js/render/cli
3
3
  import { StaticRouterProvider, createBrowserRouter, createStaticRouter, redirect } from "@modern-js/runtime-utils/router";
4
4
  import react from "react";
5
5
  import { CSSLinks } from "./CSSLinks.mjs";
6
- const safeUse = (promise)=>{
7
- if ('function' == typeof react.use) return react.use(promise);
6
+ const safeUse = (value)=>{
7
+ const reactUse = react.use;
8
+ if ('function' == typeof reactUse) return reactUse(value);
8
9
  return null;
9
10
  };
10
11
  function collectCssFilesFromRoutes(matches, routes) {
@@ -22,6 +23,17 @@ function collectCssFilesFromRoutes(matches, routes) {
22
23
  }
23
24
  const createServerPayload = (routerContext, routes)=>{
24
25
  const cssFiles = collectCssFilesFromRoutes(routerContext.matches, routes);
26
+ let cssInjectionIndex = -1;
27
+ if (cssFiles.length > 0) {
28
+ for(let i = routerContext.matches.length - 1; i >= 0; i--){
29
+ const matchRoute = findRouteInTree(routes, routerContext.matches[i].route.id);
30
+ if (matchRoute && !matchRoute.isClientComponent) {
31
+ cssInjectionIndex = i;
32
+ break;
33
+ }
34
+ }
35
+ if (-1 === cssInjectionIndex) cssInjectionIndex = routerContext.matches.length - 1;
36
+ }
25
37
  return {
26
38
  type: 'render',
27
39
  actionData: routerContext.actionData,
@@ -29,45 +41,42 @@ const createServerPayload = (routerContext, routes)=>{
29
41
  loaderData: routerContext.loaderData,
30
42
  location: routerContext.location,
31
43
  routes: routerContext.matches.map((match, index, matches)=>{
32
- const element = match.route.element;
44
+ const route = match.route;
45
+ const element = route.element;
33
46
  const parentMatch = index > 0 ? matches[index - 1] : void 0;
34
47
  let processedElement;
35
48
  if (element) {
36
49
  const ElementComponent = element.type;
37
50
  const elementProps = {
38
- loaderData: routerContext?.loaderData?.[match.route.id],
39
- actionData: routerContext?.actionData?.[match.route.id],
51
+ loaderData: routerContext?.loaderData?.[route.id],
52
+ actionData: routerContext?.actionData?.[route.id],
40
53
  params: match.params,
41
- matches: routerContext.matches.map((m)=>{
42
- const { route, pathname, params } = m;
43
- return {
44
- id: route.id,
45
- pathname,
46
- params,
47
- data: routerContext?.loaderData?.[route.id],
48
- handle: route.handle
49
- };
50
- })
54
+ matches: routerContext.matches.map((m)=>({
55
+ id: m.route.id,
56
+ pathname: m.pathname,
57
+ params: m.params,
58
+ data: routerContext?.loaderData?.[m.route.id],
59
+ handle: m.route.handle
60
+ }))
51
61
  };
52
62
  const routeElement = /*#__PURE__*/ react.createElement(ElementComponent, elementProps);
53
- const isLeafRoute = index === routerContext.matches.length - 1;
54
- processedElement = isLeafRoute && cssFiles.length > 0 ? /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(CSSLinks, {
63
+ processedElement = index === cssInjectionIndex ? /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(CSSLinks, {
55
64
  cssFiles
56
65
  }), routeElement) : routeElement;
57
66
  }
58
67
  return {
59
68
  element: processedElement,
60
- errorElement: match.route.errorElement,
61
- handle: match.route.handle,
62
- hasAction: !!match.route.action,
63
- hasErrorBoundary: !!match.route.hasErrorBoundary,
64
- hasLoader: !!match.route.loader,
65
- hasClientLoader: !!match.route.hasClientLoader,
66
- id: match.route.id,
67
- index: match.route.index,
69
+ errorElement: route.errorElement,
70
+ handle: route.handle,
71
+ hasAction: !!route.action,
72
+ hasErrorBoundary: !!route.hasErrorBoundary,
73
+ hasLoader: !!route.loader,
74
+ hasClientLoader: !!route.hasClientLoader,
75
+ id: route.id,
76
+ index: route.index,
68
77
  params: match.params,
69
- parentId: parentMatch?.route.id || match.route.parentId,
70
- path: match.route.path,
78
+ parentId: parentMatch?.route.id || route.parentId,
79
+ path: route.path,
71
80
  pathname: match.pathname,
72
81
  pathnameBase: match.pathnameBase
73
82
  };
@@ -87,10 +96,11 @@ const handleRSCRedirect = (headers, basename, status)=>{
87
96
  });
88
97
  };
89
98
  const prepareRSCRoutes = async (routes)=>{
90
- const isLazyComponent = (component)=>component && 'object' == typeof component && void 0 !== component._init && void 0 !== component._payload;
99
+ const isLazyComponent = (component)=>null != component && 'object' == typeof component && '_init' in component && '_payload' in component;
91
100
  const processRoutes = async (routesList)=>{
92
101
  await Promise.all(routesList.map(async (route)=>{
93
- if ('lazyImport' in route && isLazyComponent(route.component)) route.component = (await route.lazyImport()).default;
102
+ const modernRoute = route;
103
+ if ('lazyImport' in modernRoute && isLazyComponent(modernRoute.component)) modernRoute.component = (await modernRoute.lazyImport()).default;
94
104
  if (route.children && Array.isArray(route.children)) await processRoutes(route.children);
95
105
  }));
96
106
  };
@@ -107,12 +117,14 @@ const mergeRoutes = (routes, originalRoutes)=>{
107
117
  };
108
118
  buildRoutesMap(routes);
109
119
  const mergeRoutesRecursive = (origRoutes)=>origRoutes.map((origRoute)=>{
110
- if (origRoute.id && routesMap.has(origRoute.id)) {
111
- const matchedRoute = routesMap.get(origRoute.id);
120
+ const modernOrig = origRoute;
121
+ if (modernOrig.id && routesMap.has(modernOrig.id)) {
122
+ const matchedRoute = routesMap.get(modernOrig.id);
112
123
  const result = {
113
- loader: origRoute.hasClientLoader ? origRoute.loader : void 0,
124
+ loader: modernOrig.hasClientLoader ? modernOrig.loader : void 0,
114
125
  ...matchedRoute
115
126
  };
127
+ if (modernOrig.isClientComponent) result.isClientComponent = true;
116
128
  if (origRoute.children && Array.isArray(origRoute.children)) result.children = mergeRoutesRecursive(origRoute.children);
117
129
  return result;
118
130
  }
@@ -130,6 +142,68 @@ const findRouteInTree = (routes, routeId)=>{
130
142
  }
131
143
  return null;
132
144
  };
145
+ function getChangedMatches(matches, currentMatches) {
146
+ const currentById = new Map();
147
+ for (const m of currentMatches)if (m.route?.id) currentById.set(m.route.id, m);
148
+ return matches.filter((match)=>{
149
+ const current = currentById.get(match.route?.id);
150
+ return !current || JSON.stringify(current.params) !== JSON.stringify(match.params);
151
+ });
152
+ }
153
+ function canSkipRscFetch(matches, routerState) {
154
+ const changedMatches = getChangedMatches(matches, routerState?.matches || []);
155
+ return changedMatches.length > 0 && changedMatches.every((m)=>{
156
+ const route = m.route;
157
+ return route.isClientComponent && !(route.hasLoader && !route.hasClientLoader);
158
+ });
159
+ }
160
+ function injectRouteCss(routeId) {
161
+ if ("u" < typeof window) return;
162
+ const cssAssets = window._MODERNJS_ROUTE_MANIFEST?.routeAssets?.[routeId]?.referenceCssAssets;
163
+ if (!cssAssets) return;
164
+ const publicPath = window.__webpack_public_path__ || '/';
165
+ for (const css of cssAssets){
166
+ const href = css.startsWith('http') || css.startsWith('/') ? css : publicPath + css;
167
+ if (!document.querySelector(`link[href="${CSS.escape(href)}"]`)) {
168
+ const link = document.createElement('link');
169
+ link.rel = 'stylesheet';
170
+ link.href = href;
171
+ document.head.appendChild(link);
172
+ }
173
+ }
174
+ }
175
+ function ensureClientComponent(route, originalRoutes) {
176
+ if (route.isClientComponent && !route.Component) {
177
+ const origRoute = findRouteInTree(originalRoutes, route.id);
178
+ if (origRoute?.Component) {
179
+ route.Component = origRoute.Component;
180
+ delete route.element;
181
+ }
182
+ }
183
+ }
184
+ function resolveClientLoaders(matches, originalRoutes) {
185
+ const clientMatches = matches.filter((m)=>m.route.hasClientLoader);
186
+ if (0 === clientMatches.length) return Promise.resolve([]);
187
+ return Promise.all(clientMatches.map(async (clientMatch)=>{
188
+ const origRoute = findRouteInTree(originalRoutes, clientMatch.route.id);
189
+ clientMatch.route.loader = origRoute?.loader;
190
+ const result = await clientMatch.resolve();
191
+ return {
192
+ routeId: clientMatch.route.id,
193
+ result
194
+ };
195
+ }));
196
+ }
197
+ function applyLoaderResults(results, loaderResults) {
198
+ for (const { routeId, result } of loaderResults)results[routeId] = result;
199
+ }
200
+ async function resolveClientOnlyNavigation(matches, results, originalRoutes, routerState) {
201
+ const changedMatches = getChangedMatches(matches, routerState?.matches || []);
202
+ for (const match of changedMatches)ensureClientComponent(match.route, originalRoutes);
203
+ applyLoaderResults(results, await resolveClientLoaders(changedMatches, originalRoutes));
204
+ for (const match of changedMatches)if (match.route.id) injectRouteCss(match.route.id);
205
+ return results;
206
+ }
133
207
  const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>{
134
208
  const processedRoutes = payload.routes.reduceRight((previous, route)=>{
135
209
  if (previous.length > 0) return [
@@ -146,42 +220,28 @@ const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>
146
220
  const router = createBrowserRouter(mergedRoutes, {
147
221
  hydrationData: payload,
148
222
  basename: basename,
149
- dataStrategy: async (context)=>{
150
- const { request, matches } = context;
223
+ dataStrategy: async ({ request, matches })=>{
151
224
  const results = {};
152
- const clientMatches = matches.filter((match)=>match.route.hasClientLoader);
225
+ if (canSkipRscFetch(matches, router.state)) return resolveClientOnlyNavigation(matches, results, originalRoutes, router.state);
153
226
  const fetchPromise = fetch(request.url, {
154
227
  headers: {
155
228
  'x-rsc-tree': 'true'
156
229
  }
157
230
  });
158
- const clientLoadersPromise = clientMatches.length > 0 ? Promise.all(clientMatches.map(async (clientMatch)=>{
159
- const foundRoute = findRouteInTree(originalRoutes, clientMatch.route.id);
160
- clientMatch.route.loader = foundRoute?.loader;
161
- const res = await clientMatch.resolve();
162
- return {
163
- routeId: clientMatch.route.id,
164
- result: res
165
- };
166
- })) : Promise.resolve([]);
231
+ const clientLoadersPromise = resolveClientLoaders(matches, originalRoutes);
167
232
  const res = await fetchPromise;
168
233
  const redirectLocation = res.headers.get('X-Modernjs-Redirect');
169
234
  if (redirectLocation) {
170
235
  matches.forEach((match)=>{
171
236
  const routeId = match.route.id;
172
237
  if (routeId) results[routeId] = {
173
- type: 'redirect',
238
+ type: 'data',
174
239
  result: redirect(redirectLocation)
175
240
  };
176
241
  });
177
242
  return results;
178
243
  }
179
- const [clientLoaderResults] = await Promise.all([
180
- clientLoadersPromise
181
- ]);
182
- clientLoaderResults.forEach(({ routeId, result })=>{
183
- results[routeId] = result;
184
- });
244
+ applyLoaderResults(results, await clientLoadersPromise);
185
245
  if (!res.body) throw new Error('Response body is null');
186
246
  const payload = await createFromReadableStream(res.body);
187
247
  if ('object' != typeof payload || null === payload || void 0 === payload.type || 'render' !== payload.type) throw new Error('Unexpected payload type');
@@ -189,10 +249,14 @@ const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>
189
249
  matches.forEach((match)=>{
190
250
  const routeId = match.route.id;
191
251
  const matchedRoute = serverPayload.routes.find((route)=>route.id === routeId);
192
- if (matchedRoute) router.patchRoutes(matchedRoute.parentId, [
193
- matchedRoute
194
- ], true);
195
- if (serverPayload.loaderData?.[routeId]) results[routeId] = {
252
+ if (matchedRoute) {
253
+ const modernMatch = match.route;
254
+ router.patchRoutes(matchedRoute.parentId ?? null, [
255
+ matchedRoute
256
+ ], true);
257
+ if (modernMatch.isClientComponent && modernMatch.Component) delete modernMatch.Component;
258
+ }
259
+ if (serverPayload.loaderData && routeId in serverPayload.loaderData) results[routeId] = {
196
260
  type: 'data',
197
261
  result: serverPayload.loaderData[routeId]
198
262
  };
@@ -67,7 +67,12 @@ function getRouteObjects(routes, { globalApp, ssrMode, props }) {
67
67
  ...'object' == typeof route.config ? route.config?.handle : {}
68
68
  },
69
69
  index: route.index,
70
+ hasLoader: !!route.loader,
70
71
  hasClientLoader: !!route.clientData,
72
+ hasAction: !!route.action,
73
+ ...route.isClientComponent ? {
74
+ isClientComponent: true
75
+ } : {},
71
76
  Component: route.component ? route.component : void 0,
72
77
  errorElement: route.error ? /*#__PURE__*/ jsx(route.error, {}) : void 0,
73
78
  children: route.children ? route.children.map((child)=>getRouteObjects([
@@ -64,7 +64,7 @@ const runtimePlugin = (params)=>({
64
64
  'process.env.IS_REACT18': process.env.IS_REACT18
65
65
  },
66
66
  include: [
67
- new RegExp(`[\\\\/]node_modules[\\\\/]@${metaName}[\\\\/]runtime[\\\\/].*[\\\\/]head\\.[jt]sx?$`)
67
+ new RegExp(`[\\\\/]node_modules[\\\\/]@${metaName}[\\\\/]runtime[\\\\/].*[\\\\/]head\\.`)
68
68
  ]
69
69
  },
70
70
  tools: {
@@ -19,7 +19,7 @@ const checkUseStringSSR = (config, appDirectory, entrypoints)=>{
19
19
  }
20
20
  return true;
21
21
  };
22
- const ssrBuilderPlugin = (modernAPI, outputModule)=>({
22
+ const ssrBuilderPlugin = (modernAPI, outputModule, exportLoadablePath)=>({
23
23
  name: '@modern-js/builder-plugin-ssr',
24
24
  setup (api) {
25
25
  api.modifyEnvironmentConfig((config, { name, mergeEnvironmentConfig })=>{
@@ -29,6 +29,7 @@ const ssrBuilderPlugin = (modernAPI, outputModule)=>({
29
29
  const appContext = modernAPI.getAppContext();
30
30
  const { appDirectory, entrypoints } = appContext;
31
31
  const useLoadablePlugin = isUseSSRBundle(userConfig) && !isServerEnvironment && checkUseStringSSR(userConfig, appDirectory, entrypoints);
32
+ const useLoadableComponents = isUseSSRBundle(userConfig) && checkUseStringSSR(userConfig, appDirectory, entrypoints);
32
33
  return mergeEnvironmentConfig(config, {
33
34
  source: {
34
35
  define: {
@@ -46,33 +47,8 @@ const ssrBuilderPlugin = (modernAPI, outputModule)=>({
46
47
  filename: LOADABLE_STATS_FILE
47
48
  }
48
49
  ]);
49
- } : void 0
50
- }
51
- });
52
- });
53
- }
54
- });
55
- const ssrPlugin = ()=>({
56
- name: '@modern-js/plugin-ssr',
57
- required: [
58
- '@modern-js/runtime'
59
- ],
60
- setup: (api)=>{
61
- const appContext = api.getAppContext();
62
- const exportLoadablePath = `@${appContext.metaName}/runtime/loadable`;
63
- const runtimeUtilsPath = require.resolve('@modern-js/runtime-utils/node');
64
- const aliasPath = runtimeUtilsPath.replace(`${path.sep}cjs${path.sep}`, `${path.sep}esm${path.sep}`).replace(/\.js$/, '.mjs');
65
- api.config(()=>({
66
- builderPlugins: [
67
- ssrBuilderPlugin(api, 'module' === appContext.moduleType)
68
- ],
69
- resolve: {
70
- alias: {
71
- '@modern-js/runtime-utils/node$': aliasPath
72
- }
73
- },
74
- tools: {
75
- swc: {
50
+ } : void 0,
51
+ swc: useLoadableComponents ? {
76
52
  jsc: {
77
53
  experimental: {
78
54
  plugins: [
@@ -102,6 +78,29 @@ const ssrPlugin = ()=>({
102
78
  ]
103
79
  }
104
80
  }
81
+ } : void 0
82
+ }
83
+ });
84
+ });
85
+ }
86
+ });
87
+ const ssrPlugin = ()=>({
88
+ name: '@modern-js/plugin-ssr',
89
+ required: [
90
+ '@modern-js/runtime'
91
+ ],
92
+ setup: (api)=>{
93
+ const appContext = api.getAppContext();
94
+ const exportLoadablePath = `@${appContext.metaName}/runtime/loadable`;
95
+ const runtimeUtilsPath = require.resolve('@modern-js/runtime-utils/node');
96
+ const aliasPath = runtimeUtilsPath.replace(`${path.sep}cjs${path.sep}`, `${path.sep}esm${path.sep}`).replace(/\.js$/, '.mjs');
97
+ api.config(()=>({
98
+ builderPlugins: [
99
+ ssrBuilderPlugin(api, 'module' === appContext.moduleType, exportLoadablePath)
100
+ ],
101
+ resolve: {
102
+ alias: {
103
+ '@modern-js/runtime-utils/node$': aliasPath
105
104
  }
106
105
  }
107
106
  }));
@@ -90,6 +90,10 @@ const configureChildCompiler = (child, compiler, appDirectory)=>{
90
90
  node: true
91
91
  };
92
92
  child.options.devtool = false;
93
+ if (child.options.optimization && 'object' == typeof child.options.optimization) child.options.optimization.minimize = false;
94
+ else child.options.optimization = {
95
+ minimize: false
96
+ };
93
97
  };
94
98
  const applyExternalsPlugin = (child, compiler)=>{
95
99
  const ExternalsPlugin = compiler.rspack?.ExternalsPlugin;
@@ -90,7 +90,9 @@ const generateCode = async (appContext, config, entrypoints, api)=>{
90
90
  entryName: entrypoint.entryName,
91
91
  internalDirectory,
92
92
  splitRouteChunks: config?.output?.splitRouteChunks,
93
- isRscClient: isUseRsc(config)
93
+ isRscClientBundle: isUseRsc(config),
94
+ srcDirectory,
95
+ internalSrcAlias: appContext.internalSrcAlias
94
96
  })
95
97
  });
96
98
  if (entrypoint.nestedRoutesEntry && (isUseSSRBundle(config) || isUseRsc(config))) {
@@ -110,7 +112,9 @@ const generateCode = async (appContext, config, entrypoints, api)=>{
110
112
  entryName: entrypoint.entryName,
111
113
  internalDirectory,
112
114
  splitRouteChunks: config?.output?.splitRouteChunks,
113
- isRscClient: false
115
+ isRscClientBundle: false,
116
+ srcDirectory,
117
+ internalSrcAlias: appContext.internalSrcAlias
114
118
  });
115
119
  await fs.outputFile(path.resolve(internalDirectory, `./${entryName}/routes.server.js`), serverRoutesCode, 'utf8');
116
120
  }
@@ -1,7 +1,8 @@
1
1
  import __rslib_shim_module__ from "node:module";
2
2
  const require = /*#__PURE__*/ __rslib_shim_module__.createRequire(/*#__PURE__*/ (()=>import.meta.url)());
3
+ import { open as promises_open } from "node:fs/promises";
3
4
  import path from "path";
4
- import { findExists, formatImportPath, fs, getEntryOptions, isSSGEntry, slash } from "@modern-js/utils";
5
+ import { JS_EXTENSIONS, findExists, formatImportPath, fs, getEntryOptions, isSSGEntry, slash } from "@modern-js/utils";
5
6
  import { ROUTE_MODULES } from "@modern-js/utils/universal/constants";
6
7
  import { APP_INIT_EXPORTED, TEMP_LOADERS_DIR } from "../constants.mjs";
7
8
  import { getPathWithoutExt, getServerLoadersFile, parseModule, replaceWithAlias } from "./utils.mjs";
@@ -72,7 +73,25 @@ const routesForServer = ({ routesForServerLoaderMatches })=>{
72
73
  `;
73
74
  };
74
75
  const createMatchReg = (keyword)=>new RegExp(`("${keyword}":\\s)"([^\n]+)"`, 'g');
75
- const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry, entryName, internalDirectory, splitRouteChunks = true, isRscClient = false })=>{
76
+ async function hasUseClientDirective(componentPath, srcDirectory, internalSrcAlias) {
77
+ let realPath = componentPath;
78
+ if (internalSrcAlias && srcDirectory && realPath.startsWith(internalSrcAlias)) realPath = path.join(srcDirectory, realPath.slice(internalSrcAlias.length));
79
+ const filePath = findExists(JS_EXTENSIONS.map((ext)=>`${realPath}${ext}`));
80
+ if (!filePath) return false;
81
+ let fh;
82
+ try {
83
+ fh = await promises_open(filePath, 'r');
84
+ const buf = Buffer.alloc(64);
85
+ const { bytesRead } = await fh.read(buf, 0, 64, 0);
86
+ const content = buf.toString('utf-8', 0, bytesRead).trimStart();
87
+ return content.startsWith("'use client'") || content.startsWith('"use client"');
88
+ } catch {
89
+ return false;
90
+ } finally{
91
+ await fh?.close();
92
+ }
93
+ }
94
+ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry, entryName, internalDirectory, splitRouteChunks = true, isRscClientBundle = false, srcDirectory, internalSrcAlias })=>{
76
95
  const components = [];
77
96
  const loadings = [];
78
97
  const errors = [];
@@ -97,9 +116,9 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
97
116
  const importOptions = webpackChunkName ? `/* webpackChunkName: "${routeId}" */ ` : eager ? '/* webpackMode: "eager" */ ' : '';
98
117
  return `() => import(${importOptions}'${componentPath}').then(routeModule => handleRouteModule(routeModule, "${routeId}")).catch(handleRouteModuleError)`;
99
118
  };
100
- const traverseRouteTree = (route, isRscClient)=>{
119
+ const traverseRouteTree = async (route, isRscClientBundle)=>{
101
120
  let children;
102
- if ('children' in route && route.children) children = route?.children?.map((child)=>traverseRouteTree(child, isRscClient));
121
+ if ('children' in route && route.children) children = await Promise.all(route.children.map((child)=>traverseRouteTree(child, isRscClientBundle)));
103
122
  let loading;
104
123
  let error;
105
124
  let loader;
@@ -171,6 +190,8 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
171
190
  components.push(route._component);
172
191
  component = `component_${components.length - 1}`;
173
192
  }
193
+ const isClientComponent = 'nested' === route.type && Boolean(route._component) && Boolean(srcDirectory) && Boolean(internalSrcAlias) && await hasUseClientDirective(route._component, srcDirectory, internalSrcAlias);
194
+ const shouldIncludeClientBundle = !isRscClientBundle || isClientComponent;
174
195
  const finalRoute = {
175
196
  ...route,
176
197
  loading,
@@ -178,10 +199,17 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
178
199
  action,
179
200
  config,
180
201
  error,
181
- children
202
+ children,
203
+ ...isClientComponent && {
204
+ isClientComponent: true
205
+ },
206
+ ...shouldIncludeClientBundle && {
207
+ lazyImport
208
+ },
209
+ ...shouldIncludeClientBundle && route._component && {
210
+ component
211
+ }
182
212
  };
183
- if (!isRscClient) finalRoute.lazyImport = lazyImport;
184
- if (route._component && !isRscClient) finalRoute.component = component;
185
213
  if ('nested' === route.type && route._component && (route.loader || route.data)) finalRoute.shouldRevalidate = `createShouldRevalidate("${route.id}")`;
186
214
  return finalRoute;
187
215
  };
@@ -189,7 +217,7 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
189
217
  export const routes = [
190
218
  `;
191
219
  for (const route of routes)if ('type' in route) {
192
- const newRoute = traverseRouteTree(route, isRscClient);
220
+ const newRoute = await traverseRouteTree(route, isRscClientBundle);
193
221
  const routeStr = JSON.stringify(newRoute, null, 2);
194
222
  const keywords = [
195
223
  'component',
@@ -264,9 +292,9 @@ const fileSystemRoutes = async ({ metaName, routes, ssrMode, nestedRoutesEntry,
264
292
  `;
265
293
  return `
266
294
  ${importLazyCode}
267
- ${!isRscClient ? importComponentsCode : ''}
295
+ ${!isRscClientBundle ? importComponentsCode : ''}
268
296
  ${importRuntimeRouterCode}
269
- ${!isRscClient ? rootLayoutCode : ''}
297
+ ${!isRscClientBundle ? rootLayoutCode : ''}
270
298
  ${importLoadingCode}
271
299
  ${importErrorComponentsCode}
272
300
  ${importLoadersCode}