All files index.ts

100% Statements 35/35
100% Branches 25/25
100% Functions 6/6
100% Lines 31/31

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                                                                                        2x     38x 38x 38x 33x 33x 24x 24x 38x 38x   38x 34x 34x                 33x             23x 23x         23x   21x     29x         24x       33x   32x 5x 4x     30x   4x 4x 7x 7x 1x   6x          
import { readFile } from "node:fs/promises";
import path from "node:path";
 
import {
  fromTerserNameCache,
  type MangleCache,
  readMangleCache,
  serializeMangleCache,
  toTerserNameCache,
} from "./cache";
import { type ResolvedInput, resolveInputs } from "./inputs";
import { transformJavaScript } from "./transform";
import { type PendingWrite, readMode, writeBatch } from "./write";
 
export interface MangleOptions {
  /** Cache file owned by the consuming project. @default "mangle-cache.json" */
  cache?: string;
  /** Working directory used to resolve all relative paths. @default process.cwd() */
  cwd?: string;
  /** Files, directories, or glob patterns. @default ["dist"] */
  inputs?: readonly string[];
  /** Enable Terser compression and identifier mangling in addition to property mangling. */
  minify?: boolean;
  /** Write a single input to this file instead of changing it in place. */
  outFile?: string;
  /** Write inputs beneath this directory instead of changing them in place. */
  outDir?: string;
  /** Property names matched by this expression are eligible for mangling. @default /[^_]_$/ */
  pattern?: RegExp;
  /** Property names that must remain unchanged. */
  reserved?: readonly string[];
}
 
export interface MangleFileResult {
  input: string;
  output: string;
}
 
export interface MangleResult {
  cache: Readonly<MangleCache>;
  cachePath: string;
  files: readonly MangleFileResult[];
}
 
const defaultPattern = /[^_]_$/;
 
export async function mangle(options: MangleOptions = {}): Promise<MangleResult> {
  const cwd = path.resolve(options.cwd ?? process.cwd());
  const cachePath = path.resolve(cwd, options.cache ?? "mangle-cache.json");
  const inputs = await resolveInputs(options.inputs ?? [], cwd);
  const outputs = resolveOutputs(inputs, cwd, options);
  const sourceCache = await readMangleCache(cachePath);
  const nameCache = toTerserNameCache(sourceCache);
  const pattern = normalizePattern(options.pattern ?? defaultPattern);
  const reserved = [...new Set(options.reserved ?? [])];
  const transformed: PendingWrite[] = [];
 
  for (const [index, input] of inputs.entries()) {
    const source = await readFile(input.path, "utf8");
    const content = await transformJavaScript({
      filePath: input.path,
      minify: options.minify ?? false,
      nameCache,
      pattern,
      reserved,
      source,
    });
 
    transformed.push({
      content,
      mode: await readMode(input.path),
      target: outputs[index],
    });
  }
 
  const cache = fromTerserNameCache(nameCache);
  transformed.push({
    content: serializeMangleCache(cache),
    mode: await readMode(cachePath),
    target: cachePath,
  });
  await writeBatch(transformed);
 
  return {
    cache,
    cachePath,
    files: inputs.map((input, index) => ({ input: input.path, output: outputs[index] })),
  };
}
 
function normalizePattern(pattern: RegExp): RegExp {
  return new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, ""));
}
 
function resolveOutputs(inputs: readonly ResolvedInput[], cwd: string, options: MangleOptions): string[] {
  if (options.outFile && options.outDir) throw new Error("outFile and outDir cannot be used together");
 
  if (options.outFile) {
    if (inputs.length !== 1) throw new Error("outFile requires exactly one input file");
    return [path.resolve(cwd, options.outFile)];
  }
 
  if (!options.outDir) return inputs.map((input) => input.path);
 
  const outDir = path.resolve(cwd, options.outDir);
  return inputs.map((input) => {
    const relativePath = path.relative(input.root, input.path);
    if (relativePath.startsWith(`..${path.sep}`)) {
      throw new Error(`Cannot preserve ${input.path} beneath output directory ${outDir}`);
    }
    return path.join(outDir, relativePath);
  });
}
 
export type { MangleCache } from "./cache";