All files / integrations/src/sveltekit index.js

48.48% Statements 16/33
42.85% Branches 9/21
35.71% Functions 5/14
48.48% Lines 16/33

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 135 136 137 138 139 140 141 142 143 144 145 146                                                                                                                                1x 1x       5x                     7x 5x 3x 3x     4x                                 8x   8x       8x 8x     8x 7x 7x     8x                                                      
// src/sveltekit/index.js
//
// Public entry point for @coherent.js/integrations/sveltekit. Provides
// server-side rendering and preprocessor integration for SvelteKit projects.
//
// SvelteKit must be installed as a peer dependency to use this integration.
//
// Usage:
//   import { createSvelteKitAdapter } from '@coherent.js/integrations/sveltekit';
 
import { render } from '@coherent.js/core';
 
/**
 * Create a SvelteKit adapter for Coherent.js
 *
 * @param {Object} [options] - Adapter options
 * @returns {Object} SvelteKit adapter utilities
 *
 * @example
 * const adapter = createSvelteKitAdapter();
 * const html = adapter.renderComponent(MyComponent, { title: 'Hello' });
 */
export function createSvelteKitAdapter(_options = {}) {
  return {
    name: '@coherent.js/sveltekit',
 
    /**
     * Render a Coherent.js component to HTML
     */
    renderComponent(component, props = {}) {
      const def = typeof component === 'function' ? component(props) : component;
      return render(def);
    },
 
    /**
     * Create a SvelteKit server load function
     */
    createLoad(component, getProps) {
      return async ({ params, url, fetch }) => {
        const props = getProps
          ? await getProps({ params, url, fetch })
          : { params };
 
        const def = typeof component === 'function' ? component(props) : component;
        const html = render(def);
 
        return { html, props };
      };
    },
 
    /**
     * Create a SvelteKit form action handler
     */
    createAction(handler) {
      return async ({ request, params }) => {
        const formData = await request.formData();
        const data = Object.fromEntries(formData);
        return handler({ data, params });
      };
    }
  };
}
 
/** Local name the preprocessor imports Coherent.js's `render` under. */
const RENDER_IDENTIFIER = '__coherentRender';
const RENDER_IMPORT = `import { render as ${RENDER_IDENTIFIER} } from '@coherent.js/core';`;
 
/** `<script context="module">` (Svelte 4) or `<script module>` (Svelte 5). */
function isModuleScript(attributes) {
  return /\bcontext\s*=\s*["']?module\b/.test(attributes) || /(?:^|\s)module(?:\s|=|$)/.test(attributes);
}
 
/**
 * Make `RENDER_IDENTIFIER` available to the markup: add the import to the
 * instance `<script>`, or add an instance script if there is none. Inserted
 * without newlines so the component's line numbers do not move.
 */
function injectRenderImport(code) {
  // Case-insensitive like HTML: an instance script written <SCRIPT> was
  // missed, and a second instance script was added next to it.
  for (const match of code.matchAll(/<script\b([^>]*)>/gi)) {
    if (!isModuleScript(match[1])) {
      const at = match.index + match[0].length;
      return `${code.slice(0, at)}${RENDER_IMPORT}${code.slice(at)}`;
    }
  }
  return `<script>${RENDER_IMPORT}</script>${code}`;
}
 
/**
 * Create a Svelte preprocessor for Coherent.js templates
 *
 * Replaces each `<coherent>{ ...object literal... }</coherent>` block with
 * `{@html ...}` of the rendered component, and imports `render` from
 * `@coherent.js/core` into the component's instance script so the generated
 * expression resolves. The renderer escapes text content, so `{@html}` only
 * emits markup the component itself describes.
 *
 * @param {Object} [options] - Preprocessor options
 * @param {string} [options.tag] - Custom tag to process (default: 'coherent')
 * @returns {Object} Svelte preprocessor
 */
export function createPreprocessor(options = {}) {
  const tag = options.tag || 'coherent';
 
  return {
    name: 'coherent-preprocessor',
    markup({ content, filename: _filename }) {
      // Find <coherent> blocks; their content is a JS expression (usually an object literal)
      const regex = new RegExp(`<${tag}>(.*?)</${tag}>`, 'gs');
      let found = false;
 
      // Replacer function, so `$&`-style sequences in the block stay literal
      const transformed = content.replace(regex, (_block, expression) => {
        found = true;
        return `{@html ${RENDER_IDENTIFIER}(${expression.trim()})}`;
      });
 
      return {
        code: found ? injectRenderImport(transformed) : content,
        map: null
      };
    }
  };
}
 
/**
 * SvelteKit hooks integration
 *
 * @param {Object} [options] - Hook options
 * @returns {Object} SvelteKit handle function
 */
export function createHandle(_options = {}) {
  return async ({ event, resolve }) => {
    // Add Coherent.js render function to locals
    event.locals.coherent = {
      render(component, props = {}) {
        const def = typeof component === 'function' ? component(props) : component;
        return render(def);
      }
    };
 
    return resolve(event);
  };
}