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     43x 43x 43x 38x 38x 29x 29x 43x 43x   43x 40x 40x                 39x             28x 28x         28x   26x     35x         29x       38x   37x 6x 5x     35x   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";