All files / lib/compiler/codegen collect.js

93% Statements 266/286
65.3% Branches 32/49
100% Functions 8/8
93% Lines 266/286

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 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 17x 17x 17x 17x 17x 17x 17x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 3x 3x 3x 3x 3x 3x 3x 3x 3x 322x 322x 322x 322x 322x 322x 95x 95x 95x 95x 95x 95x 322x 322x 322x 322x 322x 322x 483x 483x 35x 35x 35x 35x 35x 483x 322x 322x 322x 322x 322x 322x 338x 338x 17x 17x 17x 17x 17x 338x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 3x 3x 3x 3x 3x 3x     3x 3x 3x 3x 3x 322x 322x 322x 322x 322x 322x 322x 180x 180x 347x 347x 347x 193x 193x 193x 193x 347x     154x 347x 347x 1540x 1540x 4x 4x 4x 4x 1540x   460x 4x 4x 1540x 347x 3x 3x 154x 347x 145x       145x 145x 145x 145x 145x 145x 145x       145x 145x 145x 145x 145x 145x 145x 154x 347x 347x 180x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 26x 26x 26x 26x           26x 26x 26x 26x 469x 469x 469x 36x 36x 469x 469x 469x 469x 469x 469x 469x     469x     469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x  
/**
 * @file collect.js
 * @description Finds every expression a component will evaluate at runtime.
 *
 * ## Why this is a scan rather than a list of call sites
 *
 * Expressions reach the runtime from more places than the render program: the
 * list manager reads `data-ax-for` and `data-ax-key` off the DOM, the defer
 * manager reads `data-ax-defer-when`, computed values arrive as declarations,
 * and event handlers travel inside a JSON attribute. Enumerating those call
 * sites in the compiler would mean two lists that have to stay in step, and the
 * failure mode of them drifting is silent: an expression that is not in the
 * table simply falls back to being interpreted, which is exactly the thing this
 * work exists to remove.
 *
 * So the collector reads the *finished* template — the one the runtime is
 * handed — and takes every expression-bearing position in it. A construct the
 * compiler learns to emit later is picked up without changing this file, as
 * long as it puts its expression where the others put theirs.
 *
 * ## Over-collecting is safe, under-collecting is not
 *
 * The table is keyed by exact source text and consulted only when the runtime
 * actually evaluates that text. An entry nothing ever looks up costs a few
 * bytes; a missing entry costs an interpreter in the bundle. The scan therefore
 * errs towards including a candidate, and anything that does not parse as an
 * expression is dropped rather than reported.
 * @module lib/compiler/codegen/collect
 */
 
import { parseHTML } from '../parser/htmlTree.js';
import { createInterpolationRegex } from '../../core/utils/templateUtils.js';
 
/**
 * Attributes whose whole value is one expression the runtime evaluates.
 * @type {string[]}
 */
const EXPRESSION_ATTRIBUTES = [
  'data-ax-for',
  'data-ax-key',
  'data-ax-show',
  'data-ax-class',
  'data-ax-html',
  'data-ax-defer-when',
  'data-ax-bind',
  'data-ax-dyn-attrs',
  'data-ax-style',
  'data-avenx-style',
];
 
/**
 * Interpolations inside a `<@for>` body, which the compiler escapes to `{% %}`
 * so the outer template's own interpolation pass leaves them alone. The list
 * manager restores them to `{{ }}` before evaluating, so the expression text
 * the runtime sees is what sits between the delimiters here.
 * @returns {RegExp} A fresh regex, because it is stateful.
 */
function listInterpolationRegex() {
  // `%+` on both sides, because nesting deepens the marker: a body inside two
  // loops is written `{%% x %%}`. Matching a single `%` would capture
  // `% x %` for that case -- a string that parses as nothing and was reported
  // as an uncompiled expression.
  return /\{%+\s*([\s\S]*?)\s*%+\}/g;
}
 
/**
 * Decodes the entities an attribute value carries.
 *
 * The template parser keeps attribute values exactly as written, so a
 * `data-ax-event` payload arrives as `{&quot;click&quot;:&quot;save()&quot;}`.
 * The browser decodes it before the runtime reads it; this does the same so
 * both see the same handler source.
 * @param {string} value - The raw attribute value.
 * @returns {string} The decoded value.
 */
function decodeEntities(value) {
  return value
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&apos;/g, "'")
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&amp;/g, '&');
}
 
/**
 * Adds a candidate expression to a set, ignoring blanks.
 * @param {Set<string>} into - The collecting set.
 * @param {any} source - The candidate text.
 */
function add(into, source) {
  if (typeof source !== 'string') return;
  const trimmed = source.trim();
  if (trimmed === '') return;
  into.add(trimmed);
}
 
/**
 * Collects the interpolations in a piece of text.
 * @param {Set<string>} into - The collecting set.
 * @param {string} text - Text that may contain `{{ }}` or `{{{ }}}`.
 */
function addInterpolations(into, text) {
  if (typeof text !== 'string' || !text.includes('{{')) return;
  const regex = createInterpolationRegex();
  let match;
  while ((match = regex.exec(text)) !== null) {
    add(into, match[1] !== undefined ? match[1] : match[2]);
  }
}
 
/**
 * Collects the escaped interpolations in a `<@for>` body.
 * @param {Set<string>} into - The collecting set.
 * @param {string} text - Text that may contain `{% %}`.
 */
function addListInterpolations(into, text) {
  if (typeof text !== 'string' || !text.includes('{%')) return;
  const regex = listInterpolationRegex();
  let match;
  while ((match = regex.exec(text)) !== null) {
    add(into, match[1]);
  }
}
 
/**
 * Collects the handler bodies out of a `data-ax-event` attribute.
 *
 * The attribute is a JSON object of event name to handler source, written by
 * {@link module:lib/compiler/templateEvents}. A malformed one is skipped: the
 * template validator already reports it, and guessing here would put a wrong
 * key in the table.
 * @param {Set<string>} into - The collecting set.
 * @param {string} value - The attribute value.
 */
function addEventHandlers(into, value) {
  if (typeof value !== 'string' || value.trim() === '') return;
  let parsed;
  try {
    parsed = JSON.parse(decodeEntities(value));
  } catch {
    return;
  }
  if (!parsed || typeof parsed !== 'object') return;
  for (const handler of Object.values(parsed)) {
    add(into, handler);
  }
}
 
/**
 * Walks a parsed template, collecting expressions and handler statements.
 * @param {object[]} nodes - Parsed template nodes.
 * @param {Set<string>} expressions - Collects value expressions.
 * @param {Set<string>} statements - Collects handler bodies.
 */
function walk(nodes, expressions, statements) {
  for (const node of nodes) {
    if (!node) continue;
 
    if (node.type === 'text') {
      addInterpolations(expressions, node.content);
      addListInterpolations(expressions, node.content);
      continue;
    }
    if (node.type === 'comment') {
      continue;
    }
 
    const attrs = node.attrs || {};
    for (const name of EXPRESSION_ATTRIBUTES) {
      const value = attrs[name];
      if (value === undefined) continue;
      // Most of these hold a bare expression, but a few are written as an
      // interpolation -- `data-ax-style="{{ { color: c } }}"` is the documented
      // form. Taking the raw value there would put the braces in the table as
      // an expression, which parses as nothing.
      if (typeof value === 'string' && value.includes('{{')) {
        addInterpolations(expressions, value);
      } else {
        add(expressions, value);
      }
    }
    if (attrs['data-ax-event'] !== undefined) {
      addEventHandlers(statements, attrs['data-ax-event']);
    }
 
    for (const [name, value] of Object.entries(attrs)) {
      if (name.startsWith('data-props-')) {
        add(expressions, value);
        continue;
      }
      // A handler the compiler left in its authored form. Most `@event`
      // attributes are rewritten into the `data-ax-event` payload above, but
      // not all of them are -- `@submit.prevent` on a form is not -- and the
      // binder reads those straight off the element. Collecting by prefix
      // rather than by a list of the ones known to survive is what stops this
      // going wrong again the next time the rewrite's coverage changes.
      if (name.startsWith('@')) {
        add(statements, value);
        continue;
      }
      // Any remaining attribute may carry interpolations in its value, and an
      // interpolated attribute *name* is a construct the string renderer
      // resolves, so both sides are scanned.
      addInterpolations(expressions, name);
      addInterpolations(expressions, value);
      addListInterpolations(expressions, value);
    }
 
    walk(node.children || [], expressions, statements);
  }
}
 
/**
 * Every expression and handler body a compiled unit will evaluate.
 * @param {object} unit - What the compiler produced for one component or page.
 * @param {string} unit.template - The finished template.
 * @param {Object<string, string>} [unit.computed] - Computed declarations.
 * @param {Object<string, string>} [unit.methods] - Action bodies.
 * @param {Object<string, string>} [unit.resources] - Resource bodies.
 * @param {object} [unit.program] - The render program, when one was compiled.
 *   Present so a caller can tell the two cases apart; its expressions are
 *   compiled separately and are not collected here.
 * @param {string[]} [unit.voidTags] - Project-specific void tag names.
 * @returns {{expressions: string[], statements: string[]}} The collected sources.
 */
export function collectExpressions(unit) {
  const expressions = new Set();
  const statements = new Set();
 
  // A component with a render program does not evaluate anything from its
  // template through this table: every interpolation, binding and handler in it
  // is addressed by index and compiled by buildProgramTables. Scanning the
  // template anyway produced entries nothing could reach, and -- because the
  // scan reads the *rewritten* template, where a loop body's interpolations are
  // escaped as `{% %}` -- reported perfectly good directives as expressions
  // that could not be compiled, under a warning claiming they kept the runtime
  // parser in the bundle. They did not: the parser was not there.
  const templateDrivesThisTable = !unit.program;
 
  if (templateDrivesThisTable && typeof unit.template === 'string' && unit.template.trim() !== '') {
    let nodes;
    try {
      nodes = parseHTML(unit.template, unit.voidTags || []);
    } catch {
      // A template that does not parse here is already failing the build
      // elsewhere with a better message. Collecting nothing simply means those
      // expressions stay interpreted, which is the pre-existing behaviour.
      nodes = null;
    }
    if (nodes) {
      walk(nodes, expressions, statements);
    }
  }
 
  if (unit.computed) {
    for (const definition of Object.values(unit.computed)) {
      add(expressions, definition);
    }
  }
 
  // Action and resource bodies run through the same statement path an inline
  // handler does, so they belong in the same table. A body using real statement
  // syntax will not compile and is simply left out, which leaves that one
  // action on the existing runtime path.
  for (const source of Object.values(unit.methods || {})) {
    add(statements, source);
  }
  for (const source of Object.values(unit.resources || {})) {
    add(statements, source);
  }
 
  // The program's own expressions are deliberately *not* collected here. They
  // are interned by the lowering pass and compiled into a positional table by
  // buildProgramTables, addressed by the indices the ops carry -- so an op's
  // `x` is a number and there is nothing here to compile.
  //
  // What remains in this table is everything the compiled renderer does not
  // drive: computed values, action bodies, resource handlers, and every
  // expression in a component that fell back.
 
  return {
    expressions: [...expressions].sort(),
    statements: [...statements].sort(),
  };
}