@alignable/bifrost-fastify 0.0.2 → 0.0.4

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.
@@ -0,0 +1,19 @@
1
+ import { FastifyPluginAsync, FastifyReply, RawServerBase } from 'fastify';
2
+ import { IncomingMessage, IncomingHttpHeaders as IncomingHttpHeaders$1 } from 'http';
3
+ import { Http2ServerRequest, IncomingHttpHeaders } from 'http2';
4
+
5
+ interface ViteProxyPluginOptions {
6
+ upstream: URL;
7
+ host: URL;
8
+ getLayout: (reply: FastifyReply<RawServerBase>) => {
9
+ layout: string;
10
+ layoutProps: any;
11
+ };
12
+ rewriteRequestHeaders?: (req: Http2ServerRequest | IncomingMessage, headers: IncomingHttpHeaders | IncomingHttpHeaders$1) => IncomingHttpHeaders | IncomingHttpHeaders$1;
13
+ }
14
+ /**
15
+ * Fastify plugin that wraps @fasitfy/http-proxy to proxy Rails/Turbolinks server into a Vite-Plugin-SSR site.
16
+ */
17
+ declare const viteProxyPlugin: FastifyPluginAsync<ViteProxyPluginOptions>;
18
+
19
+ export { viteProxyPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,132 @@
1
+ // index.ts
2
+ import proxy from "@fastify/http-proxy";
3
+ import accepts from "@fastify/accepts";
4
+ import forwarded from "@fastify/forwarded";
5
+ import jsdom from "jsdom";
6
+ import { renderPage } from "vite-plugin-ssr/server";
7
+ function streamToString(stream) {
8
+ const chunks = [];
9
+ return new Promise((resolve, reject) => {
10
+ stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
11
+ stream.on("error", (err) => reject(err));
12
+ stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
13
+ });
14
+ }
15
+ async function replyWithPage(reply, pageContext) {
16
+ const { httpResponse } = pageContext;
17
+ if (!httpResponse) {
18
+ return reply.code(404).type("text/html").send("Not Found");
19
+ }
20
+ const { body, statusCode, contentType } = httpResponse;
21
+ return reply.status(statusCode).type(contentType).send(body);
22
+ }
23
+ var proxyPageId = "/proxy/pages";
24
+ var viteProxyPlugin = async (fastify, { upstream, host, rewriteRequestHeaders, getLayout }) => {
25
+ await fastify.register(accepts);
26
+ await fastify.register(proxy, {
27
+ upstream: upstream.href,
28
+ async preHandler(req, reply) {
29
+ if (req.method === "GET" && req.accepts().type(["html"]) === "html") {
30
+ const pageContextInit = {
31
+ urlOriginal: req.url
32
+ };
33
+ const pageContext = await renderPage(pageContextInit);
34
+ const proxy2 = pageContext._pageId === proxyPageId;
35
+ if (!proxy2) {
36
+ return replyWithPage(reply, pageContext);
37
+ } else {
38
+ req._proxy = {
39
+ isPageContext: req.raw.url.includes("/index.pageContext.json"),
40
+ originalUrl: req.raw.url
41
+ };
42
+ req.raw.url = req.raw.url.replace("/index.pageContext.json", "");
43
+ }
44
+ }
45
+ },
46
+ replyOptions: {
47
+ rewriteRequestHeaders(request, headers) {
48
+ delete headers["if-modified-since"];
49
+ delete headers["if-none-match"];
50
+ delete headers["if-unmodified-since"];
51
+ delete headers["if-none-match"];
52
+ delete headers["if-range"];
53
+ const fwd = forwarded(request).reverse();
54
+ headers["X-Forwarded-For"] = fwd.join(", ");
55
+ headers["X-Forwarded-Host"] = host.host;
56
+ headers["X-Forwarded-Protocol"] = host.protocol;
57
+ if (rewriteRequestHeaders) {
58
+ return rewriteRequestHeaders(request, headers);
59
+ }
60
+ return headers;
61
+ },
62
+ async onResponse(req, reply, res) {
63
+ const { isPageContext = false, originalUrl = void 0 } = req._proxy || {};
64
+ if (isPageContext && originalUrl) {
65
+ req.raw.url = originalUrl;
66
+ }
67
+ if ([301, 302, 303, 307, 308].includes(reply.statusCode)) {
68
+ const location = reply.getHeader("location");
69
+ if (location) {
70
+ const url = new URL(location);
71
+ if (url.host === upstream.host) {
72
+ url.host = host.host;
73
+ }
74
+ reply.header("location", url);
75
+ if (isPageContext) {
76
+ reply.status(200).type("application/json").send(
77
+ JSON.stringify({
78
+ pageContext: {
79
+ // A bit hacky, but we manually construct the VPS pageContext here
80
+ _pageId: proxyPageId,
81
+ redirectTo: url
82
+ }
83
+ })
84
+ );
85
+ } else {
86
+ return reply.send(res);
87
+ }
88
+ }
89
+ }
90
+ const { layout, layoutProps } = getLayout(reply);
91
+ if (!layout) {
92
+ return reply.send(res);
93
+ }
94
+ const html = await streamToString(res);
95
+ const dom = new jsdom.JSDOM(html);
96
+ const doc = dom.window.document;
97
+ const bodyEl = doc.querySelector("body");
98
+ const head = doc.querySelector("head");
99
+ if (!bodyEl || !head) {
100
+ return reply.code(404).type("text/html").send("proxy failed");
101
+ }
102
+ bodyEl.querySelectorAll("a[rel='external']").forEach((e) => e.setAttribute("data-turbolinks", "false"));
103
+ bodyEl.querySelectorAll("a").forEach((e) => e.rel = "external");
104
+ const bodyAttrs = {};
105
+ bodyEl.getAttributeNames().forEach((name) => {
106
+ bodyAttrs[name] = bodyEl.getAttribute(name);
107
+ });
108
+ const proxy2 = {
109
+ body: bodyEl.innerHTML,
110
+ head: head.innerHTML,
111
+ bodyAttrs
112
+ };
113
+ const pageContextInit = {
114
+ urlOriginal: req.url,
115
+ layout,
116
+ layoutProps
117
+ };
118
+ if (isPageContext) {
119
+ Object.assign(pageContextInit, { proxySendClient: proxy2 });
120
+ } else {
121
+ Object.assign(pageContextInit, { proxy: proxy2 });
122
+ }
123
+ const pageContext = await renderPage(pageContextInit);
124
+ return replyWithPage(reply, pageContext);
125
+ }
126
+ }
127
+ });
128
+ };
129
+ export {
130
+ viteProxyPlugin
131
+ };
132
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../index.ts"],"sourcesContent":["// Note that this file isn't processed by Vite, see https://github.com/brillout/vite-plugin-ssr/issues/562\nimport { FastifyReply, RawServerBase, FastifyPluginAsync } from \"fastify\";\nimport { FastifyRequest, RequestGenericInterface } from \"fastify/types/request\";\nimport proxy from \"@fastify/http-proxy\";\nimport accepts from \"@fastify/accepts\";\nimport { type PageContextProxy, type Proxy } from \"../bifrost/types/internal.js\";\nimport forwarded from \"@fastify/forwarded\";\nimport { Writable } from \"stream\";\nimport jsdom from \"jsdom\";\nimport { IncomingHttpHeaders, IncomingMessage } from \"http\";\nimport {\n Http2ServerRequest,\n IncomingHttpHeaders as Http2IncomingHttpHeaders,\n} from \"http2\";\nimport { renderPage } from \"vite-plugin-ssr/server\";\n\ntype RequestExtendedWithProxy = FastifyRequest<\n RequestGenericInterface,\n RawServerBase\n> & { _proxy?: { isPageContext: boolean; originalUrl?: string } };\n\nfunction streamToString(stream: Writable): Promise<string> {\n const chunks: Buffer[] = [];\n return new Promise((resolve, reject) => {\n stream.on(\"data\", (chunk) => chunks.push(Buffer.from(chunk)));\n stream.on(\"error\", (err) => reject(err));\n stream.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n });\n}\n\nasync function replyWithPage(\n reply: FastifyReply<RawServerBase>,\n pageContext: Awaited<ReturnType<typeof renderPage>>\n): Promise<FastifyReply> {\n const { httpResponse } = pageContext;\n\n if (!httpResponse) {\n return reply.code(404).type(\"text/html\").send(\"Not Found\");\n }\n\n const { body, statusCode, contentType } = httpResponse;\n\n return reply.status(statusCode).type(contentType).send(body);\n}\n\nconst proxyPageId = \"/proxy/pages\";\n\ninterface ViteProxyPluginOptions {\n upstream: URL;\n host: URL;\n getLayout: (reply: FastifyReply<RawServerBase>) => {\n layout: string;\n layoutProps: any;\n };\n /// Use to signal to legacy backend to return special results (eg. remove navbar etc)\n rewriteRequestHeaders?: (\n req: Http2ServerRequest | IncomingMessage,\n headers: Http2IncomingHttpHeaders | IncomingHttpHeaders\n ) => Http2IncomingHttpHeaders | IncomingHttpHeaders;\n}\n/**\n * Fastify plugin that wraps @fasitfy/http-proxy to proxy Rails/Turbolinks server into a Vite-Plugin-SSR site.\n */\nexport const viteProxyPlugin: FastifyPluginAsync<\n ViteProxyPluginOptions\n> = async (fastify, { upstream, host, rewriteRequestHeaders, getLayout }) => {\n await fastify.register(accepts);\n await fastify.register(proxy, {\n upstream: upstream.href,\n async preHandler(req, reply) {\n if (req.method === \"GET\" && req.accepts().type([\"html\"]) === \"html\") {\n const pageContextInit = {\n urlOriginal: req.url,\n };\n\n const pageContext = await renderPage<\n { _pageId: string },\n typeof pageContextInit\n >(pageContextInit);\n\n const proxy = pageContext._pageId === proxyPageId;\n\n if (!proxy) {\n return replyWithPage(reply, pageContext);\n } else {\n // pageContext.json is added on client navigations to indicate we are returning just json for the client router\n // we have to remove it before proxying though.\n (req as RequestExtendedWithProxy)._proxy = {\n isPageContext: req.raw.url!.includes(\"/index.pageContext.json\"),\n originalUrl: req.raw.url,\n };\n req.raw.url = req.raw.url!.replace(\"/index.pageContext.json\", \"\");\n }\n }\n },\n replyOptions: {\n rewriteRequestHeaders(request, headers) {\n // Delete cache headers\n delete headers[\"if-modified-since\"];\n delete headers[\"if-none-match\"];\n delete headers[\"if-unmodified-since\"];\n delete headers[\"if-none-match\"];\n delete headers[\"if-range\"];\n\n const fwd = forwarded(request as IncomingMessage).reverse();\n // fwd.push(request.ip); TODO: not sure if this is needed\n headers[\"X-Forwarded-For\"] = fwd.join(\", \");\n headers[\"X-Forwarded-Host\"] = host.host;\n headers[\"X-Forwarded-Protocol\"] = host.protocol;\n if (rewriteRequestHeaders) {\n return rewriteRequestHeaders(request, headers);\n }\n return headers;\n },\n async onResponse(req, reply: FastifyReply<RawServerBase>, res) {\n const { isPageContext = false, originalUrl = undefined } =\n (req as RequestExtendedWithProxy)._proxy || {};\n if (isPageContext && originalUrl) {\n // restore url rewrite\n req.raw.url = originalUrl;\n }\n\n if ([301, 302, 303, 307, 308].includes(reply.statusCode)) {\n const location = reply.getHeader(\"location\") as string;\n if (location) {\n const url = new URL(location);\n if (url.host === upstream.host) {\n // rewrite redirect on upstream's host to the proxy host\n url.host = host.host;\n }\n reply.header(\"location\", url);\n if (isPageContext) {\n reply\n .status(200)\n .type(\"application/json\")\n .send(\n JSON.stringify({\n pageContext: {\n // A bit hacky, but we manually construct the VPS pageContext here\n _pageId: proxyPageId,\n redirectTo: url,\n },\n })\n );\n } else {\n return reply.send(res);\n }\n }\n }\n\n const { layout, layoutProps } = getLayout(reply);\n if (!layout) {\n return reply.send(res);\n }\n\n const html = await streamToString(res);\n const dom = new jsdom.JSDOM(html);\n const doc = dom.window.document;\n const bodyEl = doc.querySelector(\"body\");\n const head = doc.querySelector(\"head\");\n\n if (!bodyEl || !head) {\n return reply.code(404).type(\"text/html\").send(\"proxy failed\");\n }\n\n // disable vite-plugin-ssr link interceptor. May not be neccessary in future:\n // https://github.com/brillout/vite-plugin-ssr/discussions/728#discussioncomment-5634111\n bodyEl\n .querySelectorAll(\"a[rel='external']\")\n .forEach((e) => e.setAttribute(\"data-turbolinks\", \"false\"));\n bodyEl.querySelectorAll(\"a\").forEach((e) => (e.rel = \"external\"));\n\n const bodyAttrs: Record<string, string> = {};\n bodyEl.getAttributeNames().forEach((name) => {\n bodyAttrs[name] = bodyEl.getAttribute(name)!;\n });\n\n const proxy: Proxy = {\n body: bodyEl.innerHTML,\n head: head.innerHTML,\n bodyAttrs,\n };\n\n const pageContextInit: Partial<PageContextProxy> = {\n urlOriginal: req.url,\n layout,\n layoutProps,\n };\n // proxySendClient is serialized and sent to client on subsequent navigation. proxy is ONLY included server-side to avoid doubling page size\n if (isPageContext) { //TODO: send whole string instead of parsing and let browser do the parsing (turbolinks bridge lib)\n Object.assign(pageContextInit, { proxySendClient: proxy });\n } else {\n Object.assign(pageContextInit, { proxy });\n }\n const pageContext = await renderPage(pageContextInit);\n return replyWithPage(reply, pageContext);\n },\n },\n });\n};\n"],"mappings":"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","names":["proxy"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alignable/bifrost-fastify",
3
3
  "repository": "https://github.com/Alignable/bifrost.git",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -36,5 +36,8 @@
36
36
  },
37
37
  "scripts": {
38
38
  "build": "yarn tsup"
39
- }
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ]
40
43
  }
package/index.ts DELETED
@@ -1,200 +0,0 @@
1
- // Note that this file isn't processed by Vite, see https://github.com/brillout/vite-plugin-ssr/issues/562
2
- import { FastifyReply, RawServerBase, FastifyPluginAsync } from "fastify";
3
- import { FastifyRequest, RequestGenericInterface } from "fastify/types/request";
4
- import proxy from "@fastify/http-proxy";
5
- import accepts from "@fastify/accepts";
6
- import { type PageContextProxy, type Proxy } from "../bifrost/types/internal";
7
- import forwarded from "@fastify/forwarded";
8
- import { Writable } from "stream";
9
- import jsdom from "jsdom";
10
- import { IncomingHttpHeaders, IncomingMessage } from "http";
11
- import {
12
- Http2ServerRequest,
13
- IncomingHttpHeaders as Http2IncomingHttpHeaders,
14
- } from "http2";
15
- import { renderPage } from "vite-plugin-ssr/server";
16
-
17
- type RequestExtendedWithProxy = FastifyRequest<
18
- RequestGenericInterface,
19
- RawServerBase
20
- > & { _proxy?: { isPageContext: boolean; originalUrl?: string } };
21
-
22
- function streamToString(stream: Writable): Promise<string> {
23
- const chunks: Buffer[] = [];
24
- return new Promise((resolve, reject) => {
25
- stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
26
- stream.on("error", (err) => reject(err));
27
- stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
28
- });
29
- }
30
-
31
- async function replyWithPage(
32
- reply: FastifyReply<RawServerBase>,
33
- pageContext: Awaited<ReturnType<typeof renderPage>>
34
- ): Promise<FastifyReply> {
35
- const { httpResponse } = pageContext;
36
-
37
- if (!httpResponse) {
38
- return reply.code(404).type("text/html").send("Not Found");
39
- }
40
-
41
- const { body, statusCode, contentType } = httpResponse;
42
-
43
- return reply.status(statusCode).type(contentType).send(body);
44
- }
45
-
46
- const proxyPageId = "/proxy/pages";
47
-
48
- interface ViteProxyPluginOptions {
49
- upstream: URL;
50
- host: URL;
51
- getLayout: (reply: FastifyReply<RawServerBase>) => {
52
- layout: string;
53
- layoutProps: any;
54
- };
55
- /// Use to signal to legacy backend to return special results (eg. remove navbar etc)
56
- rewriteRequestHeaders?: (
57
- req: Http2ServerRequest | IncomingMessage,
58
- headers: Http2IncomingHttpHeaders | IncomingHttpHeaders
59
- ) => Http2IncomingHttpHeaders | IncomingHttpHeaders;
60
- }
61
- /**
62
- * Fastify plugin that wraps @fasitfy/http-proxy to proxy Rails/Turbolinks server into a Vite-Plugin-SSR site.
63
- */
64
- export const viteProxyPlugin: FastifyPluginAsync<
65
- ViteProxyPluginOptions
66
- > = async (fastify, { upstream, host, rewriteRequestHeaders, getLayout }) => {
67
- await fastify.register(accepts);
68
- await fastify.register(proxy, {
69
- upstream: upstream.href,
70
- async preHandler(req, reply) {
71
- if (req.method === "GET" && req.accepts().type(["html"]) === "html") {
72
- const pageContextInit = {
73
- urlOriginal: req.url,
74
- };
75
-
76
- const pageContext = await renderPage<
77
- { _pageId: string },
78
- typeof pageContextInit
79
- >(pageContextInit);
80
-
81
- const proxy = pageContext._pageId === proxyPageId;
82
-
83
- if (!proxy) {
84
- return replyWithPage(reply, pageContext);
85
- } else {
86
- // pageContext.json is added on client navigations to indicate we are returning just json for the client router
87
- // we have to remove it before proxying though.
88
- (req as RequestExtendedWithProxy)._proxy = {
89
- isPageContext: req.raw.url!.includes("/index.pageContext.json"),
90
- originalUrl: req.raw.url,
91
- };
92
- req.raw.url = req.raw.url!.replace("/index.pageContext.json", "");
93
- }
94
- }
95
- },
96
- replyOptions: {
97
- rewriteRequestHeaders(request, headers) {
98
- // Delete cache headers
99
- delete headers["if-modified-since"];
100
- delete headers["if-none-match"];
101
- delete headers["if-unmodified-since"];
102
- delete headers["if-none-match"];
103
- delete headers["if-range"];
104
-
105
- const fwd = forwarded(request as IncomingMessage).reverse();
106
- // fwd.push(request.ip); TODO: not sure if this is needed
107
- headers["X-Forwarded-For"] = fwd.join(", ");
108
- headers["X-Forwarded-Host"] = host.host;
109
- headers["X-Forwarded-Protocol"] = host.protocol;
110
- if (rewriteRequestHeaders) {
111
- return rewriteRequestHeaders(request, headers);
112
- }
113
- return headers;
114
- },
115
- async onResponse(req, reply: FastifyReply<RawServerBase>, res) {
116
- const { isPageContext = false, originalUrl = undefined } =
117
- (req as RequestExtendedWithProxy)._proxy || {};
118
- if (isPageContext && originalUrl) {
119
- // restore url rewrite
120
- req.raw.url = originalUrl;
121
- }
122
-
123
- if ([301, 302, 303, 307, 308].includes(reply.statusCode)) {
124
- const location = reply.getHeader("location") as string;
125
- if (location) {
126
- const url = new URL(location);
127
- if (url.host === upstream.host) {
128
- // rewrite redirect on upstream's host to the proxy host
129
- url.host = host.host;
130
- }
131
- reply.header("location", url);
132
- if (isPageContext) {
133
- reply
134
- .status(200)
135
- .type("application/json")
136
- .send(
137
- JSON.stringify({
138
- pageContext: {
139
- // A bit hacky, but we manually construct the VPS pageContext here
140
- _pageId: proxyPageId,
141
- redirectTo: url,
142
- },
143
- })
144
- );
145
- } else {
146
- return reply.send(res);
147
- }
148
- }
149
- }
150
-
151
- const { layout, layoutProps } = getLayout(reply);
152
- if (!layout) {
153
- return reply.send(res);
154
- }
155
-
156
- const html = await streamToString(res);
157
- const dom = new jsdom.JSDOM(html);
158
- const doc = dom.window.document;
159
- const bodyEl = doc.querySelector("body");
160
- const head = doc.querySelector("head");
161
-
162
- if (!bodyEl || !head) {
163
- return reply.code(404).type("text/html").send("proxy failed");
164
- }
165
-
166
- // disable vite-plugin-ssr link interceptor. May not be neccessary in future:
167
- // https://github.com/brillout/vite-plugin-ssr/discussions/728#discussioncomment-5634111
168
- bodyEl
169
- .querySelectorAll("a[rel='external']")
170
- .forEach((e) => e.setAttribute("data-turbolinks", "false"));
171
- bodyEl.querySelectorAll("a").forEach((e) => (e.rel = "external"));
172
-
173
- const bodyAttrs: Record<string, string> = {};
174
- bodyEl.getAttributeNames().forEach((name) => {
175
- bodyAttrs[name] = bodyEl.getAttribute(name)!;
176
- });
177
-
178
- const proxy: Proxy = {
179
- body: bodyEl.innerHTML,
180
- head: head.innerHTML,
181
- bodyAttrs,
182
- };
183
-
184
- const pageContextInit: Partial<PageContextProxy> = {
185
- urlOriginal: req.url,
186
- layout,
187
- layoutProps,
188
- };
189
- // proxySendClient is serialized and sent to client on subsequent navigation. proxy is ONLY included server-side to avoid doubling page size
190
- if (isPageContext) { //TODO: send whole string instead of parsing and let browser do the parsing (turbolinks bridge lib)
191
- Object.assign(pageContextInit, { proxySendClient: proxy });
192
- } else {
193
- Object.assign(pageContextInit, { proxy });
194
- }
195
- const pageContext = await renderPage(pageContextInit);
196
- return replyWithPage(reply, pageContext);
197
- },
198
- },
199
- });
200
- };
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "strict": true,
4
- "target": "ES2020",
5
- "declaration": true,
6
- "moduleResolution": "node",
7
- "skipLibCheck": true,
8
- "esModuleInterop": true,
9
- "noEmit": true
10
- },
11
- "files": ["./index.ts"],
12
- }
package/tsup.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from 'tsup'
2
-
3
- export default defineConfig({
4
- entry: [
5
- './index.ts',
6
- ],
7
- format: 'esm',
8
- clean: true,
9
- sourcemap: true,
10
- dts: true
11
- })