All files / integrations/src/fastify coherent-fastify.js

25% Statements 8/32
28.57% Branches 4/14
28.57% Functions 2/7
25.8% Lines 8/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134                                                                3x     3x                 3x                                                           3x                   3x                     1x                   1x                   1x                                    
/**
 * Fastify integration for Coherent.js
 * Provides plugins and utilities for using Coherent.js with Fastify.
 *
 * The plugin must be wrapped with fastify-plugin (fp). Without fp, every
 * `fastify.register(...)` boundary creates a fresh encapsulated context,
 * and the `preSerialization` hook + `isCoherentObject` decorator only
 * apply to routes registered INSIDE that context. The user's root-level
 * routes would never see them, and responses would JSON-serialize the
 * raw component object instead of rendering it.
 */
 
import fp from 'fastify-plugin';
import {
  renderWithTemplate,
  renderComponentFactory
} from '@coherent.js/core';
 
/**
 * Fastify plugin implementation. Not exported directly — use the fp-wrapped
 * `coherentFastify` (or its alias `setupCoherent`) instead.
 *
 * @param {Object} fastify - Fastify instance
 * @param {Object} options - Plugin options
 * @param {boolean} [options.enablePerformanceMonitoring] - Enable performance monitoring
 * @param {string} [options.template] - HTML template with {{content}} placeholder
 * @param {Function} done - Callback to signal plugin registration completion
 */
function coherentFastifyImpl(fastify, options = {}, done) {
  const {
    enablePerformanceMonitoring = false,
    template = '<!DOCTYPE html>\n{{content}}'
  } = options;
 
  // Add decorator to check if an object is a Coherent.js component
  fastify.decorateReply('isCoherentObject', (obj) => {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
      return false;
    }
    const keys = Object.keys(obj);
    return keys.length === 1;
  });
 
  // Add decorator for explicit rendering: reply.coherent(component, opts?)
  fastify.decorateReply('coherent', function(component, renderOptions = {}) {
    const {
      enablePerformanceMonitoring: renderPerformanceMonitoring = enablePerformanceMonitoring,
      template: renderTemplate = template
    } = renderOptions;
 
    try {
      const finalHtml = renderWithTemplate(component, {
        enablePerformanceMonitoring: renderPerformanceMonitoring,
        template: renderTemplate
      });
      this.header('Content-Type', 'text/html; charset=utf-8');
      this.send(finalHtml);
    } catch (_error) {
      console.error('Coherent.js rendering _error:', _error);
      this.status(500).send({
        _error: 'Internal Server Error',
        message: _error.message
      });
    }
  });
 
  // Auto-render: if a handler returns a Coherent.js component object,
  // intercept before serialization and replace the payload with HTML.
  //
  // - `onSend` runs after JSON serialization (payload is already a string),
  //   so the component object would never be detected there.
  // - `preSerialization` runs before serialization. We render to HTML and
  //   install an identity serializer for this reply, so Fastify doesn't
  //   JSON-stringify the HTML string we just produced.
  fastify.addHook('preSerialization', async (request, reply, payload) => {
    if (reply.isCoherentObject?.(payload)) {
      const finalHtml = renderWithTemplate(payload, { enablePerformanceMonitoring, template });
      reply.header('Content-Type', 'text/html; charset=utf-8');
      reply.serializer((p) => p);
      return finalHtml;
    }
    return payload;
  });
 
  done();
}
 
/**
 * Fastify plugin for Coherent.js — wrapped with fastify-plugin so decorators
 * and hooks apply to the parent (root) context. Register at the top of your
 * app, then define routes that return Coherent.js component objects:
 *
 *   await fastify.register(coherentFastify, { template: APP_HTML_TEMPLATE });
 *   fastify.get('/', async () => HomePage({}));
 */
export const coherentFastify = fp(coherentFastifyImpl, {
  name: 'coherent-fastify',
  fastify: '>=4.0.0'
});
 
/**
 * Alias for `coherentFastify`. Preserved for backward compatibility with
 * scaffolds and examples that use `setupCoherent`. Behaves identically:
 * `await fastify.register(setupCoherent, options)`.
 */
export const setupCoherent = coherentFastify;
 
/**
 * Create a Fastify route handler for Coherent.js components.
 *
 * @param {Function} componentFactory - Function that returns a Coherent.js component
 * @param {Object} options - Handler options
 * @returns {Function} Fastify route handler
 */
export function createHandler(componentFactory, options = {}) {
  return async (request, reply) => {
    try {
      const finalHtml = await renderComponentFactory(
        componentFactory,
        [request, reply],
        options
      );
      reply.header('Content-Type', 'text/html; charset=utf-8');
      return finalHtml;
    } catch (_error) {
      console.error('Coherent.js handler _error:', _error);
      throw _error;
    }
  };
}
 
// Default export = the plugin, for `fastify.register(import('@coherent.js/integrations/fastify'))`.
export default coherentFastify;