All files / src/define index.ts

100% Statements 39/39
100% Branches 26/26
100% Functions 7/7
100% Lines 34/34

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                1x 1x 1x 1x                     1x         24x   24x       24x         24x       3x                       33x 31x 27x   25x       47x 47x   2x 2x       25x   23x 22x     23x 23x   23x 28x 27x 26x   26x                     23x   21x                       27x 7x       1x         26x    
import type {
  Plugin,
  PluginContext,
  TransformPluginContext,
  SourceDescription,
} from "rollup";
import type { FilterPattern } from "@rollup/pluginutils";
 
import { createFilter } from "@rollup/pluginutils";
import MagicString from "magic-string";
import astMatcher from "ast-matcher";
import escapeStringRegexp from "escape-string-regexp";
 
export interface DefineOptions {
  include?: FilterPattern;
  exclude?: FilterPattern;
  replacements?: Record<string, string | ((key: string) => string)>;
}
 
type Edit = [number, number];
type AstNode = { start: number; end: number };
 
export default function define({
  include = ["**/*.{svelte,tsx,ts,mjs,jsx,js,cjs}"],
  exclude,
  replacements = {},
}: DefineOptions = {}): Plugin {
  const filter = createFilter(include, exclude);
 
  const keys = Object.keys(replacements);
  let matchers: ReturnType<typeof astMatcher>[];
 
  // eslint-disable-next-line unicorn/no-fn-reference-in-iterator
  const firstpass = new RegExp(
    `(?:${keys.map(escapeStringRegexp).join("|")})`,
    "g",
  );
 
  return {
    name: "define",
    transform,
    renderChunk(code, chunk) {
      return transform.call(this, code, chunk.fileName);
    },
  };
 
  function transform(
    this: {
      parse: TransformPluginContext["parse"];
      warn: TransformPluginContext["warn"];
    },
    code: string,
    id: string,
  ): SourceDescription | null {
    if (keys.length === 0) return null;
    if (!filter(id)) return null;
    if (code.search(firstpass) === -1) return null;
 
    const parse = (
      code: string,
      source = code,
    ): ReturnType<PluginContext["parse"]> => {
      try {
        return this.parse(code, undefined); // eslint-disable-line unicorn/no-useless-undefined
      } catch (error) {
        (error as Error).message += ` in ${source}`;
        throw error;
      }
    };
 
    const ast = parse(code, id);
 
    if (!matchers) {
      matchers = keys.map((key) => astMatcher(parse(key)));
    }
 
    const magicString = new MagicString(code);
    const edits: Edit[] = [];
 
    matchers.forEach((matcher, index) => {
      for (const { node } of (matcher(ast) || []) as { node: AstNode }[]) {
        if (markEdited(node, edits)) {
          const replacement = replacements[keys[index]];
 
          magicString.overwrite(
            node.start,
            node.end,
            typeof replacement === "function"
              ? replacement(keys[index])
              : replacement,
          );
        }
      }
    });
 
    if (edits.length === 0) return null;
 
    return {
      code: magicString.toString(),
      map: magicString.generateMap({
        source: code,
        includeContent: true,
        hires: true,
      }),
    };
  }
}
 
function markEdited(node: AstNode, edits: Edit[]): number | false {
  for (const [start, end] of edits) {
    if (
      (start <= node.start && node.start < end) ||
      (start < node.end && node.end <= end)
    ) {
      return false; // Already edited
    }
  }
 
  // Not edited
  return edits.push([node.start, node.end]);
}