@lazarv/react-server 0.0.0-experimental-43e79e6-20230928

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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/bin/cli.mjs +93 -0
  4. package/bin/commands/build.mjs +19 -0
  5. package/bin/commands/dev.mjs +23 -0
  6. package/bin/commands/start.mjs +16 -0
  7. package/bin/loader.mjs +38 -0
  8. package/client/ActionState.mjs +16 -0
  9. package/client/ClientOnly.jsx +14 -0
  10. package/client/ClientProvider.jsx +243 -0
  11. package/client/ErrorBoundary.jsx +45 -0
  12. package/client/FlightContext.mjs +3 -0
  13. package/client/Link.jsx +59 -0
  14. package/client/Params.mjs +15 -0
  15. package/client/ReactServerComponent.jsx +77 -0
  16. package/client/Refresh.jsx +52 -0
  17. package/client/components.mjs +28 -0
  18. package/client/context.mjs +6 -0
  19. package/client/entry.client.jsx +146 -0
  20. package/client/index.jsx +6 -0
  21. package/client/navigation.jsx +4 -0
  22. package/config/context.mjs +37 -0
  23. package/config/index.mjs +114 -0
  24. package/lib/build/action.mjs +57 -0
  25. package/lib/build/banner.mjs +13 -0
  26. package/lib/build/chunks.mjs +26 -0
  27. package/lib/build/client.mjs +114 -0
  28. package/lib/build/custom-logger.mjs +13 -0
  29. package/lib/build/dependencies.mjs +54 -0
  30. package/lib/build/resolve.mjs +101 -0
  31. package/lib/build/server.mjs +142 -0
  32. package/lib/build/static.mjs +89 -0
  33. package/lib/dev/action.mjs +63 -0
  34. package/lib/dev/create-logger.mjs +52 -0
  35. package/lib/dev/create-server.mjs +208 -0
  36. package/lib/dev/modules.mjs +20 -0
  37. package/lib/dev/ssr-handler.mjs +135 -0
  38. package/lib/handlers/error.mjs +153 -0
  39. package/lib/handlers/not-found.mjs +5 -0
  40. package/lib/handlers/redirect.mjs +1 -0
  41. package/lib/handlers/rewrite.mjs +1 -0
  42. package/lib/handlers/static.mjs +120 -0
  43. package/lib/handlers/trailing-slash.mjs +12 -0
  44. package/lib/plugins/react-server.mjs +73 -0
  45. package/lib/plugins/use-client.mjs +135 -0
  46. package/lib/plugins/use-server.mjs +175 -0
  47. package/lib/start/action.mjs +110 -0
  48. package/lib/start/create-server.mjs +111 -0
  49. package/lib/start/manifest.mjs +104 -0
  50. package/lib/start/ssr-handler.mjs +134 -0
  51. package/lib/sys.mjs +49 -0
  52. package/lib/utils/merge.mjs +31 -0
  53. package/lib/utils/server-address.mjs +14 -0
  54. package/memory-cache/index.mjs +125 -0
  55. package/package.json +81 -0
  56. package/react-server.d.ts +209 -0
  57. package/server/ErrorBoundary.jsx +14 -0
  58. package/server/RemoteComponent.jsx +210 -0
  59. package/server/Route.jsx +108 -0
  60. package/server/actions.mjs +72 -0
  61. package/server/cache.mjs +19 -0
  62. package/server/client-component.mjs +62 -0
  63. package/server/context.mjs +32 -0
  64. package/server/cookies.mjs +14 -0
  65. package/server/entry.server.jsx +972 -0
  66. package/server/error-boundary.jsx +2 -0
  67. package/server/http-headers.mjs +8 -0
  68. package/server/http-status.mjs +6 -0
  69. package/server/index.mjs +14 -0
  70. package/server/logger.mjs +15 -0
  71. package/server/module-loader.mjs +20 -0
  72. package/server/redirects.mjs +45 -0
  73. package/server/remote-component.jsx +2 -0
  74. package/server/request.mjs +37 -0
  75. package/server/revalidate.mjs +22 -0
  76. package/server/rewrites.mjs +0 -0
  77. package/server/router.jsx +6 -0
  78. package/server/runtime.mjs +32 -0
  79. package/server/symbols.mjs +24 -0
@@ -0,0 +1,208 @@
1
+ import { createRequire } from "node:module";
2
+ import { join } from "node:path";
3
+
4
+ import { createMiddleware } from "@hattip/adapter-node";
5
+ import { compose } from "@hattip/compose";
6
+ import { cookie } from "@hattip/cookie";
7
+ import { cors } from "@hattip/cors";
8
+ import { parseMultipartFormData } from "@hattip/multipart";
9
+ import react from "@vitejs/plugin-react";
10
+ import { createServer as createViteDevServer } from "vite";
11
+
12
+ import { MemoryCache } from "../../memory-cache/index.mjs";
13
+ import packageJson from "../../package.json" assert { type: "json" };
14
+ import { getRuntime, runtime$ } from "../../server/runtime.mjs";
15
+ import {
16
+ COLLECT_STYLESHEETS,
17
+ CONFIG_CONTEXT,
18
+ CONFIG_ROOT,
19
+ FORM_DATA_PARSER,
20
+ LOGGER_CONTEXT,
21
+ MEMORY_CACHE_CONTEXT,
22
+ MODULE_LOADER,
23
+ SERVER_CONTEXT,
24
+ } from "../../server/symbols.mjs";
25
+ import { clientAlias } from "../build/resolve.mjs";
26
+ import notFoundHandler from "../handlers/not-found.mjs";
27
+ import staticHandler from "../handlers/static.mjs";
28
+ import trailingSlashHandler from "../handlers/trailing-slash.mjs";
29
+ import reactServer from "../plugins/react-server.mjs";
30
+ import useClient from "../plugins/use-client.mjs";
31
+ import useServer from "../plugins/use-server.mjs";
32
+ import * as sys from "../sys.mjs";
33
+ import merge from "../utils/merge.mjs";
34
+ import createLogger from "./create-logger.mjs";
35
+ import ssrHandler from "./ssr-handler.mjs";
36
+
37
+ const __require = createRequire(import.meta.url);
38
+ const packageName = packageJson.name;
39
+ const cwd = sys.cwd();
40
+
41
+ export default async function createServer(root, options) {
42
+ const config = getRuntime(CONFIG_CONTEXT)?.[CONFIG_ROOT];
43
+ let reactServerRouterModule;
44
+ try {
45
+ reactServerRouterModule = __require.resolve("@lazarv/react-server-router", {
46
+ paths: [cwd],
47
+ });
48
+ } catch (e) {
49
+ // ignore
50
+ }
51
+
52
+ const devServerConfig = {
53
+ ...config,
54
+ server: {
55
+ ...config.server,
56
+ middlewareMode: true,
57
+ cors: options.cors ?? config.server?.cors,
58
+ hmr: {
59
+ port: 21678 + parseInt(options.port ?? config.server?.port ?? 0),
60
+ ...config.server?.hmr,
61
+ },
62
+ https: options.https ?? config.server?.https,
63
+ fs: {
64
+ ...config.server?.fs,
65
+ allow: [cwd, ...(config.server?.fs?.allow ?? [])],
66
+ },
67
+ },
68
+ publicDir: false,
69
+ root: __require.resolve(`${packageName}`),
70
+ appType: "ssr",
71
+ clearScreen: options.clearScreen,
72
+ configFile: false,
73
+ plugins: [
74
+ reactServer(),
75
+ ...(reactServerRouterModule &&
76
+ (!root || root === "@lazarv/react-server-router")
77
+ ? [
78
+ (async () =>
79
+ (
80
+ await import(
81
+ __require.resolve("@lazarv/react-server-router/plugin", {
82
+ paths: [cwd],
83
+ })
84
+ )
85
+ ).default())(),
86
+ ]
87
+ : []),
88
+ useClient(),
89
+ useServer(),
90
+ react(),
91
+ ...(config.plugins ?? []),
92
+ ],
93
+ resolve: {
94
+ ...config.resolve,
95
+ alias: [...clientAlias(true), ...(config.resolve?.alias ?? [])],
96
+ },
97
+ optimizeDeps: {
98
+ ...config.optimizeDeps,
99
+ include: [
100
+ "react",
101
+ "react-dom",
102
+ "react-dom/client",
103
+ "react-server-dom-webpack/client.browser",
104
+ "react-error-boundary",
105
+ ...(config.optimizeDeps?.include ?? []).map((m) =>
106
+ __require.resolve(m, { paths: [cwd] })
107
+ ),
108
+ ],
109
+ exclude: [
110
+ ...(config.optimizeDeps?.exclude ?? []).map((m) =>
111
+ __require.resolve(m, { paths: [cwd] })
112
+ ),
113
+ ],
114
+ force: options.force ?? config.optimizeDeps?.force,
115
+ },
116
+ css: {
117
+ ...config.css,
118
+ postcss: cwd,
119
+ },
120
+ customLogger: createLogger(),
121
+ };
122
+
123
+ const viteDevServer = await createViteDevServer(
124
+ typeof config.vite === "function"
125
+ ? config.vite(devServerConfig) ?? devServerConfig
126
+ : merge(devServerConfig, config.vite)
127
+ );
128
+
129
+ const initialRuntime = {
130
+ [SERVER_CONTEXT]: viteDevServer,
131
+ [LOGGER_CONTEXT]: viteDevServer.config.logger,
132
+ [MODULE_LOADER]: (id) => viteDevServer.ssrLoadModule(id.split("::")[0]),
133
+ [FORM_DATA_PARSER]: parseMultipartFormData,
134
+ [MEMORY_CACHE_CONTEXT]: new MemoryCache(),
135
+ [COLLECT_STYLESHEETS]: function collectCss(rootModule) {
136
+ const styles = [];
137
+ const visited = new Set();
138
+ function collectCss(moduleId) {
139
+ if (
140
+ moduleId &&
141
+ !visited.has(moduleId) &&
142
+ !moduleId.startsWith("virtual:")
143
+ ) {
144
+ visited.add(moduleId);
145
+ const mod = viteDevServer.moduleGraph.getModuleById(moduleId);
146
+ const values = Array.from(mod.importedModules.values());
147
+ const importedStyles = values.filter(
148
+ (mod) => /\.(css|scss|less)/.test(mod.id) && !styles.includes(mod)
149
+ );
150
+ const imports = values.filter(
151
+ (mod) => !/\.(css|scss|less)/.test(mod.id)
152
+ );
153
+
154
+ styles.push(...importedStyles.map((mod) => mod.url));
155
+ imports.forEach((mod) => mod.id && collectCss(mod.id));
156
+ }
157
+ }
158
+ collectCss(rootModule);
159
+ return styles;
160
+ },
161
+ };
162
+
163
+ runtime$(
164
+ typeof config.runtime === "function"
165
+ ? config.runtime(initialRuntime) ?? initialRuntime
166
+ : {
167
+ ...initialRuntime,
168
+ ...config.runtime,
169
+ }
170
+ );
171
+
172
+ const publicDir =
173
+ typeof config.publicDir === "string" ? config.publicDir : "public";
174
+ const initialHandlers = [
175
+ ...(config.publicDir !== false
176
+ ? [
177
+ await staticHandler(join(cwd, publicDir), {
178
+ cwd: publicDir,
179
+ }),
180
+ ]
181
+ : []),
182
+ await trailingSlashHandler(),
183
+ cookie(config.cookies),
184
+ ...(config.handlers?.pre ?? []),
185
+ await ssrHandler(root),
186
+ ...(config.handlers?.post ?? []),
187
+ await notFoundHandler(),
188
+ ];
189
+ if (options.cors) {
190
+ initialHandlers.unshift(cors());
191
+ }
192
+
193
+ viteDevServer.middlewares.use(
194
+ createMiddleware(
195
+ compose(
196
+ typeof config.handlers === "function"
197
+ ? config.handlers(initialHandlers) ?? initialHandlers
198
+ : [...initialHandlers, ...(config.handlers ?? [])]
199
+ )
200
+ )
201
+ );
202
+
203
+ return {
204
+ listen: (...args) => viteDevServer.middlewares.listen(...args),
205
+ close: () => viteDevServer.close(),
206
+ ws: viteDevServer.ws,
207
+ };
208
+ }
@@ -0,0 +1,20 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ import packageJson from "../../package.json" assert { type: "json" };
4
+ import { cwd } from "../sys.mjs";
5
+
6
+ const __require = createRequire(import.meta.url);
7
+
8
+ export default function getModules(root) {
9
+ const entryModule = __require.resolve(
10
+ `${packageJson.name}/server/entry.server.jsx`
11
+ );
12
+ const rootModule = __require.resolve(root ?? "@lazarv/react-server-router", {
13
+ paths: [cwd()],
14
+ });
15
+ const memoryCacheModule = __require.resolve(
16
+ `${packageJson.name}/memory-cache`
17
+ );
18
+
19
+ return { entryModule, rootModule, memoryCacheModule };
20
+ }
@@ -0,0 +1,135 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ import { forChild } from "../../config/index.mjs";
4
+ import packageJson from "../../package.json" assert { type: "json" };
5
+ import { context$, ContextStorage, getContext } from "../../server/context.mjs";
6
+ import { init$ as module_loader_init$ } from "../../server/module-loader.mjs";
7
+ import { getRuntime } from "../../server/runtime.mjs";
8
+ import {
9
+ COLLECT_STYLESHEETS,
10
+ CONFIG_CONTEXT,
11
+ ERROR_CONTEXT,
12
+ FORM_DATA_PARSER,
13
+ HTTP_CONTEXT,
14
+ LOGGER_CONTEXT,
15
+ MAIN_MODULE,
16
+ MEMORY_CACHE_CONTEXT,
17
+ MODULE_LOADER,
18
+ REDIRECT_CONTEXT,
19
+ SERVER_CONTEXT,
20
+ STYLES_CONTEXT,
21
+ } from "../../server/symbols.mjs";
22
+ import errorHandler from "../handlers/error.mjs";
23
+ import getModules from "./modules.mjs";
24
+
25
+ const __require = createRequire(import.meta.url);
26
+
27
+ export default async function ssrHandler(root) {
28
+ const { entryModule, rootModule, memoryCacheModule } = getModules(root);
29
+ const viteDevServer = getRuntime(SERVER_CONTEXT);
30
+ const ssrLoadModule = getRuntime(MODULE_LOADER);
31
+ const { default: React } = await import("react");
32
+ const logger = getRuntime(LOGGER_CONTEXT);
33
+ const config = getRuntime(CONFIG_CONTEXT);
34
+
35
+ return async (httpContext) => {
36
+ return new Promise((resolve) => {
37
+ try {
38
+ ContextStorage.run(
39
+ {
40
+ [SERVER_CONTEXT]: viteDevServer,
41
+ [HTTP_CONTEXT]: httpContext,
42
+ [CONFIG_CONTEXT]: config,
43
+ [ERROR_CONTEXT]: errorHandler,
44
+ [MODULE_LOADER]: ssrLoadModule,
45
+ [LOGGER_CONTEXT]: logger,
46
+ [MAIN_MODULE]: [
47
+ "/@vite/client",
48
+ `/${packageJson.name}/client/hmr.mjs`,
49
+ ],
50
+ [FORM_DATA_PARSER]: getRuntime(FORM_DATA_PARSER),
51
+ [MEMORY_CACHE_CONTEXT]: getRuntime(MEMORY_CACHE_CONTEXT),
52
+ [REDIRECT_CONTEXT]: {},
53
+ [COLLECT_STYLESHEETS]: getRuntime(COLLECT_STYLESHEETS),
54
+ },
55
+ async () => {
56
+ try {
57
+ // clear up server context registry object
58
+ Reflect.ownKeys(
59
+ React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
60
+ .ContextRegistry
61
+ ).forEach((key) =>
62
+ Reflect.deleteProperty(
63
+ React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
64
+ .ContextRegistry,
65
+ key
66
+ )
67
+ );
68
+
69
+ const cacheModule = forChild(httpContext.url)?.cache?.module;
70
+
71
+ const [
72
+ { render },
73
+ { default: Component, init$: root_init$ },
74
+ { init$: cache_init$ },
75
+ ] = await Promise.all([
76
+ ssrLoadModule(entryModule),
77
+ ssrLoadModule(rootModule),
78
+ import(
79
+ cacheModule
80
+ ? __require.resolve(cacheModule, {
81
+ paths: [process.cwd()],
82
+ })
83
+ : memoryCacheModule
84
+ ),
85
+ ]);
86
+
87
+ await cache_init$?.();
88
+ try {
89
+ const middlewares = await root_init$?.();
90
+ if (middlewares) {
91
+ const response = await middlewares(httpContext);
92
+ if (response) {
93
+ return resolve(response);
94
+ }
95
+ }
96
+ } catch (e) {
97
+ const redirect = getContext(REDIRECT_CONTEXT);
98
+ if (redirect?.response) {
99
+ return resolve(redirect.response);
100
+ } else {
101
+ throw e;
102
+ }
103
+ }
104
+
105
+ const accept = httpContext.request.headers.get("accept");
106
+ if (
107
+ !accept ||
108
+ !(
109
+ accept.includes("text/html") ||
110
+ accept.includes("text/x-component") ||
111
+ accept.includes("application/json")
112
+ )
113
+ ) {
114
+ return resolve();
115
+ }
116
+
117
+ const styles =
118
+ getRuntime(COLLECT_STYLESHEETS)?.(rootModule) ?? [];
119
+ context$(STYLES_CONTEXT, styles);
120
+
121
+ await module_loader_init$?.(ssrLoadModule);
122
+ return resolve(render(Component));
123
+ } catch (e) {
124
+ logger.error(e);
125
+ return resolve(await getContext(ERROR_CONTEXT)?.(e));
126
+ }
127
+ }
128
+ );
129
+ } catch (e) {
130
+ logger.error(e);
131
+ return resolve(errorHandler(e));
132
+ }
133
+ });
134
+ };
135
+ }
@@ -0,0 +1,153 @@
1
+ import strip from "strip-ansi";
2
+
3
+ import packageJson from "../../package.json" assert { type: "json" };
4
+ import { getContext } from "../../server/context.mjs";
5
+ import {
6
+ HTTP_CONTEXT,
7
+ HTTP_HEADERS,
8
+ HTTP_STATUS,
9
+ SERVER_CONTEXT,
10
+ } from "../../server/symbols.mjs";
11
+
12
+ function cleanStack(stack) {
13
+ return stack
14
+ .split(/\n/g)
15
+ .filter((l) => /^\s*at/.test(l))
16
+ .map((l) => l.trim())
17
+ .join("\n");
18
+ }
19
+
20
+ export function prepareError(err) {
21
+ try {
22
+ if (!err.id) {
23
+ const viteDevServer = getContext(SERVER_CONTEXT);
24
+ viteDevServer.ssrFixStacktrace(err);
25
+ const [id, line, column] =
26
+ err.stack
27
+ .split("\n")[1]
28
+ .match(/\((.*:[0-9]+:[0-9]+)\)/)?.[1]
29
+ .split(":") ?? [];
30
+
31
+ if (viteDevServer.moduleGraph.idToModuleMap.has(id)) {
32
+ const {
33
+ ssrTransformResult: { map },
34
+ } = viteDevServer.moduleGraph.getModuleById(id);
35
+
36
+ err.id = id;
37
+ if (!err.loc) {
38
+ err.loc = {
39
+ file: id,
40
+ column: parseInt(column, 10),
41
+ line: parseInt(line, 10),
42
+ length: 0,
43
+ lineText: "",
44
+ namespace: "",
45
+ suggestion: "",
46
+ };
47
+ }
48
+
49
+ if (!err.frame) {
50
+ const lines = map.sourcesContent[0].split("\n");
51
+ const start = Math.max(0, err.loc.line - 3);
52
+ const end = Math.min(lines.length, err.loc.line + 3);
53
+ const frame = lines
54
+ .slice(start, end)
55
+ .flatMap((l, i) => {
56
+ const curr = i + start;
57
+ const indent = " ".repeat(
58
+ Math.max(start, end).toString().length - curr.toString().length
59
+ );
60
+ return [
61
+ `${indent}${curr} | ${l}`,
62
+ ...(i === 2
63
+ ? [
64
+ `${indent}${curr
65
+ .toString()
66
+ .replaceAll(/./g, " ")} |${" ".repeat(
67
+ err.loc.column
68
+ )}^`,
69
+ ]
70
+ : []),
71
+ ];
72
+ })
73
+ .join("\n");
74
+ err.frame = `${err.message}\n${frame}\n`;
75
+ }
76
+ }
77
+
78
+ err.plugin = err.plugin || packageJson.name;
79
+ }
80
+ } catch (e) {
81
+ console.error(e);
82
+ }
83
+ return {
84
+ message: strip(err.message),
85
+ stack: strip(cleanStack(err.stack || "")),
86
+ id: err.id,
87
+ frame: strip(err.frame || ""),
88
+ plugin: err.plugin,
89
+ pluginCode: err.pluginCode?.toString(),
90
+ loc: err.loc,
91
+ };
92
+ }
93
+
94
+ function plainResponse(e) {
95
+ const httpStatus = getContext(HTTP_STATUS) ?? {
96
+ status: 500,
97
+ statusText: "Internal Server Error",
98
+ };
99
+ return new Response(e?.stack ?? null, {
100
+ ...httpStatus,
101
+ headers: {
102
+ "Content-Type": "text/plain",
103
+ ...(getContext(HTTP_HEADERS) ?? {}),
104
+ },
105
+ });
106
+ }
107
+
108
+ export default async function errorHandler(err) {
109
+ try {
110
+ const server = getContext(SERVER_CONTEXT);
111
+ // TODO: is there a better way to check if this is a vite dev server?
112
+ if (typeof server?.ssrFixStacktrace !== "function") {
113
+ return plainResponse(err);
114
+ }
115
+
116
+ const accept = getContext(HTTP_CONTEXT)?.request?.headers?.get?.("accept");
117
+ if (accept?.includes?.(";standalone")) {
118
+ return plainResponse(err);
119
+ }
120
+
121
+ const httpStatus = getContext(HTTP_STATUS) ?? {
122
+ status: 500,
123
+ statusText: "Internal Server Error",
124
+ };
125
+
126
+ return new Response(
127
+ `<!DOCTYPE html>
128
+ <html lang="en">
129
+ <head>
130
+ <meta charset="UTF-8" />
131
+ <title>Error</title>
132
+ <script type="module">
133
+ import { ErrorOverlay } from '/@vite/client'
134
+ document.body.appendChild(new ErrorOverlay(${JSON.stringify(
135
+ await prepareError(err)
136
+ ).replace(/</g, "\\u003c")}))
137
+ </script>
138
+ </head>
139
+ <body>
140
+ </body>
141
+ </html>`,
142
+ {
143
+ ...httpStatus,
144
+ headers: {
145
+ "Content-Type": "text/html",
146
+ ...(getContext(HTTP_HEADERS) ?? {}),
147
+ },
148
+ }
149
+ );
150
+ } catch (e) {
151
+ return plainResponse(e);
152
+ }
153
+ }
@@ -0,0 +1,5 @@
1
+ export default async function notFoundHandler() {
2
+ return async () => {
3
+ return new Response("Not Found", { status: 404 });
4
+ };
5
+ }
@@ -0,0 +1 @@
1
+ export default () => {};
@@ -0,0 +1 @@
1
+ export default () => {};
@@ -0,0 +1,120 @@
1
+ import { open, readFile } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+
4
+ import glob from "fast-glob";
5
+ import mime from "mime";
6
+
7
+ export default async function staticHandler(dir, options = {}) {
8
+ const files = (
9
+ await glob(`${dir}/**/*`, {
10
+ cwd: options.cwd,
11
+ stats: true,
12
+ absolute: true,
13
+ })
14
+ ).reduce((files, file) => {
15
+ files.set(
16
+ `/${relative(join(process.cwd(), options.cwd ?? "."), file.path)}`,
17
+ {
18
+ ...file,
19
+ etag: `W/"${file.stats.size}-${file.stats.mtime.getTime()}"`,
20
+ mime:
21
+ mime.getType(file.path.replace(/\.(br|gz)$/, "")) ||
22
+ "application/octet-stream",
23
+ }
24
+ );
25
+ return files;
26
+ }, new Map());
27
+ const fileCache = new Map();
28
+
29
+ return async (context) => {
30
+ let { pathname } = context.url;
31
+
32
+ if (pathname.startsWith("/@source")) {
33
+ return new Response(await readFile(pathname.slice(8), "utf8"), {
34
+ headers: {
35
+ "content-type": "text/plain",
36
+ },
37
+ });
38
+ }
39
+
40
+ const accept = context.request.headers.get("accept");
41
+ const isHTML = accept?.includes("text/html");
42
+ const isRSC = accept?.includes("text/x-component");
43
+ let contentEncoding = undefined;
44
+
45
+ if (isHTML) {
46
+ const acceptEncoding = context.request.headers.get("accept-encoding");
47
+ const isBrotli = acceptEncoding?.includes("br");
48
+ const isGzip = acceptEncoding?.includes("gzip");
49
+
50
+ if (isBrotli && files.has(`${pathname}/index.html.br`)) {
51
+ pathname = `${pathname}/index.html.br`;
52
+ contentEncoding = "br";
53
+ } else if (isGzip && files.has(`${pathname}/index.html.gz`)) {
54
+ pathname = `${pathname}/index.html.gz`;
55
+ contentEncoding = "gzip";
56
+ } else if (files.has(`${pathname}/index.html`)) {
57
+ pathname = `${pathname}/index.html`;
58
+ }
59
+ }
60
+
61
+ if (pathname !== "/" && files.has(pathname)) {
62
+ try {
63
+ const file = files.get(pathname);
64
+ if (
65
+ context.request.headers.get("if-none-match") === file.etag &&
66
+ !isRSC
67
+ ) {
68
+ return new Response(null, {
69
+ status: 304,
70
+ });
71
+ }
72
+ let res = null;
73
+ if (!fileCache.has(pathname)) {
74
+ const fd = await open(file.path, "r");
75
+ try {
76
+ const fs = fd.createReadStream();
77
+ res = new ReadableStream({
78
+ type: "bytes",
79
+ async start(controller) {
80
+ const payload = [];
81
+ for await (const chunk of fs) {
82
+ payload.push(Buffer.copyBytesFrom(chunk));
83
+ controller.enqueue(chunk);
84
+ }
85
+ fileCache.set(pathname, Buffer.concat(payload));
86
+ controller.close();
87
+ },
88
+ });
89
+ } catch (e) {
90
+ fd.close();
91
+ return;
92
+ }
93
+ } else {
94
+ res = fileCache.get(pathname);
95
+ }
96
+ if (res) {
97
+ return new Response(res, {
98
+ headers: {
99
+ "content-type": file.mime,
100
+ "content-length": file.stats.size,
101
+ etag: file.etag,
102
+ "cache-control":
103
+ context.request.headers.get("cache-control") === "no-cache"
104
+ ? "no-cache"
105
+ : isHTML
106
+ ? "must-revalidate"
107
+ : "public,max-age=600",
108
+ "last-modified": file.stats.mtime.toUTCString(),
109
+ "content-encoding": contentEncoding,
110
+ },
111
+ });
112
+ }
113
+ } catch (error) {
114
+ if (error.code !== "ENOENT") {
115
+ throw error;
116
+ }
117
+ }
118
+ }
119
+ };
120
+ }
@@ -0,0 +1,12 @@
1
+ export default async function trailingSlash() {
2
+ return async ({ url: { pathname } }) => {
3
+ if (pathname !== "/" && pathname.endsWith("/")) {
4
+ return new Response(null, {
5
+ status: 301,
6
+ headers: {
7
+ Location: pathname.replace(/\/+$/g, "") || "/",
8
+ },
9
+ });
10
+ }
11
+ };
12
+ }