@modern-js/server-core 3.10.0 → 3.10.2

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,
@@ -109,11 +109,11 @@ var __webpack_exports__ = {};
109
109
  }
110
110
  async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config }) {
111
111
  const router = getRouter(routes);
112
+ const framework = (0, universal_namespaceObject.cutNameByHyphen)(metaName || 'modern-js');
113
+ const fallbackHeader = `x-${framework}-ssr-fallback`;
112
114
  return async (req, { monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext, contextForceCSR, reporter })=>{
113
115
  const forMatchpathname = matchPathname ?? (0, index_js_namespaceObject.getPathname)(req);
114
116
  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
117
  let fallbackReason = null;
118
118
  const fallbackWrapper = (reason)=>{
119
119
  fallbackReason = reason;
@@ -139,9 +139,8 @@ var __webpack_exports__ = {};
139
139
  });
140
140
  const finalForceCSR = routeInfo.entryName ? forceCSRMap?.get(routeInfo.entryName) ?? forceCSR : forceCSR;
141
141
  const renderMode = await getRenderMode(req, fallbackHeader, routeInfo.isSSR, finalForceCSR, nodeReq, fallbackWrapper, contextForceCSR);
142
- const headerData = (0, index_js_namespaceObject.parseHeaders)(req);
143
142
  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));
143
+ 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
144
  };
146
145
  const onTiming = (name, dur)=>{
147
146
  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];
@@ -31,6 +31,7 @@ __webpack_require__.d(__webpack_exports__, {
31
31
  ServerBase: ()=>ServerBase,
32
32
  createServerBase: ()=>createServerBase
33
33
  });
34
+ const fileReader_namespaceObject = require("@modern-js/runtime-utils/fileReader");
34
35
  const server_namespaceObject = require("@modern-js/plugin/server");
35
36
  const external_hono_namespaceObject = require("hono");
36
37
  const external_context_js_namespaceObject = require("./context.js");
@@ -111,6 +112,7 @@ class ServerBase {
111
112
  }
112
113
  constructor(options){
113
114
  _class_private_method_init(this, _applyMiddlewares);
115
+ this.fileReader = fileReader_namespaceObject.fileReader;
114
116
  this.plugins = [];
115
117
  this.serverContext = null;
116
118
  this.serverOptions = options;
@@ -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,
@@ -61,11 +61,11 @@ function getHeadersWithoutCookie(headers) {
61
61
  }
62
62
  async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config }) {
63
63
  const router = getRouter(routes);
64
+ const framework = cutNameByHyphen(metaName || 'modern-js');
65
+ const fallbackHeader = `x-${framework}-ssr-fallback`;
64
66
  return async (req, { monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext, contextForceCSR, reporter })=>{
65
67
  const forMatchpathname = matchPathname ?? getPathname(req);
66
68
  const [routeInfo, params] = matchRoute(router, forMatchpathname, matchEntryName);
67
- const framework = cutNameByHyphen(metaName || 'modern-js');
68
- const fallbackHeader = `x-${framework}-ssr-fallback`;
69
69
  let fallbackReason = null;
70
70
  const fallbackWrapper = (reason)=>{
71
71
  fallbackReason = reason;
@@ -91,9 +91,8 @@ async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig
91
91
  });
92
92
  const finalForceCSR = routeInfo.entryName ? forceCSRMap?.get(routeInfo.entryName) ?? forceCSR : forceCSR;
93
93
  const renderMode = await getRenderMode(req, fallbackHeader, routeInfo.isSSR, finalForceCSR, nodeReq, fallbackWrapper, contextForceCSR);
94
- const headerData = parseHeaders(req);
95
94
  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));
95
+ 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
96
  };
98
97
  const onTiming = (name, dur)=>{
99
98
  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,3 +1,4 @@
1
+ import { fileReader } from "@modern-js/runtime-utils/fileReader";
1
2
  import { server as server_server } from "@modern-js/plugin/server";
2
3
  import { Hono } from "hono";
3
4
  import { run } from "./context.mjs";
@@ -78,6 +79,7 @@ class ServerBase {
78
79
  }
79
80
  constructor(options){
80
81
  _class_private_method_init(this, _applyMiddlewares);
82
+ this.fileReader = fileReader;
81
83
  this.plugins = [];
82
84
  this.serverContext = null;
83
85
  this.serverOptions = options;
@@ -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,
@@ -64,11 +64,11 @@ function getHeadersWithoutCookie(headers) {
64
64
  }
65
65
  async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config }) {
66
66
  const router = getRouter(routes);
67
+ const framework = cutNameByHyphen(metaName || 'modern-js');
68
+ const fallbackHeader = `x-${framework}-ssr-fallback`;
67
69
  return async (req, { monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext, contextForceCSR, reporter })=>{
68
70
  const forMatchpathname = matchPathname ?? getPathname(req);
69
71
  const [routeInfo, params] = matchRoute(router, forMatchpathname, matchEntryName);
70
- const framework = cutNameByHyphen(metaName || 'modern-js');
71
- const fallbackHeader = `x-${framework}-ssr-fallback`;
72
72
  let fallbackReason = null;
73
73
  const fallbackWrapper = (reason)=>{
74
74
  fallbackReason = reason;
@@ -94,9 +94,8 @@ async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig
94
94
  });
95
95
  const finalForceCSR = routeInfo.entryName ? forceCSRMap?.get(routeInfo.entryName) ?? forceCSR : forceCSR;
96
96
  const renderMode = await getRenderMode(req, fallbackHeader, routeInfo.isSSR, finalForceCSR, nodeReq, fallbackWrapper, contextForceCSR);
97
- const headerData = parseHeaders(req);
98
97
  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));
98
+ 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
99
  };
101
100
  const onTiming = (name, dur)=>{
102
101
  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];
@@ -1,4 +1,5 @@
1
1
  import "node:module";
2
+ import { fileReader } from "@modern-js/runtime-utils/fileReader";
2
3
  import { server as server_server } from "@modern-js/plugin/server";
3
4
  import { Hono } from "hono";
4
5
  import { run } from "./context.mjs";
@@ -79,6 +80,7 @@ class ServerBase {
79
80
  }
80
81
  constructor(options){
81
82
  _class_private_method_init(this, _applyMiddlewares);
83
+ this.fileReader = fileReader;
82
84
  this.plugins = [];
83
85
  this.serverContext = null;
84
86
  this.serverOptions = options;
@@ -9,6 +9,7 @@ export interface ServerBaseOptions extends ServerCreateOptions {
9
9
  }
10
10
  export declare class ServerBase<E extends Env = any> {
11
11
  #private;
12
+ readonly fileReader: import("@modern-js/runtime-utils/fileReader").FileReader;
12
13
  serverOptions: ServerBaseOptions;
13
14
  private app;
14
15
  private plugins;
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "modern",
16
16
  "modern.js"
17
17
  ],
18
- "version": "3.10.0",
18
+ "version": "3.10.2",
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.0",
75
- "@modern-js/runtime-utils": "3.10.0",
76
- "@modern-js/utils": "3.10.0"
74
+ "@modern-js/plugin": "3.10.2",
75
+ "@modern-js/runtime-utils": "3.10.2",
76
+ "@modern-js/utils": "3.10.2"
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.0",
86
- "@scripts/rstest-config": "2.66.0"
84
+ "@modern-js/rslib": "3.10.2",
85
+ "@modern-js/types": "3.10.2",
86
+ "@scripts/rstest-config": "2.66.1"
87
87
  },
88
88
  "sideEffects": false,
89
89
  "publishConfig": {