Skip to content

Commit

Permalink
feat: rework with fastify plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurfiorette committed Apr 5, 2024
1 parent 675cc96 commit 77d4d05
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-lemons-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kitajs/fastify-html-plugin': patch
---

Fixed encapsulation
5 changes: 5 additions & 0 deletions .changeset/lemon-rules-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kitajs/fastify-html-plugin': minor
---

Improved performance and async support
100 changes: 69 additions & 31 deletions packages/fastify-html-plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,96 @@ if (!globalThis.SUSPENSE_ROOT) {
require('@kitajs/html/suspense');
}

/** @type {import('./types/index').kAutoDoctype} */
const kAutoDoctype = Symbol.for('fastify-kita-html.autoDoctype');

/**
* @type {import('fastify').FastifyPluginCallback<
* import('./types').FastifyKitaHtmlOptions
* >}
*/
function fastifyKitaHtml(fastify, opts, next) {
function plugin(fastify, opts, next) {
// Good defaults
opts.autoDoctype ??= true;

fastify.decorateReply(kAutoDoctype, opts.autoDoctype);
fastify.decorateReply('html', html);

return next();
}

/** @type {import('fastify').FastifyReply['html']} */
function html(htmlStr) {
if (typeof htmlStr !== 'string') {
// Recursive promise handling, rejections should just rethrow
// just like as if it was a sync component error.
return htmlStr.then(html.bind(this));
}
/** @type {import('fastify').FastifyReply['html']} */
function html(htmlStr) {
// @ts-expect-error - generics break the type inference here
return typeof htmlStr === 'string'
? handleHtml(htmlStr, this)
: handleAsyncHtml(htmlStr, this);
}

// prepends doctype if the html is a full html document
if (opts.autoDoctype && isTagHtml(htmlStr)) {
htmlStr = '<!doctype html>' + htmlStr;
}
/**
* Simple helper that can be optimized by the JS engine to avoid having async await in the
* main flow
*
* @template {import('fastify').FastifyReply} R
* @param {Promise<string>} promise
* @param {R} reply
* @returns {Promise<R>}
*/
async function handleAsyncHtml(promise, reply) {
return handleHtml(await promise, reply);
}

this.type('text/html; charset=utf-8');
/**
* @template {import('fastify').FastifyReply} R
* @param {string} htmlStr
* @param {R} reply
*/
function handleHtml(htmlStr, reply) {
// @ts-expect-error - prepends doctype if the html is a full html document
if (reply[kAutoDoctype] && isTagHtml(htmlStr)) {
htmlStr = '<!doctype html>' + htmlStr;
}

// If no suspense component was used, this will not be defined.
const requestData = SUSPENSE_ROOT.requests.get(this.request.id);
reply.type('text/html; charset=utf-8');

if (!requestData) {
return (
this
// Nothing needs to be sent later, content length is known
.header('content-length', Buffer.byteLength(htmlStr))
.send(htmlStr)
);
}
// If no suspense component was used, this will not be defined.
const requestData = SUSPENSE_ROOT.requests.get(reply.request.id);

requestData.stream.push(htmlStr);
if (requestData === undefined) {
return (
reply
// Should be safe to use .length instead of Buffer.byteLength here
.header('content-length', handleHtml.length)
.send(htmlStr)
);
}

// Content-length is optional as long as the connection is closed after the response is done
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.3
requestData.stream.push(htmlStr);

return this.send(requestData.stream);
}
// Content-length is optional as long as the connection is closed after the response is done
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.3
return reply.send(requestData.stream);
}

module.exports = fp(fastifyKitaHtml, {
const fastifyKitaHtml = fp(plugin, {
fastify: '4.x',
name: '@kitajs/fastify-html-plugin'
});
module.exports.default = fastifyKitaHtml;
module.exports.fastifyKitaHtml = fastifyKitaHtml;

/**
* These export configurations enable JS and TS developers to consume
*
* @kitajs/fastify-html-plugin in whatever way best suits their needs. Some examples of
* supported import syntax includes:
*
* - `const fastifyKitaHtml = require('@kitajs/fastify-html-plugin')`
* - `const { fastifyKitaHtml } = require('@kitajs/fastify-html-plugin')`
* - `import * as fastifyKitaHtml from '@kitajs/fastify-html-plugin'`
* - `import { fastifyKitaHtml } from '@kitajs/fastify-html-plugin'`
* - `import fastifyKitaHtml from '@kitajs/fastify-html-plugin'`
*/
module.exports = fastifyKitaHtml;
module.exports.default = fastifyKitaHtml; // supersedes fastifyKitaHtml.default = fastifyKitaHtml
module.exports.fastifyKitaHtml = fastifyKitaHtml; // supersedes fastifyKitaHtml.fastifyKitaHtml = fastifyKitaHtml

module.exports.kAutoDoctype = kAutoDoctype;
2 changes: 1 addition & 1 deletion packages/fastify-html-plugin/lib/is-tag-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module.exports.isTagHtml = function isTagHtml(value) {
// remove whitespace from the start of the string
.trimStart()
// get the first 5 characters
.slice(0, 5)
.substring(0, 5)
// compare to `<html`
.toLowerCase() === '<html'
);
Expand Down
4 changes: 2 additions & 2 deletions packages/fastify-html-plugin/test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('reply.html()', () => {
assert.deepStrictEqual(res.json(), {
statusCode: 500,
error: 'Internal Server Error',
message: 'htmlStr.then is not a function'
message: 'value.trimStart is not a function'
});
});

Expand All @@ -69,7 +69,7 @@ describe('reply.html()', () => {
assert.deepStrictEqual(res.json(), {
statusCode: 500,
error: 'Internal Server Error',
message: 'htmlStr.then is not a function'
message: 'value.trimStart is not a function'
});
});
});
17 changes: 16 additions & 1 deletion packages/fastify-html-plugin/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import type { FastifyPluginCallback } from 'fastify';

declare module 'fastify' {
interface FastifyReply {
/**
* This gets assigned to every reply instance. You can manually change this value to
* `false` if you want to "hand pick" when or when not to add the doctype.
*/
[fastifyKitaHtml.kAutoDoctype]: boolean;

/**
* **Synchronously** waits for the component tree to resolve and sends it at once to
* the browser.
Expand Down Expand Up @@ -31,7 +37,10 @@ declare module 'fastify' {
* @param html The HTML to send.
* @returns The response.
*/
html(this: this, html: JSX.Element): this | Promise<this>;
html<H extends JSX.Element>(
this: this,
html: H
): H extends Promise<string> ? Promise<void> : void;
}
}

Expand Down Expand Up @@ -61,6 +70,12 @@ declare namespace fastifyKitaHtml {

export const fastifyKitaHtml: FastifyKitaHtmlPlugin;

/**
* This gets assigned to every reply instance. You can manually change this value to
* `false` if you want to "hand pick" when or when not to add the doctype.
*/
export const kAutoDoctype: unique symbol;

export { fastifyKitaHtml as default };
}

Expand Down

0 comments on commit 77d4d05

Please sign in to comment.