All files / plugins/avenx-vite/src compiler.js

95.53% Statements 171/179
87.5% Branches 28/32
100% Functions 9/9
95.53% Lines 171/179

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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 1802x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 33x 33x 33x 33x 7x 7x 7x 7x 7x 7x 7x 33x 33x 33x 33x 33x 33x 33x 33x 33x 33x 7x 7x 7x 7x 7x 7x 7x 33x 33x 33x 33x 33x 33x 33x 7x 7x 7x 7x 7x 7x 7x 17x 17x 17x 17x 17x 19x 19x 16x 16x 19x 1x 17x 7x 7x 7x 7x 7x 7x 7x 7x 33x 33x 33x 33x 33x 33x 33x 33x 33x 7x 7x 7x 7x 7x 7x 7x 7x 7x 15x 15x 15x 15x 15x 15x         15x 15x 15x 15x 15x 15x 15x 7x 7x 7x 7x 7x 7x 7x 7x 7x 2x 2x 2x 2x 2x 2x         2x 2x 2x 2x 2x 2x 2x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 17x 17x 1x 1x 17x 17x 17x 7x 7x 7x  
import fs from 'node:fs';
import path from 'node:path';
import StyleProcessor from '../../../lib/compiler/StyleProcessor.js';
import ComponentParser from '../../../lib/compiler/ComponentParser.js';
import { wrapComponent, wrapPage } from './wrapper.js';
import { generateTemplateSourceMap } from './sourcemap.js';
import { COMPONENT_CSS_EXT, PAGE_CSS_EXT } from './constants.js';
 
/**
 * Source extensions a stylesheet may belong to, in resolution order.
 * @type {string[]}
 */
const MODULE_EXTENSIONS = ['.js', '.html', '.avx'];
 
/**
 * Creates an Avenx compiler instance.
 * @param {object} [options] - Compiler configuration options.
 * @param {boolean} [options.debug] - Enables debug logging.
 * @returns {object} Compiler API.
 */
export function createCompiler(options = {}) {
  const debug = options.debug ?? false;
 
  /**
   * Builds a parser backed by a dedicated StyleProcessor.
   *
   * A StyleProcessor accumulates every scoped rule, source and generated line
   * it is given and is only reset by the CLI compiler's build(). Sharing one
   * instance across transforms would therefore grow for the lifetime of the dev
   * server and emit other components' rules into each module's stylesheet, so
   * each compile gets its own.
   * @returns {{parser: ComponentParser, styleProcessor: StyleProcessor}}
   */
  function createParser() {
    const styleProcessor = new StyleProcessor(options.style || {}, options.config || null);
    const parser = new ComponentParser(styleProcessor, options.voidTags || [], options.config || null);
    return { parser, styleProcessor };
  }
 
  /**
   * Returns the generated class name from a component or page file.
   * @param {string} filePath Full path to the source file.
   * @returns {string} Generated class name.
   */
  function getClassName(filePath) {
    const base = path.basename(filePath);
    const cleaned = base
      .replace(/\.(component|page)\.(js|html|avx)$/i, '')
      .replace(/\.(js|html|avx)$/i, '');
 
    return cleaned
      .split(/[-_]/)
      .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
      .join('');
  }
 
  /**
   * Resolves the co-located stylesheet for a component or page module.
   * @param {string} filePath - Path to the component or page module.
   * @returns {string|null} The stylesheet path, or null when there is none.
   */
  function findStyleSheet(filePath) {
    const isPage = /\.page\.(js|html|avx)$/i.test(filePath);
    const stylePath = filePath.replace(
      /\.(component|page)\.(js|html|avx)$/i,
      isPage ? PAGE_CSS_EXT : COMPONENT_CSS_EXT,
    );
    return stylePath !== filePath && fs.existsSync(stylePath) ? stylePath : null;
  }
 
  /**
   * Resolves the component or page module that owns a stylesheet.
   * @param {string} cssPath - Path to a .component.css or .page.css file.
   * @returns {string|null} The owning module path, or null when none exists.
   */
  function findOwningModule(cssPath) {
    const isPage = cssPath.endsWith(PAGE_CSS_EXT);
    const base = cssPath.slice(0, -(isPage ? PAGE_CSS_EXT.length : COMPONENT_CSS_EXT.length));
    const suffix = isPage ? '.page' : '.component';
 
    for (const ext of MODULE_EXTENSIONS) {
      const candidate = `${base}${suffix}${ext}`;
      if (fs.existsSync(candidate)) {
        return candidate;
      }
    }
    return null;
  }
 
  /**
   * Compiles a component or page module and its scoped styles.
   * @param {string} filePath - Path to the source file.
   * @param {'component'|'page'} type - Compilation type.
   * @returns {{template: string, css: string, className: string, stylePath: string|null}}
   */
  function compileModule(filePath, type) {
    const { parser, styleProcessor } = createParser();
    const parsedCode = parser.parse(filePath, type);
    return {
      template: parsedCode,
      css: styleProcessor.getGlobalStyles(),
      className: getClassName(filePath),
      stylePath: findStyleSheet(filePath),
    };
  }
 
  return {
    /**
     * Compiles an Avenx component.
     * @param {string} filePath Path to the component file.
     * @param {string} [code] Optional raw source code.
     * @returns {{code: string, map: object, css: string, className: string}}
     */
    compileComponent(filePath, code) {
      const originalCode = code ?? (fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '');
      const { template, css, className, stylePath } = compileModule(filePath, 'component');
      const wrappedCode = wrapComponent(template, className, stylePath);
      const map = generateTemplateSourceMap(filePath, originalCode, wrappedCode);
 
      if (debug) {
        console.log('\n================ COMPILED COMPONENT ================\n');
        console.log(wrappedCode);
        console.log('\n====================================================\n');
      }
 
      return {
        code: wrappedCode,
        map,
        css,
        className,
      };
    },
 
    /**
     * Compiles an Avenx page.
     * @param {string} filePath Path to the page file.
     * @param {string} [code] Optional raw source code.
     * @returns {{code: string, map: object, css: string, className: string}}
     */
    compilePage(filePath, code) {
      const originalCode = code ?? (fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '');
      const { template, css, className, stylePath } = compileModule(filePath, 'page');
      const wrappedCode = wrapPage(template, className, stylePath);
      const map = generateTemplateSourceMap(filePath, originalCode, wrappedCode);
 
      if (debug) {
        console.log('\n================ COMPILED PAGE =====================\n');
        console.log(wrappedCode);
        console.log('\n====================================================\n');
      }
 
      return {
        code: wrappedCode,
        map,
        css,
        className,
      };
    },
 
    /**
     * Compiles a component or page stylesheet into its scoped CSS.
     *
     * Scoping depends on the owning module (the hash is derived from the block
     * contents and the component name, and the matching class is injected into
     * that module's template), so the owning module is compiled to produce it.
     * @param {string} cssPath - Path to the stylesheet.
     * @returns {string|null} The scoped CSS, or null when there is no owner.
     */
    compileStyle(cssPath) {
      const owner = findOwningModule(cssPath);
      if (!owner) {
        return null;
      }
      const type = cssPath.endsWith(PAGE_CSS_EXT) ? 'page' : 'component';
      const { css } = compileModule(owner, type);
      return css;
    },
  };
}