@bleedingdev/modern-js-server-core 3.8.2-ultramodern.9 → 3.8.3-ultramodern.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.
Files changed (39) hide show
  1. package/dist/cjs/adapters/node/node.js +3 -1
  2. package/dist/cjs/adapters/node/plugins/staticModuleFederation.js +5 -1
  3. package/dist/cjs/adapters/node/plugins/staticPrecompressed.js +39 -19
  4. package/dist/cjs/adapters/node/plugins/staticServing.js +27 -4
  5. package/dist/cjs/plugins/compat/hooks.js +0 -1
  6. package/dist/cjs/plugins/compat/index.js +0 -1
  7. package/dist/cjs/plugins/render/inject.js +3 -13
  8. package/dist/cjs/plugins/render/render.js +9 -8
  9. package/dist/cjs/plugins/render/serverActionHandler.js +6 -0
  10. package/dist/cjs/utils/error.js +4 -56
  11. package/dist/esm/adapters/node/node.mjs +3 -1
  12. package/dist/esm/adapters/node/plugins/staticModuleFederation.mjs +5 -1
  13. package/dist/esm/adapters/node/plugins/staticPrecompressed.mjs +39 -19
  14. package/dist/esm/adapters/node/plugins/staticServing.mjs +27 -4
  15. package/dist/esm/plugins/compat/hooks.mjs +0 -1
  16. package/dist/esm/plugins/compat/index.mjs +1 -2
  17. package/dist/esm/plugins/render/inject.mjs +3 -13
  18. package/dist/esm/plugins/render/render.mjs +9 -8
  19. package/dist/esm/plugins/render/serverActionHandler.mjs +6 -0
  20. package/dist/esm/utils/error.mjs +2 -54
  21. package/dist/esm-node/adapters/node/node.mjs +3 -1
  22. package/dist/esm-node/adapters/node/plugins/staticModuleFederation.mjs +5 -1
  23. package/dist/esm-node/adapters/node/plugins/staticPrecompressed.mjs +39 -19
  24. package/dist/esm-node/adapters/node/plugins/staticServing.mjs +27 -4
  25. package/dist/esm-node/plugins/compat/hooks.mjs +0 -1
  26. package/dist/esm-node/plugins/compat/index.mjs +1 -2
  27. package/dist/esm-node/plugins/render/inject.mjs +3 -13
  28. package/dist/esm-node/plugins/render/render.mjs +9 -8
  29. package/dist/esm-node/plugins/render/serverActionHandler.mjs +6 -0
  30. package/dist/esm-node/utils/error.mjs +2 -54
  31. package/dist/types/adapters/node/plugins/staticPrecompressed.d.ts +1 -0
  32. package/dist/types/adapters/node/plugins/staticServing.d.ts +4 -4
  33. package/dist/types/plugins/render/inject.d.ts +1 -1
  34. package/dist/types/plugins/render/render.d.ts +2 -3
  35. package/dist/types/types/plugins/base.d.ts +1 -13
  36. package/dist/types/types/plugins/index.d.ts +1 -1
  37. package/dist/types/types/plugins/plugin.d.ts +2 -4
  38. package/dist/types/utils/error.d.ts +2 -16
  39. package/package.json +7 -6
@@ -5,23 +5,14 @@ const injectRenderHandlerPlugin = ({ staticGenerate, cacheConfig })=>({
5
5
  api.onPrepare(async ()=>{
6
6
  const { distDirectory: pwd, routes, metaName } = api.getServerContext();
7
7
  const config = api.getServerConfig();
8
- const hooks = api.getHooks();
9
8
  if (!routes) return;
10
- const onFallback = async (reason, error, context)=>{
11
- await hooks.fallback.call({
12
- reason,
13
- error,
14
- context
15
- });
16
- };
17
9
  const getRenderHandlerOptions = {
18
10
  pwd: pwd,
19
11
  routes,
20
12
  config,
21
13
  metaName,
22
14
  cacheConfig: cacheConfig,
23
- staticGenerate,
24
- onFallback
15
+ staticGenerate
25
16
  };
26
17
  const render = await getRenderHandler(getRenderHandlerOptions);
27
18
  api.updateServerContext({
@@ -31,7 +22,7 @@ const injectRenderHandlerPlugin = ({ staticGenerate, cacheConfig })=>({
31
22
  });
32
23
  }
33
24
  });
34
- async function getRenderHandler({ pwd, routes, config, cacheConfig, metaName, staticGenerate, onFallback }) {
25
+ async function getRenderHandler({ pwd, routes, config, cacheConfig, metaName, staticGenerate }) {
35
26
  const ssrConfig = config.server?.ssr;
36
27
  const ssrByEntries = config.server?.ssrByEntries;
37
28
  const forceCSR = 'object' == typeof ssrConfig ? ssrConfig.forceCSR : false;
@@ -48,8 +39,7 @@ async function getRenderHandler({ pwd, routes, config, cacheConfig, metaName, st
48
39
  forceCSR,
49
40
  forceCSRMap,
50
41
  nonce: config.security?.nonce,
51
- metaName: metaName || 'modern-js',
52
- onFallback
42
+ metaName: metaName || 'modern-js'
53
43
  });
54
44
  return render;
55
45
  }
@@ -67,7 +67,7 @@ function getHeadersWithoutCookie(headers) {
67
67
  delete _headers.cookie;
68
68
  return _headers;
69
69
  }
70
- async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config, onFallback }) {
70
+ async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config }) {
71
71
  const router = getRouter(routes);
72
72
  return async (req, { monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext, serverContext, contextForceCSR, reporter })=>{
73
73
  const forMatchpathname = matchPathname ?? getPathname(req);
@@ -75,13 +75,14 @@ async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig
75
75
  const framework = cutNameByHyphen(metaName || 'modern-js');
76
76
  const fallbackHeader = `x-${framework}-ssr-fallback`;
77
77
  let fallbackReason = null;
78
- const fallbackContext = {
79
- request: req,
80
- monitors
81
- };
82
- const fallbackWrapper = async (reason, error)=>{
78
+ const fallbackWrapper = (reason)=>{
83
79
  fallbackReason = reason;
84
- return onFallback?.(reason, error, fallbackContext);
80
+ monitors?.warn('fallback to CSR reason: %o', {
81
+ type: reason
82
+ });
83
+ monitors?.counter('ssr-fallback', {
84
+ type: reason
85
+ });
85
86
  };
86
87
  if (!routeInfo) return new Response(createErrorHtml(404), {
87
88
  status: 404,
@@ -177,7 +178,7 @@ async function renderHandler(request, options, mode, fallbackWrapper, framework)
177
178
  response = await ssrRender(request, options);
178
179
  } catch (e) {
179
180
  options.onError(e, ErrorDigest.ERENDER);
180
- await fallbackWrapper('error', e);
181
+ fallbackWrapper('error');
181
182
  response = await csrRender(request, {
182
183
  ...options,
183
184
  html: injectFallbackReasonToHtml({
@@ -1,5 +1,11 @@
1
1
  import { MAIN_ENTRY_NAME } from "@modern-js/utils/universal/constants";
2
2
  const serverActionHandler = async (req, { serverManifest, routeInfo, rscClientManifest })=>{
3
+ if ('POST' !== req.method) return new Response('Method not allowed', {
4
+ status: 405,
5
+ headers: {
6
+ Allow: 'POST'
7
+ }
8
+ });
3
9
  const serverBundle = serverManifest?.renderBundles?.[routeInfo.entryName || MAIN_ENTRY_NAME];
4
10
  if (!serverBundle) return new Response('Cannot find server bundle for server action', {
5
11
  status: 500
@@ -3,59 +3,6 @@ const ERROR_PAGE_TEXT = {
3
3
  404: 'This page could not be found.',
4
4
  500: 'Internal Server Error.'
5
5
  };
6
- const SAFE_FAILURE_MESSAGES = {
7
- 500: 'Internal Server Error',
8
- 503: 'Service Unavailable'
9
- };
10
- const SAFE_FAILURE_CODES = {
11
- 500: 'INTERNAL_SERVER_ERROR',
12
- 503: 'SERVICE_UNAVAILABLE'
13
- };
14
- const readErrorProperty = (error, key)=>{
15
- if ('object' != typeof error || null === error || !(key in error)) return;
16
- return error[key];
17
- };
18
- const normalizeFailureStatus = (value)=>{
19
- if ('number' != typeof value || !Number.isInteger(value)) return;
20
- return value >= 400 && value <= 599 ? value : void 0;
21
- };
22
- const normalizeRetryAfter = (value)=>{
23
- if ('number' == typeof value && Number.isFinite(value) && value >= 0) return String(Math.ceil(value));
24
- if ('string' == typeof value) {
25
- const trimmed = value.trim();
26
- return trimmed.length > 0 ? trimmed : void 0;
27
- }
28
- if (value instanceof Date) return value.toUTCString();
29
- };
30
- const getSafeFailureStatus = (error)=>normalizeFailureStatus(readErrorProperty(error, 'status')) ?? normalizeFailureStatus(readErrorProperty(error, 'statusCode')) ?? 500;
31
- const createSafeFailureHttpResult = (error)=>{
32
- const status = getSafeFailureStatus(error);
33
- const retryAfter = 503 === status ? normalizeRetryAfter(readErrorProperty(error, 'retryAfter')) ?? normalizeRetryAfter(readErrorProperty(error, 'retryAfterSeconds')) ?? normalizeRetryAfter('number' == typeof readErrorProperty(error, 'retryAfterMs') ? readErrorProperty(error, 'retryAfterMs') / 1000 : void 0) : void 0;
34
- return {
35
- status,
36
- body: {
37
- success: false,
38
- error: {
39
- code: SAFE_FAILURE_CODES[status] ?? 'REQUEST_FAILED',
40
- message: SAFE_FAILURE_MESSAGES[status] ?? 'Request failed',
41
- status
42
- }
43
- },
44
- headers: {
45
- 'content-type': 'application/json; charset=utf-8',
46
- ...retryAfter ? {
47
- 'Retry-After': retryAfter
48
- } : {}
49
- }
50
- };
51
- };
52
- const createSafeJsonFailureResponse = (error)=>{
53
- const result = createSafeFailureHttpResult(error);
54
- return new Response(JSON.stringify(result.body), {
55
- status: result.status,
56
- headers: result.headers
57
- });
58
- };
59
6
  const createErrorHtml = (status)=>{
60
7
  const text = ERROR_PAGE_TEXT[status] || '';
61
8
  const title = `${status}: ${text}`;
@@ -103,4 +50,5 @@ function onError(digest, error, monitors, req) {
103
50
  else if (req) console.error(`Server Error - ${digest}, error = ${error instanceof Error ? error.stack || error.message : error}, req.url = ${req.url}, req.headers = ${JSON.stringify(headerData)}`);
104
51
  else console.error(`Server Error - ${digest}, error = ${error instanceof Error ? error.stack || error.message : error} `);
105
52
  }
106
- export { createErrorHtml, createSafeFailureHttpResult, createSafeJsonFailureResponse, error_ErrorDigest as ErrorDigest, getSafeFailureStatus, onError };
53
+ export { createSafeFailureHttpResult, createSafeJsonFailureResponse, getSafeFailureStatus } from "@modern-js/runtime-extensions/safe-failure";
54
+ export { createErrorHtml, error_ErrorDigest as ErrorDigest, onError };
@@ -26,7 +26,9 @@ const createWebRequest = (req, res, body)=>{
26
26
  signal: controller.signal
27
27
  };
28
28
  res.on('close', ()=>controller.abort('res closed'));
29
- const url = `http://${req.headers.host}${req.url}`;
29
+ const protocol = 'encrypted' in req.socket && req.socket.encrypted ? 'https' : 'http';
30
+ const authority = req.headers[':authority'] ?? req.headers.host;
31
+ const url = `${protocol}://${authority}${req.url}`;
30
32
  const needsRequestBody = body || !('GET' === method || 'HEAD' === method);
31
33
  const cloneableReq = needsRequestBody ? cloneable_readable(req) : null;
32
34
  if (needsRequestBody) {
@@ -12,7 +12,11 @@ const MODULE_FEDERATION_OPTIONAL_FILES = [
12
12
  'mf-stats.json'
13
13
  ];
14
14
  const trimLeadingSlash = (value)=>value.replace(/^\/+/, '');
15
- const getModuleFederationRequestPath = (pathname, pathPrefix)=>trimLeadingSlash(pathname.replace(pathPrefix, ()=>''));
15
+ const getModuleFederationRequestPath = (pathname, pathPrefix)=>{
16
+ const normalizedPrefix = `/${trimLeadingSlash(pathPrefix)}`.replace(/\/+$/u, '');
17
+ const requestPath = normalizedPrefix && (pathname === normalizedPrefix || pathname.startsWith(`${normalizedPrefix}/`)) ? pathname.slice(normalizedPrefix.length) : pathname;
18
+ return trimLeadingSlash(requestPath);
19
+ };
16
20
  const isModuleFederationManifestRequest = (requestPath)=>MODULE_FEDERATION_MANIFEST_FILES.includes(requestPath);
17
21
  const isBackendModuleFederationManifestRequest = (requestPath)=>requestPath === BACKEND_MODULE_FEDERATION_MANIFEST_FILE;
18
22
  const applyModuleFederationAssetHeaders = (c)=>{
@@ -8,23 +8,32 @@ const PRE_COMPRESSED_SUPPORTED_ENCODINGS = [
8
8
  'br',
9
9
  'gzip'
10
10
  ];
11
+ const QUALITY_VALUE_PATTERN = /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/u;
11
12
  const parseAcceptEncoding = (value)=>value.split(',').map((item)=>item.trim()).filter(Boolean).map((item)=>{
12
13
  const [rawName, ...params] = item.split(';');
13
14
  const name = rawName.trim().toLowerCase();
14
15
  let q = 1;
16
+ let qualitySeen = false;
15
17
  for (const param of params){
16
18
  const [key, rawValue] = param.split('=').map((v)=>v.trim());
17
- if ('q' !== key.toLowerCase() || null == rawValue) continue;
18
- const parsedQ = Number(rawValue);
19
- if (!Number.isNaN(parsedQ)) q = Math.max(0, Math.min(parsedQ, 1));
19
+ if ('q' === key.toLowerCase()) {
20
+ if (qualitySeen || null == rawValue || !QUALITY_VALUE_PATTERN.test(rawValue)) {
21
+ q = 0;
22
+ break;
23
+ }
24
+ qualitySeen = true;
25
+ q = Number(rawValue);
26
+ }
20
27
  }
21
28
  return {
22
29
  name,
23
30
  q
24
31
  };
25
32
  });
26
- const getAcceptedEncodings = (value)=>{
27
- if (!value) return [];
33
+ const getAcceptedRepresentations = (value)=>{
34
+ if (!value) return [
35
+ 'identity'
36
+ ];
28
37
  const parsed = parseAcceptEncoding(value);
29
38
  const qualityByEncoding = new Map();
30
39
  let wildcardQuality;
@@ -40,10 +49,17 @@ const getAcceptedEncodings = (value)=>{
40
49
  if (void 0 !== explicit) return explicit;
41
50
  return wildcardQuality ?? 0;
42
51
  };
43
- return PRE_COMPRESSED_SUPPORTED_ENCODINGS.map((encoding)=>({
44
- encoding,
45
- quality: getQuality(encoding)
46
- })).filter((item)=>item.quality > 0).sort((a, b)=>b.quality - a.quality).map((item)=>item.encoding);
52
+ const identityQuality = qualityByEncoding.get('identity') ?? (0 === wildcardQuality ? 0 : 1);
53
+ return [
54
+ ...PRE_COMPRESSED_SUPPORTED_ENCODINGS.map((encoding)=>({
55
+ encoding,
56
+ quality: getQuality(encoding)
57
+ })),
58
+ {
59
+ encoding: 'identity',
60
+ quality: identityQuality
61
+ }
62
+ ].filter((item)=>item.quality > 0).sort((a, b)=>b.quality - a.quality).map((item)=>item.encoding);
47
63
  };
48
64
  const appendVaryHeader = (c, value)=>{
49
65
  const current = c.res.headers.get('Vary');
@@ -59,34 +75,38 @@ const resolvePreCompressedAsset = async (c, filepath)=>{
59
75
  fs.pathExists(gzipPath)
60
76
  ]);
61
77
  const hasVariant = hasBr || hasGzip;
62
- if (!hasVariant) return {
63
- selected: null,
64
- hasVariant: false
65
- };
66
- const acceptedEncodings = getAcceptedEncodings(c.req.header('accept-encoding'));
67
- for (const encoding of acceptedEncodings){
78
+ const acceptedRepresentations = getAcceptedRepresentations(c.req.header('accept-encoding'));
79
+ for (const encoding of acceptedRepresentations){
80
+ if ('identity' === encoding) return {
81
+ selected: null,
82
+ hasVariant,
83
+ acceptable: true
84
+ };
68
85
  if ('br' === encoding && hasBr) return {
69
86
  selected: {
70
87
  filepath: brPath,
71
88
  encoding
72
89
  },
73
- hasVariant: true
90
+ hasVariant: true,
91
+ acceptable: true
74
92
  };
75
93
  if ('gzip' === encoding && hasGzip) return {
76
94
  selected: {
77
95
  filepath: gzipPath,
78
96
  encoding
79
97
  },
80
- hasVariant: true
98
+ hasVariant: true,
99
+ acceptable: true
81
100
  };
82
101
  }
83
102
  return {
84
103
  selected: null,
85
- hasVariant: true
104
+ hasVariant,
105
+ acceptable: false
86
106
  };
87
107
  };
88
108
  const applyPreCompressedAssetHeaders = (c, preCompressedAsset)=>{
89
- if (preCompressedAsset.hasVariant) appendVaryHeader(c, 'Accept-Encoding');
109
+ if (preCompressedAsset.hasVariant || !preCompressedAsset.acceptable) appendVaryHeader(c, 'Accept-Encoding');
90
110
  if (preCompressedAsset.selected) c.header('Content-Encoding', preCompressedAsset.selected.encoding);
91
111
  };
92
112
  export { applyPreCompressedAssetHeaders, resolvePreCompressedAsset };
@@ -5,11 +5,16 @@ import { getMimeType } from "hono/utils/mime";
5
5
  import path from "path";
6
6
  import { applyModuleFederationAssetHeaders, getModuleFederationAssetList, getModuleFederationRequestPath, isBackendModuleFederationManifestRequest, isModuleFederationManifestRequest, patchModuleFederationManifestPublicPath, patchModuleFederationRemoteEntryPublicPath } from "./staticModuleFederation.mjs";
7
7
  import { applyPreCompressedAssetHeaders, resolvePreCompressedAsset } from "./staticPrecompressed.mjs";
8
+ const MODULE_FEDERATION_ASSET_REFRESH_INTERVAL_MS = 1000;
8
9
  const getStaticMimeType = (filename)=>getMimeType(filename) ?? ('.cjs' === path.extname(filename).toLowerCase() ? "text/javascript; charset=UTF-8" : void 0);
9
10
  const servePreCompressedPublicRouteAsset = async (c, pwd, route)=>{
10
11
  const { entryPath } = route;
11
12
  const originFilename = path.join(pwd, entryPath);
12
13
  const preCompressedAsset = await resolvePreCompressedAsset(c, originFilename);
14
+ if (!preCompressedAsset.acceptable) {
15
+ applyPreCompressedAssetHeaders(c, preCompressedAsset);
16
+ return c.body(null, 406);
17
+ }
13
18
  const filename = preCompressedAsset.selected?.filepath ?? originFilename;
14
19
  const data = await fileReader.readFile(filename, 'buffer');
15
20
  const mimeType = getStaticMimeType(originFilename);
@@ -56,6 +61,10 @@ const servePublicDirectoryAsset = async (c, pwd)=>{
56
61
  const originFilename = await resolvePublicDirectoryAsset(pwd, c.req.path);
57
62
  if (null === originFilename) return null;
58
63
  const preCompressedAsset = await resolvePreCompressedAsset(c, originFilename);
64
+ if (!preCompressedAsset.acceptable) {
65
+ applyPreCompressedAssetHeaders(c, preCompressedAsset);
66
+ return c.body(null, 406);
67
+ }
59
68
  const selectedFilename = preCompressedAsset.selected?.filepath ?? originFilename;
60
69
  const publicDirectory = await fs.realpath(path.join(pwd, 'public'));
61
70
  let realSelectedFilename;
@@ -76,10 +85,19 @@ const servePublicDirectoryAsset = async (c, pwd)=>{
76
85
  return c.body(body, 200);
77
86
  };
78
87
  const createModuleFederationStaticServing = ({ pwd, pathPrefix })=>{
79
- let moduleFederationAssetsPromise = null;
88
+ let moduleFederationAssets = null;
89
+ let moduleFederationAssetsExpiresAt = 0;
90
+ let moduleFederationAssetsRefresh = null;
80
91
  const getModuleFederationAssets = async ()=>{
81
- if (!moduleFederationAssetsPromise) moduleFederationAssetsPromise = getModuleFederationAssetList(pwd);
82
- return moduleFederationAssetsPromise;
92
+ if (moduleFederationAssets && Date.now() < moduleFederationAssetsExpiresAt) return moduleFederationAssets;
93
+ if (!moduleFederationAssetsRefresh) moduleFederationAssetsRefresh = getModuleFederationAssetList(pwd).then((assets)=>{
94
+ moduleFederationAssets = assets;
95
+ moduleFederationAssetsExpiresAt = Date.now() + MODULE_FEDERATION_ASSET_REFRESH_INTERVAL_MS;
96
+ return assets;
97
+ }).finally(()=>{
98
+ moduleFederationAssetsRefresh = null;
99
+ });
100
+ return moduleFederationAssetsRefresh;
83
101
  };
84
102
  const resolveRequest = async (pathname)=>{
85
103
  const requestPath = getModuleFederationRequestPath(pathname, pathPrefix);
@@ -100,8 +118,13 @@ const createModuleFederationStaticServing = ({ pwd, pathPrefix })=>{
100
118
  const canUsePreCompressed = !shouldPatchManifest && !shouldPatchRemoteEntry;
101
119
  const preCompressedAsset = canUsePreCompressed ? await resolvePreCompressedAsset(c, filepath) : {
102
120
  selected: null,
103
- hasVariant: false
121
+ hasVariant: false,
122
+ acceptable: true
104
123
  };
124
+ if (!preCompressedAsset.acceptable) {
125
+ applyPreCompressedAssetHeaders(c, preCompressedAsset);
126
+ return c.body(null, 406);
127
+ }
105
128
  const targetFilepath = preCompressedAsset.selected?.filepath ?? filepath;
106
129
  const chunk = await fileReader.readFileFromSystem(targetFilepath, 'buffer');
107
130
  if (null === chunk) return null;
@@ -5,7 +5,6 @@ function getHookRunners(context) {
5
5
  config: (params)=>hooks.modifyConfig.call(params),
6
6
  prepare: ()=>hooks.onPrepare.call(),
7
7
  reset: (params)=>hooks.onReset.call(params),
8
- fallback: (input)=>hooks.fallback.call(input),
9
8
  prepareWebServer: (input)=>hooks.prepareWebServer.call(input),
10
9
  prepareApiServer: (input)=>hooks.prepareApiServer.call(input),
11
10
  afterMatch: (ctx)=>hooks.afterMatch.call(ctx),
@@ -1,10 +1,9 @@
1
1
  import "node:module";
2
- import { createAsyncHook, createAsyncPipelineHook } from "@modern-js/plugin";
2
+ import { createAsyncPipelineHook } from "@modern-js/plugin";
3
3
  import { getHookRunners, handleSetupResult } from "./hooks.mjs";
4
4
  const compatPlugin = ()=>({
5
5
  name: '@modern-js/server-compat',
6
6
  registryHooks: {
7
- fallback: createAsyncHook(),
8
7
  prepareWebServer: createAsyncPipelineHook(),
9
8
  prepareApiServer: createAsyncPipelineHook(),
10
9
  afterMatch: createAsyncPipelineHook(),
@@ -6,23 +6,14 @@ const injectRenderHandlerPlugin = ({ staticGenerate, cacheConfig })=>({
6
6
  api.onPrepare(async ()=>{
7
7
  const { distDirectory: pwd, routes, metaName } = api.getServerContext();
8
8
  const config = api.getServerConfig();
9
- const hooks = api.getHooks();
10
9
  if (!routes) return;
11
- const onFallback = async (reason, error, context)=>{
12
- await hooks.fallback.call({
13
- reason,
14
- error,
15
- context
16
- });
17
- };
18
10
  const getRenderHandlerOptions = {
19
11
  pwd: pwd,
20
12
  routes,
21
13
  config,
22
14
  metaName,
23
15
  cacheConfig: cacheConfig,
24
- staticGenerate,
25
- onFallback
16
+ staticGenerate
26
17
  };
27
18
  const render = await getRenderHandler(getRenderHandlerOptions);
28
19
  api.updateServerContext({
@@ -32,7 +23,7 @@ const injectRenderHandlerPlugin = ({ staticGenerate, cacheConfig })=>({
32
23
  });
33
24
  }
34
25
  });
35
- async function getRenderHandler({ pwd, routes, config, cacheConfig, metaName, staticGenerate, onFallback }) {
26
+ async function getRenderHandler({ pwd, routes, config, cacheConfig, metaName, staticGenerate }) {
36
27
  const ssrConfig = config.server?.ssr;
37
28
  const ssrByEntries = config.server?.ssrByEntries;
38
29
  const forceCSR = 'object' == typeof ssrConfig ? ssrConfig.forceCSR : false;
@@ -49,8 +40,7 @@ async function getRenderHandler({ pwd, routes, config, cacheConfig, metaName, st
49
40
  forceCSR,
50
41
  forceCSRMap,
51
42
  nonce: config.security?.nonce,
52
- metaName: metaName || 'modern-js',
53
- onFallback
43
+ metaName: metaName || 'modern-js'
54
44
  });
55
45
  return render;
56
46
  }
@@ -68,7 +68,7 @@ function getHeadersWithoutCookie(headers) {
68
68
  delete _headers.cookie;
69
69
  return _headers;
70
70
  }
71
- async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config, onFallback }) {
71
+ async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config }) {
72
72
  const router = getRouter(routes);
73
73
  return async (req, { monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext, serverContext, contextForceCSR, reporter })=>{
74
74
  const forMatchpathname = matchPathname ?? getPathname(req);
@@ -76,13 +76,14 @@ async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig
76
76
  const framework = cutNameByHyphen(metaName || 'modern-js');
77
77
  const fallbackHeader = `x-${framework}-ssr-fallback`;
78
78
  let fallbackReason = null;
79
- const fallbackContext = {
80
- request: req,
81
- monitors
82
- };
83
- const fallbackWrapper = async (reason, error)=>{
79
+ const fallbackWrapper = (reason)=>{
84
80
  fallbackReason = reason;
85
- return onFallback?.(reason, error, fallbackContext);
81
+ monitors?.warn('fallback to CSR reason: %o', {
82
+ type: reason
83
+ });
84
+ monitors?.counter('ssr-fallback', {
85
+ type: reason
86
+ });
86
87
  };
87
88
  if (!routeInfo) return new Response(createErrorHtml(404), {
88
89
  status: 404,
@@ -178,7 +179,7 @@ async function renderHandler(request, options, mode, fallbackWrapper, framework)
178
179
  response = await ssrRender(request, options);
179
180
  } catch (e) {
180
181
  options.onError(e, ErrorDigest.ERENDER);
181
- await fallbackWrapper('error', e);
182
+ fallbackWrapper('error');
182
183
  response = await csrRender(request, {
183
184
  ...options,
184
185
  html: injectFallbackReasonToHtml({
@@ -1,6 +1,12 @@
1
1
  import "node:module";
2
2
  import { MAIN_ENTRY_NAME } from "@modern-js/utils/universal/constants";
3
3
  const serverActionHandler = async (req, { serverManifest, routeInfo, rscClientManifest })=>{
4
+ if ('POST' !== req.method) return new Response('Method not allowed', {
5
+ status: 405,
6
+ headers: {
7
+ Allow: 'POST'
8
+ }
9
+ });
4
10
  const serverBundle = serverManifest?.renderBundles?.[routeInfo.entryName || MAIN_ENTRY_NAME];
5
11
  if (!serverBundle) return new Response('Cannot find server bundle for server action', {
6
12
  status: 500
@@ -4,59 +4,6 @@ const ERROR_PAGE_TEXT = {
4
4
  404: 'This page could not be found.',
5
5
  500: 'Internal Server Error.'
6
6
  };
7
- const SAFE_FAILURE_MESSAGES = {
8
- 500: 'Internal Server Error',
9
- 503: 'Service Unavailable'
10
- };
11
- const SAFE_FAILURE_CODES = {
12
- 500: 'INTERNAL_SERVER_ERROR',
13
- 503: 'SERVICE_UNAVAILABLE'
14
- };
15
- const readErrorProperty = (error, key)=>{
16
- if ('object' != typeof error || null === error || !(key in error)) return;
17
- return error[key];
18
- };
19
- const normalizeFailureStatus = (value)=>{
20
- if ('number' != typeof value || !Number.isInteger(value)) return;
21
- return value >= 400 && value <= 599 ? value : void 0;
22
- };
23
- const normalizeRetryAfter = (value)=>{
24
- if ('number' == typeof value && Number.isFinite(value) && value >= 0) return String(Math.ceil(value));
25
- if ('string' == typeof value) {
26
- const trimmed = value.trim();
27
- return trimmed.length > 0 ? trimmed : void 0;
28
- }
29
- if (value instanceof Date) return value.toUTCString();
30
- };
31
- const getSafeFailureStatus = (error)=>normalizeFailureStatus(readErrorProperty(error, 'status')) ?? normalizeFailureStatus(readErrorProperty(error, 'statusCode')) ?? 500;
32
- const createSafeFailureHttpResult = (error)=>{
33
- const status = getSafeFailureStatus(error);
34
- const retryAfter = 503 === status ? normalizeRetryAfter(readErrorProperty(error, 'retryAfter')) ?? normalizeRetryAfter(readErrorProperty(error, 'retryAfterSeconds')) ?? normalizeRetryAfter('number' == typeof readErrorProperty(error, 'retryAfterMs') ? readErrorProperty(error, 'retryAfterMs') / 1000 : void 0) : void 0;
35
- return {
36
- status,
37
- body: {
38
- success: false,
39
- error: {
40
- code: SAFE_FAILURE_CODES[status] ?? 'REQUEST_FAILED',
41
- message: SAFE_FAILURE_MESSAGES[status] ?? 'Request failed',
42
- status
43
- }
44
- },
45
- headers: {
46
- 'content-type': 'application/json; charset=utf-8',
47
- ...retryAfter ? {
48
- 'Retry-After': retryAfter
49
- } : {}
50
- }
51
- };
52
- };
53
- const createSafeJsonFailureResponse = (error)=>{
54
- const result = createSafeFailureHttpResult(error);
55
- return new Response(JSON.stringify(result.body), {
56
- status: result.status,
57
- headers: result.headers
58
- });
59
- };
60
7
  const createErrorHtml = (status)=>{
61
8
  const text = ERROR_PAGE_TEXT[status] || '';
62
9
  const title = `${status}: ${text}`;
@@ -104,4 +51,5 @@ function onError(digest, error, monitors, req) {
104
51
  else if (req) console.error(`Server Error - ${digest}, error = ${error instanceof Error ? error.stack || error.message : error}, req.url = ${req.url}, req.headers = ${JSON.stringify(headerData)}`);
105
52
  else console.error(`Server Error - ${digest}, error = ${error instanceof Error ? error.stack || error.message : error} `);
106
53
  }
107
- export { createErrorHtml, createSafeFailureHttpResult, createSafeJsonFailureResponse, error_ErrorDigest as ErrorDigest, getSafeFailureStatus, onError };
54
+ export { createSafeFailureHttpResult, createSafeJsonFailureResponse, getSafeFailureStatus } from "@modern-js/runtime-extensions/safe-failure";
55
+ export { createErrorHtml, error_ErrorDigest as ErrorDigest, onError };
@@ -6,6 +6,7 @@ type ResolvePreCompressedAssetResult = {
6
6
  encoding: SupportedEncoding;
7
7
  } | null;
8
8
  hasVariant: boolean;
9
+ acceptable: boolean;
9
10
  };
10
11
  export declare const resolvePreCompressedAsset: (c: Parameters<Middleware>[0], filepath: string) => Promise<ResolvePreCompressedAssetResult>;
11
12
  export declare const applyPreCompressedAssetHeaders: (c: Parameters<Middleware>[0], preCompressedAsset: ResolvePreCompressedAssetResult) => void;
@@ -10,16 +10,16 @@ type StaticServingOptions = {
10
10
  pwd: string;
11
11
  pathPrefix: string;
12
12
  };
13
- export declare const servePreCompressedPublicRouteAsset: (c: MiddlewareContext, pwd: string, route: ServerRoute) => Promise<(Response & import("hono").TypedResponse<Uint8Array<ArrayBuffer>, 200, "body">) | null>;
13
+ export declare const servePreCompressedPublicRouteAsset: (c: MiddlewareContext, pwd: string, route: ServerRoute) => Promise<(Response & import("hono").TypedResponse<null, 406, "body">) | (Response & import("hono").TypedResponse<Uint8Array<ArrayBuffer>, 200, "body">) | null>;
14
14
  /**
15
15
  * Serves post-build convention assets generated under dist/public at their
16
16
  * root URL. This is intentionally independent of config/public and route.json:
17
17
  * the generator runs after the route manifest is built.
18
18
  */
19
- export declare const servePublicDirectoryAsset: (c: MiddlewareContext, pwd: string) => Promise<(Response & import("hono").TypedResponse<null, 200, "body">) | (Response & import("hono").TypedResponse<Uint8Array<ArrayBuffer>, 200, "body">) | null>;
19
+ export declare const servePublicDirectoryAsset: (c: MiddlewareContext, pwd: string) => Promise<(Response & import("hono").TypedResponse<null, 200, "body">) | (Response & import("hono").TypedResponse<null, 406, "body">) | (Response & import("hono").TypedResponse<Uint8Array<ArrayBuffer>, 200, "body">) | null>;
20
20
  export declare const createModuleFederationStaticServing: ({ pwd, pathPrefix, }: StaticServingOptions) => {
21
21
  resolveRequest: (pathname: string) => Promise<StaticServingRequest | null>;
22
- serveStaticHit: (c: MiddlewareContext, request: StaticServingRequest) => Promise<(Response & import("hono").TypedResponse<Uint8Array<ArrayBuffer>, 200, "body">) | null>;
23
- serveModuleFederationAsset: (c: MiddlewareContext, request: StaticServingRequest) => Promise<(Response & import("hono").TypedResponse<Uint8Array<ArrayBuffer>, 200, "body">) | null> | null;
22
+ serveStaticHit: (c: MiddlewareContext, request: StaticServingRequest) => Promise<(Response & import("hono").TypedResponse<null, 406, "body">) | (Response & import("hono").TypedResponse<Uint8Array<ArrayBuffer>, 200, "body">) | null>;
23
+ serveModuleFederationAsset: (c: MiddlewareContext, request: StaticServingRequest) => Promise<(Response & import("hono").TypedResponse<null, 406, "body">) | (Response & import("hono").TypedResponse<Uint8Array<ArrayBuffer>, 200, "body">) | null> | null;
24
24
  };
25
25
  export {};
@@ -4,4 +4,4 @@ export interface InjectRenderHandlerOptions {
4
4
  cacheConfig?: CacheConfig;
5
5
  }
6
6
  export declare const injectRenderHandlerPlugin: ({ staticGenerate, cacheConfig, }: InjectRenderHandlerOptions) => ServerPlugin;
7
- export declare function getRenderHandler({ pwd, routes, config, cacheConfig, metaName, staticGenerate, onFallback, }: GetRenderHandlerOptions): Promise<Render>;
7
+ export declare function getRenderHandler({ pwd, routes, config, cacheConfig, metaName, staticGenerate, }: GetRenderHandlerOptions): Promise<Render>;
@@ -1,6 +1,6 @@
1
1
  import type { ServerRoute } from '@modern-js/types';
2
2
  import type { Router } from 'hono/router';
3
- import type { CacheConfig, OnFallback, Render, UserConfig } from '../../types';
3
+ import type { CacheConfig, Render, UserConfig } from '../../types';
4
4
  import type { Params } from '../../types/requestHandler';
5
5
  interface CreateRenderOptions {
6
6
  pwd: string;
@@ -8,7 +8,6 @@ interface CreateRenderOptions {
8
8
  config: UserConfig;
9
9
  cacheConfig?: CacheConfig;
10
10
  staticGenerate?: boolean;
11
- onFallback?: OnFallback;
12
11
  metaName?: string;
13
12
  forceCSR?: boolean;
14
13
  forceCSRMap?: Map<string, boolean>;
@@ -16,5 +15,5 @@ interface CreateRenderOptions {
16
15
  }
17
16
  type MatchedRoute = [ServerRoute | undefined, Params];
18
17
  export declare function matchRoute(router: Router<ServerRoute>, pathname: string, entryName?: string): MatchedRoute;
19
- export declare function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config, onFallback, }: CreateRenderOptions): Promise<Render>;
18
+ export declare function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, forceCSRMap, config, }: CreateRenderOptions): Promise<Render>;
20
19
  export {};
@@ -1,20 +1,9 @@
1
- import type { CacheOption, Container, HttpMethodDecider, MiddlewareContext, Monitors, ServerRoute } from '@modern-js/types';
1
+ import type { CacheOption, Container, HttpMethodDecider, MiddlewareContext, ServerRoute } from '@modern-js/types';
2
2
  import type { Context, MiddlewareHandler } from 'hono';
3
3
  import type { UserConfig } from '../config';
4
4
  import type { Render } from '../render';
5
5
  import type { ServerPlugin } from './plugin';
6
6
  export type { FileChangeEvent, ResetEvent } from '@modern-js/plugin';
7
- export type FallbackReason = 'error' | 'header' | 'query' | `header,${string}`;
8
- export type FallbackContext = {
9
- request: Request;
10
- monitors?: Monitors;
11
- };
12
- export type FallbackInput = {
13
- reason: FallbackReason;
14
- error: unknown;
15
- context?: FallbackContext;
16
- };
17
- export type OnFallback = (reason: FallbackReason, error?: unknown, context?: FallbackContext) => Promise<void>;
18
7
  export type APIServerStartInput = {
19
8
  pwd: string;
20
9
  prefix?: string;
@@ -44,7 +33,6 @@ export interface GetRenderHandlerOptions {
44
33
  pwd: string;
45
34
  routes: ServerRoute[];
46
35
  config: UserConfig;
47
- onFallback?: OnFallback;
48
36
  cacheConfig?: CacheConfig;
49
37
  staticGenerate?: boolean;
50
38
  metaName?: string;
@@ -1,2 +1,2 @@
1
- export type { APIServerStartInput, CacheConfig, FallbackContext, FallbackInput, FallbackReason, FileChangeEvent, GetRenderHandlerOptions, MiddlewareObj, OnFallback, ServerConfig, ServerMiddleware, WebServerStartInput, } from './base';
1
+ export type { APIServerStartInput, CacheConfig, FileChangeEvent, GetRenderHandlerOptions, MiddlewareObj, ServerConfig, ServerMiddleware, WebServerStartInput, } from './base';
2
2
  export * from './plugin';