All files / lib/bundler minify.js

96.42% Statements 108/112
89.28% Branches 25/28
100% Functions 3/3
96.42% Lines 108/112

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 113278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 278x 86x 86x 86x 86x 86x 86x 86x 86x 387478x 387478x 25561361x 25561361x 155489x 86x 86x 86x 86x 86x 86x 86x 86x 86x 86x 86x 86x 32315341x 7191493x 32315341x 86x 86x 86x 86x 86x 86x 86x 86x 86x 86x 1741455x 1741455x 1741455x 1741455x 1741455x 1741455x 1741455x 755530x 755530x 755530x 985925x 985925x 985925x 985925x 985925x 985925x 1741455x 28470248x   28470248x       1741455x 86x 86x 86x 60555108x 1741369x 1741369x 1741369x 60555108x 86x 86x 86x 86x  
/**
 * @file minify.js
 * @description Removes what a browser does not need, and nothing else.
 *
 * ## What this does, stated precisely
 *
 * Comments are deleted. Leading and trailing whitespace is trimmed from every
 * line whose margins are code rather than the inside of a string or a template
 * literal. That is the whole transformation.
 *
 * It does **not** rename identifiers, fold constants, remove dead branches or
 * join statements. Those need a real ECMAScript parser to do safely, and a
 * minifier that guesses produces a bundle that is smaller and wrong — which is
 * strictly worse than one that is larger and right. The size this leaves on the
 * table is reported honestly rather than closed by guessing.
 *
 * ## One property worth the constraint
 *
 * Line count is preserved exactly. A deleted block comment leaves its newlines
 * behind; trimming touches only the margins. So the source map emitted for a
 * development build is equally valid for the minified production build, and a
 * production stack trace still names a file and a line the developer wrote.
 * Most minifiers buy their last few per cent by giving that up.
 *
 * Comments and indentation are also what gzip compresses best, so the gap
 * between this and an identifier-mangling minifier is much narrower on the wire
 * than it is on disk. The build reports both numbers.
 * @module lib/bundler/minify
 */
 
import { CodeMask } from './scanner.js';
 
/**
 * Strips comments and indentation from emitted JavaScript.
 * @param {string} code - The bundle source.
 * @returns {string} The same program, with comments and margins removed.
 */
export function minify(code) {
  const mask = new CodeMask(code);
 
  // Comment spans, so a line that is entirely comment collapses to nothing and
  // one that ends in a comment loses the tail. Recorded as ranges rather than
  // by rewriting a character array: the bundle is hundreds of kilobytes and
  // this runs on every build, including every rebuild `avenx watch` performs.
  const comment = new Uint8Array(code.length);
  for (const region of mask.regions) {
    if (region.kind !== 'line-comment' && region.kind !== 'block-comment') continue;
    for (let i = region.start; i < Math.min(region.end, code.length); i += 1) {
      comment[i] = 1;
    }
  }
 
  /**
   * Whether an offset can be dropped from a line's margin.
   *
   * A blanked comment can; a string or template literal's own spacing cannot,
   * because it is a value. That distinction is the whole safety argument for
   * this minifier, and getting it backwards would corrupt every multi-line
   * template in the runtime.
   * @param {number} index - Offset to test.
   * @returns {boolean} True when the character is droppable margin.
   */
  const droppable = (index) => {
    if (comment[index] === 1) return true;
    const char = code[index];
    return (char === ' ' || char === '\t' || char === '\r') && !mask.isLiteral(index);
  };
 
  const out = [];
  let lineStart = 0;
 
  /**
   * Emits one line, trimmed where trimming is safe.
   * @param {number} end - Exclusive end offset of the line.
   */
  const flush = (end) => {
    let from = lineStart;
    let to = end;
 
    while (from < to && droppable(from)) from += 1;
    while (to > from && droppable(to - 1)) to -= 1;
 
    if (to <= from) {
      out.push('');
      return;
    }
 
    // A comment in the middle of a line -- `const x = 1; // why` is handled by
    // the trailing trim, but `foo(/* note */ bar)` is not -- so the interior is
    // copied with comment characters replaced by a single space each.
    let piece = '';
    let spanStart = from;
    for (let i = from; i < to; i += 1) {
      if (comment[i] !== 1) continue;
      piece += code.slice(spanStart, i);
      while (i < to && comment[i] === 1) i += 1;
      piece += ' ';
      spanStart = i;
    }
    out.push(spanStart === from ? code.slice(from, to) : piece + code.slice(spanStart, to));
  };
 
  for (let i = 0; i < code.length; i += 1) {
    if (code[i] === '\n') {
      flush(i);
      lineStart = i + 1;
    }
  }
  flush(code.length);
 
  return out.join('\n');
}