All files / lib/compiler/codegen table.js

97.26% Statements 249/256
81.39% Branches 35/43
100% Functions 6/6
97.26% Lines 249/256

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 257322x 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 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 890x 890x 377x 377x 513x 513x 810x 923x 923x 923x 3x     3x 3x 923x 510x 510x 890x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 446x 446x 444x 444x 444x 443x 443x 446x 446x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 938x 938x 938x 81x 81x 81x 3x     3x 3x 1x 3x 2x 2x 3x 81x 938x 938x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 938x 938x 938x 938x 268x 268x 268x 268x 268x 2x     2x 2x   2x 2x 2x 2x 268x 938x 938x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 557x 557x 557x 557x 21x 2x 2x 19x 19x 557x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 322x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 59x 59x 469x 2x 2x 469x 151x 151x 469x 4x 4x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x 469x  
/**
 * @file table.js
 * @description Emits a component's compiled expressions as module source.
 *
 * ## Shape, and why it is keyed by source
 *
 * ```js
 * Counter.__axExprs = {
 *   "count": (scope) => axGet(scope, "count"),
 *   "item.qty": (scope) => axRead(axGet(scope, "item"), "qty", false),
 * };
 * ```
 *
 * Keying by the original source text rather than by an index is what lets every
 * evaluation site take the compiled path at once. The render program addresses
 * its bindings by index and could have used one; the list manager, the defer
 * manager and computed declarations all arrive holding *text*, read from a DOM
 * attribute or a declaration. One table keyed by that text serves all of them,
 * so no call site had to learn a new calling convention in order to stop being
 * interpreted.
 *
 * The cost is the key: a component's expression sources are in the bundle
 * twice, once as a key and once as compiled code. That is paid back many times
 * over by not shipping a parser and an interpreter, and it is temporary — once
 * every evaluation site addresses bindings by index, the keys go.
 *
 * ## What is not in the table
 *
 * An expression the generator refuses. Refusals are returned to the caller
 * rather than swallowed, because the two reasons are very different. A security
 * refusal (naming `window`, writing `__proto__`) must fail the build, while an
 * expression merely outside the supported language should leave the entry
 * absent and let the existing runtime path handle it.
 * @module lib/compiler/codegen/table
 */
 
import {
  compileExpressionToSource,
  compileStatementsToSource,
  ExpressionCodegenError,
} from './expression.js';
import { compileActionToSource } from './actions.js';
 
/**
 * Security refusals that must fail the build rather than fall back.
 *
 * An expression outside the supported language is a capability gap and falls
 * back. An expression that names a restricted global or touches a forbidden key
 * is a mistake in the application, and letting it fall back would mean the
 * developer learns about it from a runtime sandbox violation instead of from
 * the build.
 * @type {RegExp}
 */
const SECURITY_REFUSAL = /blocked for security reasons|restricted global|constructs code from a string/i;
 
/**
 * Compiles an ordered list of sources into a positional array literal.
 *
 * The render program addresses expressions by index, so its table is an array
 * and every slot must be filled: a hole would be a binding the runtime cannot
 * evaluate and cannot name, because the source is not in the bundle. So this
 * refuses the whole table rather than emitting a sparse one, and the caller
 * keeps the component on the legacy path -- the same compile-or-refuse rule the
 * program itself follows.
 * @param {string} className - The component class name.
 * @param {string} staticName - The static to assign, e.g. `__axProgramExprs`.
 * @param {string[]} sources - The sources, in index order.
 * @param {function(string): string} generate - The generator to apply.
 * @returns {{source: string, failure: {source: string, reason: string}|null}}
 *   The emitted assignment, or the first source that would not compile.
 */
function buildIndexedTable(className, staticName, sources, generate) {
  if (!sources || sources.length === 0) {
    return { source: '', failure: null };
  }
 
  const entries = [];
  for (const source of sources) {
    try {
      entries.push(`  ${generate(source)}`);
    } catch (error) {
      if (!(error instanceof ExpressionCodegenError)) {
        throw error;
      }
      return { source: '', failure: { source, reason: error.message } };
    }
  }
 
  return { source: `${className}.${staticName} = [\n${entries.join(',\n')}\n];`, failure: null };
}
 
/**
 * Compiles a render program's expression and statement tables.
 *
 * Separate from {@link buildExpressionTable} because the two have different
 * failure modes. A source-keyed entry that does not compile leaves the runtime
 * to interpret the source it still has; an indexed entry that does not compile
 * leaves nothing behind, so the program cannot ship.
 * @param {string} className - The component class name.
 * @param {string[]} expressions - Expression sources, in index order.
 * @param {string[]} statements - Statement sources, in index order.
 * @returns {{source: string, failure: {source: string, reason: string}|null}}
 *   The emitted assignments, or the first source that would not compile.
 */
export function buildProgramTables(className, expressions, statements) {
  const exprs = buildIndexedTable(className, '__axProgramExprs', expressions, compileExpressionToSource);
  if (exprs.failure) return { source: '', failure: exprs.failure };
 
  const stmts = buildIndexedTable(className, '__axProgramStmts', statements, generateBody);
  if (stmts.failure) return { source: '', failure: stmts.failure };
 
  const parts = [exprs.source, stmts.source].filter(Boolean);
  return { source: parts.length > 0 ? `\n${parts.join('\n')}\n` : '', failure: null };
}
 
/**
 * @typedef {object} CompiledTable
 * @property {string} source - The `Name.__axExprs = {...}` statements, or ''.
 * @property {number} compiled - How many sources compiled.
 * @property {number} skipped - How many were left to the runtime.
 * @property {Array<{source: string, reason: string}>} refusals - Security refusals.
 * @property {Array<{source: string, reason: string}>} gaps - Language gaps.
 * @property {Set<string>} [compiledActions] - Action names that compiled.
 * @property {Set<string>} [compiledResources] - Resource names that compiled.
 */
 
/**
 * Compiles a set of sources into table entries.
 * @param {string[]} sources - The expression or statement sources.
 * @param {function(string): string} generate - The generator to apply.
 * @param {object} accumulator - Collects refusals and gaps.
 * @returns {string[]} Entry source lines.
 */
function buildEntries(sources, generate, accumulator) {
  const entries = [];
  for (const source of sources) {
    try {
      entries.push(`  ${JSON.stringify(source)}: ${generate(source)}`);
    } catch (error) {
      if (!(error instanceof ExpressionCodegenError)) {
        throw error;
      }
      const record = { source, reason: error.message };
      if (SECURITY_REFUSAL.test(error.message)) {
        accumulator.refusals.push(record);
      } else {
        accumulator.gaps.push(record);
      }
    }
  }
  return entries;
}
 
/**
 * Compiles a set of named bodies into table entries.
 *
 * Actions and resources are addressed by name at run time, not by source, so
 * they get their own table. Keying by name is what allows a production build to
 * leave the body text out of the bundle entirely: nothing has to match a string
 * in order to find the implementation.
 * @param {Object<string, string>} bodies - Sources keyed by name.
 * @param {object} accumulator - Collects refusals and gaps.
 * @param {{ambient?: boolean}} [options] - Compilation options for each body.
 * @returns {{entries: string[], compiledNames: Set<string>}} Entry lines and what compiled.
 */
function buildNamedEntries(bodies, accumulator, options = {}) {
  const entries = [];
  const compiledNames = new Set();
  for (const [name, source] of Object.entries(bodies || {})) {
    if (typeof source !== 'string' || source.trim() === '') continue;
    try {
      entries.push(`  ${JSON.stringify(name)}: ${generateBody(source, options)}`);
      compiledNames.add(name);
    } catch (error) {
      if (!(error instanceof ExpressionCodegenError)) {
        throw error;
      }
      const record = { source, reason: error.message };
      if (SECURITY_REFUSAL.test(error.message)) {
        accumulator.refusals.push(record);
      } else {
        accumulator.gaps.push(record);
      }
    }
  }
  return { entries, compiledNames };
}
 
/**
 * Compiles one executable body, preferring the expression-program generator.
 *
 * A run of expressions -- `count++`, `busy = true; save()` -- takes the same
 * path a template binding does, so it emits the same guarded primitives.
 * Anything with real statement syntax goes to the action compiler, which parses
 * it properly and rewrites only how free identifiers resolve.
 * @param {string} source - The body source.
 * @param {{ambient?: boolean}} [options] - `ambient: true` for an `<action>` or `<resource>` body.
 * @returns {string} JavaScript source for the function.
 */
function generateBody(source, options = {}) {
  try {
    return compileStatementsToSource(source, options);
  } catch (error) {
    if (!(error instanceof ExpressionCodegenError) || SECURITY_REFUSAL.test(error.message)) {
      throw error;
    }
    return compileActionToSource(source, options);
  }
}
 
/**
 * Builds the compiled-expression statics for one component class.
 * @param {string} className - The generated class name.
 * @param {object} collected - Output of {@link module:lib/compiler/codegen/collect}.
 * @param {string[]} collected.expressions - Value expressions.
 * @param {string[]} collected.statements - Inline handler bodies.
 * @param {Object<string, string>} [collected.actions] - Action bodies by name.
 * @param {Object<string, string>} [collected.resources] - Resource bodies by name.
 * @returns {CompiledTable} The generated source and what it covers.
 */
export function buildExpressionTable(className, collected) {
  const accumulator = { refusals: [], gaps: [] };
 
  const expressionEntries = buildEntries(collected.expressions || [], compileExpressionToSource, accumulator);
 
  const statementEntries = buildEntries(collected.statements || [], generateBody, accumulator);
  // Action and resource bodies are ordinary JavaScript and may use browser
  // globals; inline handlers above keep the template rule.
  const actions = buildNamedEntries(collected.actions, accumulator, { ambient: true });
  const resources = buildNamedEntries(collected.resources, accumulator, { ambient: true });
 
  const parts = [];
  if (expressionEntries.length > 0) {
    parts.push(`${className}.__axExprs = {\n${expressionEntries.join(',\n')}\n};`);
  }
  if (statementEntries.length > 0) {
    parts.push(`${className}.__axStmts = {\n${statementEntries.join(',\n')}\n};`);
  }
  if (actions.entries.length > 0) {
    parts.push(`${className}.__axActions = {\n${actions.entries.join(',\n')}\n};`);
  }
  if (resources.entries.length > 0) {
    parts.push(`${className}.__axResources = {\n${resources.entries.join(',\n')}\n};`);
  }
 
  return {
    source: parts.length > 0 ? `\n${parts.join('\n')}\n` : '',
    compiled:
      expressionEntries.length + statementEntries.length + actions.entries.length + resources.entries.length,
    skipped: accumulator.refusals.length + accumulator.gaps.length,
    refusals: accumulator.refusals,
    gaps: accumulator.gaps,
    compiledActions: actions.compiledNames,
    compiledResources: resources.compiledNames,
  };
}