@modern-js/server-core 3.10.1 → 3.10.3

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.
@@ -109,6 +109,67 @@ const createWebRequest = (req, res, body)=>{
109
109
  }
110
110
  return originalRequest;
111
111
  };
112
+ const noop = ()=>{};
113
+ const isDestroyed = (res)=>'stream' in res ? res.stream.destroyed : res.destroyed;
114
+ const createAbortError = ()=>Object.assign(new Error('The operation was aborted'), {
115
+ name: 'AbortError',
116
+ code: 'ABORT_ERR'
117
+ });
118
+ const writeBody = async (body, res)=>{
119
+ const reader = body.getReader();
120
+ let abortError;
121
+ let cancelled;
122
+ let resume;
123
+ let waitsForDrain = false;
124
+ const onDrain = ()=>resume?.();
125
+ const abort = (error)=>{
126
+ if (cancelled) return;
127
+ abortError = error;
128
+ cancelled = reader.cancel(error);
129
+ cancelled.catch(noop);
130
+ resume?.();
131
+ };
132
+ const onClose = ()=>abort(createAbortError());
133
+ res.on('close', onClose);
134
+ res.on('error', abort);
135
+ try {
136
+ if (isDestroyed(res)) onClose();
137
+ while(!cancelled){
138
+ let result;
139
+ try {
140
+ result = await reader.read();
141
+ } catch (error) {
142
+ res.destroy(error);
143
+ throw error;
144
+ }
145
+ if (result.done || cancelled) break;
146
+ try {
147
+ if (!res.write(result.value) && !cancelled) {
148
+ if (!waitsForDrain) {
149
+ waitsForDrain = true;
150
+ res.on('drain', onDrain);
151
+ }
152
+ await new Promise((resolve)=>{
153
+ resume = resolve;
154
+ });
155
+ resume = void 0;
156
+ }
157
+ } catch (error) {
158
+ abort(error);
159
+ }
160
+ }
161
+ if (cancelled) {
162
+ await cancelled;
163
+ throw abortError;
164
+ }
165
+ res.end();
166
+ } finally{
167
+ res.off('close', onClose);
168
+ res.off('error', abort);
169
+ if (waitsForDrain) res.off('drain', onDrain);
170
+ reader.releaseLock();
171
+ }
172
+ };
112
173
  const sendResponse = async (response, res)=>{
113
174
  res.statusMessage = response.statusText;
114
175
  res.statusCode = response.status;
@@ -117,10 +178,8 @@ const sendResponse = async (response, res)=>{
117
178
  else res.setHeader(key, value);
118
179
  if (cookies.length > 0) res.setHeader('set-cookie', cookies);
119
180
  if (response.headers.get('Content-Type')?.match(/text\/event-stream/i) && res instanceof external_node_http_namespaceObject.ServerResponse) res.flushHeaders();
120
- if (response.body) {
121
- const writable = external_node_stream_namespaceObject.Writable.toWeb(res);
122
- await response.body.pipeTo(writable);
123
- } else res.end();
181
+ if (response.body) await writeBody(response.body, res);
182
+ else res.end();
124
183
  };
125
184
  const handleResponseError = (e, res)=>{
126
185
  const err = e instanceof Error ? e : new Error('unknown error', {
@@ -69,8 +69,9 @@ const serverStaticPlugin = ()=>({
69
69
  }
70
70
  });
71
71
  function createPublicMiddleware({ pwd, routes }) {
72
+ const publicRoutes = routes.filter((route)=>!route.isSSR && route.entryPath?.startsWith('public')).sort(index_js_namespaceObject.sortRoutes);
72
73
  return async (c, next)=>{
73
- const route = matchPublicRoute(c.req, routes);
74
+ const route = matchPublicRoute(c.req, publicRoutes);
74
75
  if (route) {
75
76
  const { entryPath } = route;
76
77
  const filename = external_path_default().join(pwd, entryPath);
@@ -88,8 +89,8 @@ function createPublicMiddleware({ pwd, routes }) {
88
89
  return await next();
89
90
  };
90
91
  }
91
- function matchPublicRoute(req, routes) {
92
- for (const route of routes.sort(index_js_namespaceObject.sortRoutes))if (!route.isSSR && route.entryPath.startsWith('public') && req.path.startsWith(route.urlPath)) return route;
92
+ function matchPublicRoute(req, publicRoutes) {
93
+ return publicRoutes.find((route)=>req.path.startsWith(route.urlPath));
93
94
  }
94
95
  const isPathInside = (target, root)=>{
95
96
  const relative = external_path_default().relative(external_path_default().resolve(root), external_path_default().resolve(target));
@@ -134,26 +135,26 @@ function createStaticMiddleware(options) {
134
135
  ...staticReg,
135
136
  ...iconReg
136
137
  ].join('|')})`);
138
+ const publicMiddleware = createPublicMiddleware({
139
+ pwd,
140
+ routes: routes || []
141
+ });
137
142
  return async (c, next)=>{
138
143
  const pageRoute = c.get('route');
139
144
  const pathname = c.req.path;
140
145
  if (pageRoute && '' === external_path_default().extname(pathname)) return next();
141
146
  const hit = staticPathRegExp.test(pathname);
142
- if (!hit) return createPublicMiddleware({
143
- pwd,
144
- routes: routes || []
145
- })(c, next);
147
+ if (!hit) return publicMiddleware(c, next);
146
148
  else {
147
149
  const filepath = external_path_default().join(pwd, pathname.replace(pathPrefix, ()=>''));
148
150
  if (!isPathInside(filepath, pwd)) return next();
149
- if (!await utils_namespaceObject.fs.pathExists(filepath)) return next();
150
- const mimeType = (0, mime_namespaceObject.getMimeType)(filepath);
151
- if (mimeType) c.header('Content-Type', mimeType);
152
- const stat = await utils_namespaceObject.fs.lstat(filepath);
153
- const { size } = stat;
151
+ const stat = await utils_namespaceObject.fs.stat(filepath).catch(()=>null);
152
+ if (!stat?.isFile()) return next();
154
153
  const chunk = await fileReader_namespaceObject.fileReader.readFileFromSystem(filepath, 'buffer');
155
- c.header('Content-Length', String(size));
156
154
  if (null === chunk) return next();
155
+ const mimeType = (0, mime_namespaceObject.getMimeType)(filepath);
156
+ if (mimeType) c.header('Content-Type', mimeType);
157
+ c.header('Content-Length', String(chunk.byteLength));
157
158
  const body = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
158
159
  return c.body(body, 200);
159
160
  }
@@ -158,13 +158,10 @@ const injectServerTiming = ()=>({
158
158
  const monitors = c.get('monitors');
159
159
  monitors?.push(timingMonitor);
160
160
  await next();
161
- serverTimings.forEach((serverTiming)=>{
162
- const { name, desc, dur } = serverTiming;
163
- const _name = `bd-${metaName}-${name}`;
164
- const value = `${_name};${desc ? `decs="${desc}";` : ''} dur=${dur}`;
165
- c.header(external_constants_js_namespaceObject.SERVER_TIMING, value, {
166
- append: true
167
- });
161
+ if (0 === serverTimings.length) return;
162
+ const value = serverTimings.map(({ name, desc, dur })=>`bd-${metaName}-${name};${desc ? `decs="${desc}";` : ''} dur=${dur}`).join(', ');
163
+ c.header(external_constants_js_namespaceObject.SERVER_TIMING, value, {
164
+ append: true
168
165
  });
169
166
  }
170
167
  });
@@ -70,10 +70,6 @@ var __webpack_exports__ = {};
70
70
  "default"
71
71
  ].indexOf(__rspack_import_key) < 0) __rspack_reexport[__rspack_import_key] = ()=>_inject__rspack_import_3[__rspack_import_key];
72
72
  __webpack_require__.d(__webpack_exports__, __rspack_reexport);
73
- const DYNAMIC_ROUTE_REG = /\/:./;
74
- function escapeRegexSpecialChars(path) {
75
- return path.replace(/[()[\]{}*+?.^$|\\]/g, '\\$&');
76
- }
77
73
  const renderPlugin = ()=>({
78
74
  name: '@modern-js/plugin-render',
79
75
  setup (api) {
@@ -87,9 +83,7 @@ var __webpack_exports__ = {};
87
83
  });
88
84
  for (const route of pageRoutes){
89
85
  const { urlPath: originUrlPath } = route;
90
- const isDynamic = DYNAMIC_ROUTE_REG.test(originUrlPath);
91
- const escapedPath = isDynamic ? originUrlPath : escapeRegexSpecialChars(originUrlPath);
92
- const urlPath = escapedPath.endsWith('/') ? `${escapedPath}*` : `${escapedPath}/*`;
86
+ const urlPath = originUrlPath.endsWith('/') ? `${originUrlPath}*` : `${originUrlPath}/*`;
93
87
  renderMiddlewares?.forEach((m)=>{
94
88
  middlewares.push({
95
89
  name: m.name,
@@ -48,6 +48,7 @@ var __webpack_exports__ = {};
48
48
  });
49
49
  const universal_namespaceObject = require("@modern-js/utils/universal");
50
50
  const trie_router_namespaceObject = require("hono/router/trie-router");
51
+ const url_namespaceObject = require("hono/utils/url");
51
52
  const external_constants_js_namespaceObject = require("../../constants.js");
52
53
  const index_js_namespaceObject = require("../../utils/index.js");
53
54
  const external_csrRscRender_js_namespaceObject = require("./csrRscRender.js");
@@ -109,11 +110,11 @@ var __webpack_exports__ = {};
109
110
  }
110
111
  async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config }) {
111
112
  const router = getRouter(routes);
113
+ const framework = (0, universal_namespaceObject.cutNameByHyphen)(metaName || 'modern-js');
114
+ const fallbackHeader = `x-${framework}-ssr-fallback`;
112
115
  return async (req, { monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext, contextForceCSR, reporter })=>{
113
- const forMatchpathname = matchPathname ?? (0, index_js_namespaceObject.getPathname)(req);
116
+ const forMatchpathname = matchPathname ?? (0, url_namespaceObject.getPath)(req);
114
117
  const [routeInfo, params] = matchRoute(router, forMatchpathname, matchEntryName);
115
- const framework = (0, universal_namespaceObject.cutNameByHyphen)(metaName || 'modern-js');
116
- const fallbackHeader = `x-${framework}-ssr-fallback`;
117
118
  let fallbackReason = null;
118
119
  const fallbackWrapper = (reason)=>{
119
120
  fallbackReason = reason;
@@ -139,9 +140,8 @@ var __webpack_exports__ = {};
139
140
  });
140
141
  const finalForceCSR = routeInfo.entryName ? forceCSRMap?.get(routeInfo.entryName) ?? forceCSR : forceCSR;
141
142
  const renderMode = await getRenderMode(req, fallbackHeader, routeInfo.isSSR, finalForceCSR, nodeReq, fallbackWrapper, contextForceCSR);
142
- const headerData = (0, index_js_namespaceObject.parseHeaders)(req);
143
143
  const onError = (e, key)=>{
144
- monitors?.error(`SSR Error - ${key || (e instanceof Error ? e.name : e)}, error = %s, req.url = %s, req.headers = %o`, e instanceof Error ? e.stack || e.message : e, forMatchpathname, getHeadersWithoutCookie(headerData));
144
+ monitors?.error(`SSR Error - ${key || (e instanceof Error ? e.name : e)}, error = %s, req.url = %s, req.headers = %o`, e instanceof Error ? e.stack || e.message : e, forMatchpathname, getHeadersWithoutCookie((0, index_js_namespaceObject.parseHeaders)(req)));
145
145
  };
146
146
  const onTiming = (name, dur)=>{
147
147
  monitors?.timing(name, dur, 'SSR');
@@ -137,6 +137,15 @@ function shouldUseCache(request) {
137
137
  const hasLoaderQuery = url.searchParams.has('__loader');
138
138
  return !(hasRSCAction || hasRSCTree || hasLoaderQuery);
139
139
  }
140
+ const cacheKeyRegExps = new Map();
141
+ function getCacheKeyRegExp(key) {
142
+ let regExp = cacheKeyRegExps.get(key);
143
+ if (!regExp) {
144
+ regExp = new RegExp(key);
145
+ cacheKeyRegExps.set(key, regExp);
146
+ }
147
+ return regExp;
148
+ }
140
149
  function matchCacheControl(cacheOption, req) {
141
150
  if (!cacheOption || !req) return;
142
151
  else {
@@ -144,7 +153,7 @@ function matchCacheControl(cacheOption, req) {
144
153
  if (isCacheOptionProvider(cacheOption)) return cacheOption(req);
145
154
  const url = req.url;
146
155
  const options = Object.entries(cacheOption);
147
- for (const [key, option] of options)if ('*' === key || new RegExp(key).test(url)) if ('function' == typeof option) return option(req);
156
+ for (const [key, option] of options)if ('*' === key || getCacheKeyRegExp(key).test(url)) if ('function' == typeof option) return option(req);
148
157
  else return option;
149
158
  return;
150
159
  }
@@ -40,10 +40,6 @@ async function ssrRender(request, { routeInfo, html, config: userConfig, staticG
40
40
  const { entryName } = routeInfo;
41
41
  const loadableStats = serverManifest.loadableStats || {};
42
42
  const routeManifest = serverManifest.routeManifest || {};
43
- const headers = (0, index_js_namespaceObject.parseHeaders)(request);
44
- if (nodeReq) {
45
- for(const key in nodeReq.headers)if (!headers[key]) headers[key] = nodeReq.headers[key];
46
- }
47
43
  const renderBundle = serverManifest.renderBundles?.[entryName || constants_namespaceObject.MAIN_ENTRY_NAME];
48
44
  if (!renderBundle) throw new Error(`Can't find renderBundle ${entryName || constants_namespaceObject.MAIN_ENTRY_NAME}`);
49
45
  const requestHandler = await renderBundle[SERVER_RUNTIME_ENTRY];
@@ -1,5 +1,5 @@
1
1
  import { ServerResponse } from "node:http";
2
- import { Readable, Writable } from "node:stream";
2
+ import { Readable } from "node:stream";
3
3
  import cloneable_readable from "cloneable-readable";
4
4
  import { isResFinalized } from "./helper/index.mjs";
5
5
  const createWebRequest = (req, res, body)=>{
@@ -65,6 +65,67 @@ const createWebRequest = (req, res, body)=>{
65
65
  }
66
66
  return originalRequest;
67
67
  };
68
+ const noop = ()=>{};
69
+ const isDestroyed = (res)=>'stream' in res ? res.stream.destroyed : res.destroyed;
70
+ const createAbortError = ()=>Object.assign(new Error('The operation was aborted'), {
71
+ name: 'AbortError',
72
+ code: 'ABORT_ERR'
73
+ });
74
+ const writeBody = async (body, res)=>{
75
+ const reader = body.getReader();
76
+ let abortError;
77
+ let cancelled;
78
+ let resume;
79
+ let waitsForDrain = false;
80
+ const onDrain = ()=>resume?.();
81
+ const abort = (error)=>{
82
+ if (cancelled) return;
83
+ abortError = error;
84
+ cancelled = reader.cancel(error);
85
+ cancelled.catch(noop);
86
+ resume?.();
87
+ };
88
+ const onClose = ()=>abort(createAbortError());
89
+ res.on('close', onClose);
90
+ res.on('error', abort);
91
+ try {
92
+ if (isDestroyed(res)) onClose();
93
+ while(!cancelled){
94
+ let result;
95
+ try {
96
+ result = await reader.read();
97
+ } catch (error) {
98
+ res.destroy(error);
99
+ throw error;
100
+ }
101
+ if (result.done || cancelled) break;
102
+ try {
103
+ if (!res.write(result.value) && !cancelled) {
104
+ if (!waitsForDrain) {
105
+ waitsForDrain = true;
106
+ res.on('drain', onDrain);
107
+ }
108
+ await new Promise((resolve)=>{
109
+ resume = resolve;
110
+ });
111
+ resume = void 0;
112
+ }
113
+ } catch (error) {
114
+ abort(error);
115
+ }
116
+ }
117
+ if (cancelled) {
118
+ await cancelled;
119
+ throw abortError;
120
+ }
121
+ res.end();
122
+ } finally{
123
+ res.off('close', onClose);
124
+ res.off('error', abort);
125
+ if (waitsForDrain) res.off('drain', onDrain);
126
+ reader.releaseLock();
127
+ }
128
+ };
68
129
  const sendResponse = async (response, res)=>{
69
130
  res.statusMessage = response.statusText;
70
131
  res.statusCode = response.status;
@@ -73,10 +134,8 @@ const sendResponse = async (response, res)=>{
73
134
  else res.setHeader(key, value);
74
135
  if (cookies.length > 0) res.setHeader('set-cookie', cookies);
75
136
  if (response.headers.get('Content-Type')?.match(/text\/event-stream/i) && res instanceof ServerResponse) res.flushHeaders();
76
- if (response.body) {
77
- const writable = Writable.toWeb(res);
78
- await response.body.pipeTo(writable);
79
- } else res.end();
137
+ if (response.body) await writeBody(response.body, res);
138
+ else res.end();
80
139
  };
81
140
  const handleResponseError = (e, res)=>{
82
141
  const err = e instanceof Error ? e : new Error('unknown error', {
@@ -25,8 +25,9 @@ const serverStaticPlugin = ()=>({
25
25
  }
26
26
  });
27
27
  function createPublicMiddleware({ pwd, routes }) {
28
+ const publicRoutes = routes.filter((route)=>!route.isSSR && route.entryPath?.startsWith('public')).sort(sortRoutes);
28
29
  return async (c, next)=>{
29
- const route = matchPublicRoute(c.req, routes);
30
+ const route = matchPublicRoute(c.req, publicRoutes);
30
31
  if (route) {
31
32
  const { entryPath } = route;
32
33
  const filename = path.join(pwd, entryPath);
@@ -44,8 +45,8 @@ function createPublicMiddleware({ pwd, routes }) {
44
45
  return await next();
45
46
  };
46
47
  }
47
- function matchPublicRoute(req, routes) {
48
- for (const route of routes.sort(sortRoutes))if (!route.isSSR && route.entryPath.startsWith('public') && req.path.startsWith(route.urlPath)) return route;
48
+ function matchPublicRoute(req, publicRoutes) {
49
+ return publicRoutes.find((route)=>req.path.startsWith(route.urlPath));
49
50
  }
50
51
  const isPathInside = (target, root)=>{
51
52
  const relative = path.relative(path.resolve(root), path.resolve(target));
@@ -90,26 +91,26 @@ function createStaticMiddleware(options) {
90
91
  ...staticReg,
91
92
  ...iconReg
92
93
  ].join('|')})`);
94
+ const publicMiddleware = createPublicMiddleware({
95
+ pwd,
96
+ routes: routes || []
97
+ });
93
98
  return async (c, next)=>{
94
99
  const pageRoute = c.get('route');
95
100
  const pathname = c.req.path;
96
101
  if (pageRoute && '' === path.extname(pathname)) return next();
97
102
  const hit = staticPathRegExp.test(pathname);
98
- if (!hit) return createPublicMiddleware({
99
- pwd,
100
- routes: routes || []
101
- })(c, next);
103
+ if (!hit) return publicMiddleware(c, next);
102
104
  else {
103
105
  const filepath = path.join(pwd, pathname.replace(pathPrefix, ()=>''));
104
106
  if (!isPathInside(filepath, pwd)) return next();
105
- if (!await fs.pathExists(filepath)) return next();
106
- const mimeType = getMimeType(filepath);
107
- if (mimeType) c.header('Content-Type', mimeType);
108
- const stat = await fs.lstat(filepath);
109
- const { size } = stat;
107
+ const stat = await fs.stat(filepath).catch(()=>null);
108
+ if (!stat?.isFile()) return next();
110
109
  const chunk = await fileReader.readFileFromSystem(filepath, 'buffer');
111
- c.header('Content-Length', String(size));
112
110
  if (null === chunk) return next();
111
+ const mimeType = getMimeType(filepath);
112
+ if (mimeType) c.header('Content-Type', mimeType);
113
+ c.header('Content-Length', String(chunk.byteLength));
113
114
  const body = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
114
115
  return c.body(body, 200);
115
116
  }
@@ -123,13 +123,10 @@ const injectServerTiming = ()=>({
123
123
  const monitors = c.get('monitors');
124
124
  monitors?.push(timingMonitor);
125
125
  await next();
126
- serverTimings.forEach((serverTiming)=>{
127
- const { name, desc, dur } = serverTiming;
128
- const _name = `bd-${metaName}-${name}`;
129
- const value = `${_name};${desc ? `decs="${desc}";` : ''} dur=${dur}`;
130
- c.header(SERVER_TIMING, value, {
131
- append: true
132
- });
126
+ if (0 === serverTimings.length) return;
127
+ const value = serverTimings.map(({ name, desc, dur })=>`bd-${metaName}-${name};${desc ? `decs="${desc}";` : ''} dur=${dur}`).join(', ');
128
+ c.header(SERVER_TIMING, value, {
129
+ append: true
133
130
  });
134
131
  }
135
132
  });
@@ -2,10 +2,6 @@ import { getLoaderCtx } from "../../helper.mjs";
2
2
  import { sortRoutes } from "../../utils/index.mjs";
3
3
  import { requestLatencyMiddleware } from "../monitors.mjs";
4
4
  export * from "./inject.mjs";
5
- const DYNAMIC_ROUTE_REG = /\/:./;
6
- function escapeRegexSpecialChars(path) {
7
- return path.replace(/[()[\]{}*+?.^$|\\]/g, '\\$&');
8
- }
9
5
  const renderPlugin = ()=>({
10
6
  name: '@modern-js/plugin-render',
11
7
  setup (api) {
@@ -19,9 +15,7 @@ const renderPlugin = ()=>({
19
15
  });
20
16
  for (const route of pageRoutes){
21
17
  const { urlPath: originUrlPath } = route;
22
- const isDynamic = DYNAMIC_ROUTE_REG.test(originUrlPath);
23
- const escapedPath = isDynamic ? originUrlPath : escapeRegexSpecialChars(originUrlPath);
24
- const urlPath = escapedPath.endsWith('/') ? `${escapedPath}*` : `${escapedPath}/*`;
18
+ const urlPath = originUrlPath.endsWith('/') ? `${originUrlPath}*` : `${originUrlPath}/*`;
25
19
  renderMiddlewares?.forEach((m)=>{
26
20
  middlewares.push({
27
21
  name: m.name,
@@ -1,7 +1,8 @@
1
1
  import { cutNameByHyphen } from "@modern-js/utils/universal";
2
2
  import { TrieRouter } from "hono/router/trie-router";
3
+ import { getPath } from "hono/utils/url";
3
4
  import { X_MODERNJS_RENDER } from "../../constants.mjs";
4
- import { ErrorDigest, createErrorHtml, getPathname, getRuntimeEnv, parseHeaders, parseQuery, sortRoutes, uniqueKeyByRoute } from "../../utils/index.mjs";
5
+ import { ErrorDigest, createErrorHtml, getRuntimeEnv, parseHeaders, parseQuery, sortRoutes, uniqueKeyByRoute } from "../../utils/index.mjs";
5
6
  import { csrRscRender } from "./csrRscRender.mjs";
6
7
  import { dataHandler } from "./dataHandler.mjs";
7
8
  import { renderRscHandler } from "./renderRscHandler.mjs";
@@ -61,11 +62,11 @@ function getHeadersWithoutCookie(headers) {
61
62
  }
62
63
  async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config }) {
63
64
  const router = getRouter(routes);
65
+ const framework = cutNameByHyphen(metaName || 'modern-js');
66
+ const fallbackHeader = `x-${framework}-ssr-fallback`;
64
67
  return async (req, { monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext, contextForceCSR, reporter })=>{
65
- const forMatchpathname = matchPathname ?? getPathname(req);
68
+ const forMatchpathname = matchPathname ?? getPath(req);
66
69
  const [routeInfo, params] = matchRoute(router, forMatchpathname, matchEntryName);
67
- const framework = cutNameByHyphen(metaName || 'modern-js');
68
- const fallbackHeader = `x-${framework}-ssr-fallback`;
69
70
  let fallbackReason = null;
70
71
  const fallbackWrapper = (reason)=>{
71
72
  fallbackReason = reason;
@@ -91,9 +92,8 @@ async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig
91
92
  });
92
93
  const finalForceCSR = routeInfo.entryName ? forceCSRMap?.get(routeInfo.entryName) ?? forceCSR : forceCSR;
93
94
  const renderMode = await getRenderMode(req, fallbackHeader, routeInfo.isSSR, finalForceCSR, nodeReq, fallbackWrapper, contextForceCSR);
94
- const headerData = parseHeaders(req);
95
95
  const onError = (e, key)=>{
96
- monitors?.error(`SSR Error - ${key || (e instanceof Error ? e.name : e)}, error = %s, req.url = %s, req.headers = %o`, e instanceof Error ? e.stack || e.message : e, forMatchpathname, getHeadersWithoutCookie(headerData));
96
+ monitors?.error(`SSR Error - ${key || (e instanceof Error ? e.name : e)}, error = %s, req.url = %s, req.headers = %o`, e instanceof Error ? e.stack || e.message : e, forMatchpathname, getHeadersWithoutCookie(parseHeaders(req)));
97
97
  };
98
98
  const onTiming = (name, dur)=>{
99
99
  monitors?.timing(name, dur, 'SSR');
@@ -103,6 +103,15 @@ function shouldUseCache(request) {
103
103
  const hasLoaderQuery = url.searchParams.has('__loader');
104
104
  return !(hasRSCAction || hasRSCTree || hasLoaderQuery);
105
105
  }
106
+ const cacheKeyRegExps = new Map();
107
+ function getCacheKeyRegExp(key) {
108
+ let regExp = cacheKeyRegExps.get(key);
109
+ if (!regExp) {
110
+ regExp = new RegExp(key);
111
+ cacheKeyRegExps.set(key, regExp);
112
+ }
113
+ return regExp;
114
+ }
106
115
  function matchCacheControl(cacheOption, req) {
107
116
  if (!cacheOption || !req) return;
108
117
  else {
@@ -110,7 +119,7 @@ function matchCacheControl(cacheOption, req) {
110
119
  if (isCacheOptionProvider(cacheOption)) return cacheOption(req);
111
120
  const url = req.url;
112
121
  const options = Object.entries(cacheOption);
113
- for (const [key, option] of options)if ('*' === key || new RegExp(key).test(url)) if ('function' == typeof option) return option(req);
122
+ for (const [key, option] of options)if ('*' === key || getCacheKeyRegExp(key).test(url)) if ('function' == typeof option) return option(req);
114
123
  else return option;
115
124
  return;
116
125
  }
@@ -1,6 +1,6 @@
1
1
  import { MAIN_ENTRY_NAME } from "@modern-js/utils/universal/constants";
2
2
  import { X_MODERNJS_RENDER } from "../../constants.mjs";
3
- import { getPathname, parseHeaders } from "../../utils/index.mjs";
3
+ import { getPathname } from "../../utils/index.mjs";
4
4
  import { getCacheResult, matchCacheControl, shouldUseCache } from "./ssrCache.mjs";
5
5
  import { createRequestHandlerConfig } from "./utils.mjs";
6
6
  const SERVER_RUNTIME_ENTRY = 'requestHandler';
@@ -8,10 +8,6 @@ async function ssrRender(request, { routeInfo, html, config: userConfig, staticG
8
8
  const { entryName } = routeInfo;
9
9
  const loadableStats = serverManifest.loadableStats || {};
10
10
  const routeManifest = serverManifest.routeManifest || {};
11
- const headers = parseHeaders(request);
12
- if (nodeReq) {
13
- for(const key in nodeReq.headers)if (!headers[key]) headers[key] = nodeReq.headers[key];
14
- }
15
11
  const renderBundle = serverManifest.renderBundles?.[entryName || MAIN_ENTRY_NAME];
16
12
  if (!renderBundle) throw new Error(`Can't find renderBundle ${entryName || MAIN_ENTRY_NAME}`);
17
13
  const requestHandler = await renderBundle[SERVER_RUNTIME_ENTRY];
@@ -1,6 +1,6 @@
1
1
  import "node:module";
2
2
  import { ServerResponse } from "node:http";
3
- import { Readable, Writable } from "node:stream";
3
+ import { Readable } from "node:stream";
4
4
  import cloneable_readable from "cloneable-readable";
5
5
  import { isResFinalized } from "./helper/index.mjs";
6
6
  const createWebRequest = (req, res, body)=>{
@@ -66,6 +66,67 @@ const createWebRequest = (req, res, body)=>{
66
66
  }
67
67
  return originalRequest;
68
68
  };
69
+ const noop = ()=>{};
70
+ const isDestroyed = (res)=>'stream' in res ? res.stream.destroyed : res.destroyed;
71
+ const createAbortError = ()=>Object.assign(new Error('The operation was aborted'), {
72
+ name: 'AbortError',
73
+ code: 'ABORT_ERR'
74
+ });
75
+ const writeBody = async (body, res)=>{
76
+ const reader = body.getReader();
77
+ let abortError;
78
+ let cancelled;
79
+ let resume;
80
+ let waitsForDrain = false;
81
+ const onDrain = ()=>resume?.();
82
+ const abort = (error)=>{
83
+ if (cancelled) return;
84
+ abortError = error;
85
+ cancelled = reader.cancel(error);
86
+ cancelled.catch(noop);
87
+ resume?.();
88
+ };
89
+ const onClose = ()=>abort(createAbortError());
90
+ res.on('close', onClose);
91
+ res.on('error', abort);
92
+ try {
93
+ if (isDestroyed(res)) onClose();
94
+ while(!cancelled){
95
+ let result;
96
+ try {
97
+ result = await reader.read();
98
+ } catch (error) {
99
+ res.destroy(error);
100
+ throw error;
101
+ }
102
+ if (result.done || cancelled) break;
103
+ try {
104
+ if (!res.write(result.value) && !cancelled) {
105
+ if (!waitsForDrain) {
106
+ waitsForDrain = true;
107
+ res.on('drain', onDrain);
108
+ }
109
+ await new Promise((resolve)=>{
110
+ resume = resolve;
111
+ });
112
+ resume = void 0;
113
+ }
114
+ } catch (error) {
115
+ abort(error);
116
+ }
117
+ }
118
+ if (cancelled) {
119
+ await cancelled;
120
+ throw abortError;
121
+ }
122
+ res.end();
123
+ } finally{
124
+ res.off('close', onClose);
125
+ res.off('error', abort);
126
+ if (waitsForDrain) res.off('drain', onDrain);
127
+ reader.releaseLock();
128
+ }
129
+ };
69
130
  const sendResponse = async (response, res)=>{
70
131
  res.statusMessage = response.statusText;
71
132
  res.statusCode = response.status;
@@ -74,10 +135,8 @@ const sendResponse = async (response, res)=>{
74
135
  else res.setHeader(key, value);
75
136
  if (cookies.length > 0) res.setHeader('set-cookie', cookies);
76
137
  if (response.headers.get('Content-Type')?.match(/text\/event-stream/i) && res instanceof ServerResponse) res.flushHeaders();
77
- if (response.body) {
78
- const writable = Writable.toWeb(res);
79
- await response.body.pipeTo(writable);
80
- } else res.end();
138
+ if (response.body) await writeBody(response.body, res);
139
+ else res.end();
81
140
  };
82
141
  const handleResponseError = (e, res)=>{
83
142
  const err = e instanceof Error ? e : new Error('unknown error', {
@@ -26,8 +26,9 @@ const serverStaticPlugin = ()=>({
26
26
  }
27
27
  });
28
28
  function createPublicMiddleware({ pwd, routes }) {
29
+ const publicRoutes = routes.filter((route)=>!route.isSSR && route.entryPath?.startsWith('public')).sort(sortRoutes);
29
30
  return async (c, next)=>{
30
- const route = matchPublicRoute(c.req, routes);
31
+ const route = matchPublicRoute(c.req, publicRoutes);
31
32
  if (route) {
32
33
  const { entryPath } = route;
33
34
  const filename = path.join(pwd, entryPath);
@@ -45,8 +46,8 @@ function createPublicMiddleware({ pwd, routes }) {
45
46
  return await next();
46
47
  };
47
48
  }
48
- function matchPublicRoute(req, routes) {
49
- for (const route of routes.sort(sortRoutes))if (!route.isSSR && route.entryPath.startsWith('public') && req.path.startsWith(route.urlPath)) return route;
49
+ function matchPublicRoute(req, publicRoutes) {
50
+ return publicRoutes.find((route)=>req.path.startsWith(route.urlPath));
50
51
  }
51
52
  const isPathInside = (target, root)=>{
52
53
  const relative = path.relative(path.resolve(root), path.resolve(target));
@@ -91,26 +92,26 @@ function createStaticMiddleware(options) {
91
92
  ...staticReg,
92
93
  ...iconReg
93
94
  ].join('|')})`);
95
+ const publicMiddleware = createPublicMiddleware({
96
+ pwd,
97
+ routes: routes || []
98
+ });
94
99
  return async (c, next)=>{
95
100
  const pageRoute = c.get('route');
96
101
  const pathname = c.req.path;
97
102
  if (pageRoute && '' === path.extname(pathname)) return next();
98
103
  const hit = staticPathRegExp.test(pathname);
99
- if (!hit) return createPublicMiddleware({
100
- pwd,
101
- routes: routes || []
102
- })(c, next);
104
+ if (!hit) return publicMiddleware(c, next);
103
105
  else {
104
106
  const filepath = path.join(pwd, pathname.replace(pathPrefix, ()=>''));
105
107
  if (!isPathInside(filepath, pwd)) return next();
106
- if (!await fs.pathExists(filepath)) return next();
107
- const mimeType = getMimeType(filepath);
108
- if (mimeType) c.header('Content-Type', mimeType);
109
- const stat = await fs.lstat(filepath);
110
- const { size } = stat;
108
+ const stat = await fs.stat(filepath).catch(()=>null);
109
+ if (!stat?.isFile()) return next();
111
110
  const chunk = await fileReader.readFileFromSystem(filepath, 'buffer');
112
- c.header('Content-Length', String(size));
113
111
  if (null === chunk) return next();
112
+ const mimeType = getMimeType(filepath);
113
+ if (mimeType) c.header('Content-Type', mimeType);
114
+ c.header('Content-Length', String(chunk.byteLength));
114
115
  const body = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
115
116
  return c.body(body, 200);
116
117
  }
@@ -124,13 +124,10 @@ const injectServerTiming = ()=>({
124
124
  const monitors = c.get('monitors');
125
125
  monitors?.push(timingMonitor);
126
126
  await next();
127
- serverTimings.forEach((serverTiming)=>{
128
- const { name, desc, dur } = serverTiming;
129
- const _name = `bd-${metaName}-${name}`;
130
- const value = `${_name};${desc ? `decs="${desc}";` : ''} dur=${dur}`;
131
- c.header(SERVER_TIMING, value, {
132
- append: true
133
- });
127
+ if (0 === serverTimings.length) return;
128
+ const value = serverTimings.map(({ name, desc, dur })=>`bd-${metaName}-${name};${desc ? `decs="${desc}";` : ''} dur=${dur}`).join(', ');
129
+ c.header(SERVER_TIMING, value, {
130
+ append: true
134
131
  });
135
132
  }
136
133
  });
@@ -3,10 +3,6 @@ import { getLoaderCtx } from "../../helper.mjs";
3
3
  import { sortRoutes } from "../../utils/index.mjs";
4
4
  import { requestLatencyMiddleware } from "../monitors.mjs";
5
5
  export * from "./inject.mjs";
6
- const DYNAMIC_ROUTE_REG = /\/:./;
7
- function escapeRegexSpecialChars(path) {
8
- return path.replace(/[()[\]{}*+?.^$|\\]/g, '\\$&');
9
- }
10
6
  const renderPlugin = ()=>({
11
7
  name: '@modern-js/plugin-render',
12
8
  setup (api) {
@@ -20,9 +16,7 @@ const renderPlugin = ()=>({
20
16
  });
21
17
  for (const route of pageRoutes){
22
18
  const { urlPath: originUrlPath } = route;
23
- const isDynamic = DYNAMIC_ROUTE_REG.test(originUrlPath);
24
- const escapedPath = isDynamic ? originUrlPath : escapeRegexSpecialChars(originUrlPath);
25
- const urlPath = escapedPath.endsWith('/') ? `${escapedPath}*` : `${escapedPath}/*`;
19
+ const urlPath = originUrlPath.endsWith('/') ? `${originUrlPath}*` : `${originUrlPath}/*`;
26
20
  renderMiddlewares?.forEach((m)=>{
27
21
  middlewares.push({
28
22
  name: m.name,
@@ -1,8 +1,9 @@
1
1
  import "node:module";
2
2
  import { cutNameByHyphen } from "@modern-js/utils/universal";
3
3
  import { TrieRouter } from "hono/router/trie-router";
4
+ import { getPath } from "hono/utils/url";
4
5
  import { X_MODERNJS_RENDER } from "../../constants.mjs";
5
- import { ErrorDigest, createErrorHtml, getPathname, getRuntimeEnv, parseHeaders, parseQuery, sortRoutes, uniqueKeyByRoute } from "../../utils/index.mjs";
6
+ import { ErrorDigest, createErrorHtml, getRuntimeEnv, parseHeaders, parseQuery, sortRoutes, uniqueKeyByRoute } from "../../utils/index.mjs";
6
7
  import { csrRscRender } from "./csrRscRender.mjs";
7
8
  import { dataHandler } from "./dataHandler.mjs";
8
9
  import { renderRscHandler } from "./renderRscHandler.mjs";
@@ -64,11 +65,11 @@ function getHeadersWithoutCookie(headers) {
64
65
  }
65
66
  async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config }) {
66
67
  const router = getRouter(routes);
68
+ const framework = cutNameByHyphen(metaName || 'modern-js');
69
+ const fallbackHeader = `x-${framework}-ssr-fallback`;
67
70
  return async (req, { monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext, contextForceCSR, reporter })=>{
68
- const forMatchpathname = matchPathname ?? getPathname(req);
71
+ const forMatchpathname = matchPathname ?? getPath(req);
69
72
  const [routeInfo, params] = matchRoute(router, forMatchpathname, matchEntryName);
70
- const framework = cutNameByHyphen(metaName || 'modern-js');
71
- const fallbackHeader = `x-${framework}-ssr-fallback`;
72
73
  let fallbackReason = null;
73
74
  const fallbackWrapper = (reason)=>{
74
75
  fallbackReason = reason;
@@ -94,9 +95,8 @@ async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig
94
95
  });
95
96
  const finalForceCSR = routeInfo.entryName ? forceCSRMap?.get(routeInfo.entryName) ?? forceCSR : forceCSR;
96
97
  const renderMode = await getRenderMode(req, fallbackHeader, routeInfo.isSSR, finalForceCSR, nodeReq, fallbackWrapper, contextForceCSR);
97
- const headerData = parseHeaders(req);
98
98
  const onError = (e, key)=>{
99
- monitors?.error(`SSR Error - ${key || (e instanceof Error ? e.name : e)}, error = %s, req.url = %s, req.headers = %o`, e instanceof Error ? e.stack || e.message : e, forMatchpathname, getHeadersWithoutCookie(headerData));
99
+ monitors?.error(`SSR Error - ${key || (e instanceof Error ? e.name : e)}, error = %s, req.url = %s, req.headers = %o`, e instanceof Error ? e.stack || e.message : e, forMatchpathname, getHeadersWithoutCookie(parseHeaders(req)));
100
100
  };
101
101
  const onTiming = (name, dur)=>{
102
102
  monitors?.timing(name, dur, 'SSR');
@@ -104,6 +104,15 @@ function shouldUseCache(request) {
104
104
  const hasLoaderQuery = url.searchParams.has('__loader');
105
105
  return !(hasRSCAction || hasRSCTree || hasLoaderQuery);
106
106
  }
107
+ const cacheKeyRegExps = new Map();
108
+ function getCacheKeyRegExp(key) {
109
+ let regExp = cacheKeyRegExps.get(key);
110
+ if (!regExp) {
111
+ regExp = new RegExp(key);
112
+ cacheKeyRegExps.set(key, regExp);
113
+ }
114
+ return regExp;
115
+ }
107
116
  function matchCacheControl(cacheOption, req) {
108
117
  if (!cacheOption || !req) return;
109
118
  else {
@@ -111,7 +120,7 @@ function matchCacheControl(cacheOption, req) {
111
120
  if (isCacheOptionProvider(cacheOption)) return cacheOption(req);
112
121
  const url = req.url;
113
122
  const options = Object.entries(cacheOption);
114
- for (const [key, option] of options)if ('*' === key || new RegExp(key).test(url)) if ('function' == typeof option) return option(req);
123
+ for (const [key, option] of options)if ('*' === key || getCacheKeyRegExp(key).test(url)) if ('function' == typeof option) return option(req);
115
124
  else return option;
116
125
  return;
117
126
  }
@@ -1,7 +1,7 @@
1
1
  import "node:module";
2
2
  import { MAIN_ENTRY_NAME } from "@modern-js/utils/universal/constants";
3
3
  import { X_MODERNJS_RENDER } from "../../constants.mjs";
4
- import { getPathname, parseHeaders } from "../../utils/index.mjs";
4
+ import { getPathname } from "../../utils/index.mjs";
5
5
  import { getCacheResult, matchCacheControl, shouldUseCache } from "./ssrCache.mjs";
6
6
  import { createRequestHandlerConfig } from "./utils.mjs";
7
7
  const SERVER_RUNTIME_ENTRY = 'requestHandler';
@@ -9,10 +9,6 @@ async function ssrRender(request, { routeInfo, html, config: userConfig, staticG
9
9
  const { entryName } = routeInfo;
10
10
  const loadableStats = serverManifest.loadableStats || {};
11
11
  const routeManifest = serverManifest.routeManifest || {};
12
- const headers = parseHeaders(request);
13
- if (nodeReq) {
14
- for(const key in nodeReq.headers)if (!headers[key]) headers[key] = nodeReq.headers[key];
15
- }
16
12
  const renderBundle = serverManifest.renderBundles?.[entryName || MAIN_ENTRY_NAME];
17
13
  if (!renderBundle) throw new Error(`Can't find renderBundle ${entryName || MAIN_ENTRY_NAME}`);
18
14
  const requestHandler = await renderBundle[SERVER_RUNTIME_ENTRY];
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "modern",
16
16
  "modern.js"
17
17
  ],
18
- "version": "3.10.1",
18
+ "version": "3.10.3",
19
19
  "types": "./dist/types/index.d.ts",
20
20
  "main": "./dist/cjs/index.js",
21
21
  "exports": {
@@ -71,9 +71,9 @@
71
71
  "flatted": "^3.4.4",
72
72
  "hono": "^4.11.7",
73
73
  "ts-deepmerge": "8.0.0",
74
- "@modern-js/plugin": "3.10.1",
75
- "@modern-js/utils": "3.10.1",
76
- "@modern-js/runtime-utils": "3.10.1"
74
+ "@modern-js/runtime-utils": "3.10.3",
75
+ "@modern-js/utils": "3.10.3",
76
+ "@modern-js/plugin": "3.10.3"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/cloneable-readable": "^2.0.3",
@@ -81,9 +81,9 @@
81
81
  "@types/node": "^20",
82
82
  "http-proxy-middleware": "^2.0.10",
83
83
  "typescript": "^5",
84
- "@modern-js/rslib": "2.68.10",
85
- "@modern-js/types": "3.10.1",
86
- "@scripts/rstest-config": "2.66.0"
84
+ "@modern-js/rslib": "3.10.3",
85
+ "@modern-js/types": "3.10.3",
86
+ "@scripts/rstest-config": "2.66.2"
87
87
  },
88
88
  "sideEffects": false,
89
89
  "publishConfig": {