All files / src/jsx-source index.ts

98.21% Statements 55/56
84.37% Branches 27/32
100% Functions 6/6
100% Lines 47/47

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    1x 1x 1x                     1x     17x 17x 644x   17x             19x 19x 19x 10x 10x 4x   19x             1057x 337x 91x 91x   246x 246x 1220x 974x                                   1x 19x   19x   19x     21x   17x   17x 17x   17x             17x     17x 17x 17x       17x   17x 246x 19x   19x       19x   19x                                 19x 19x     17x         17x        
import type { Plugin } from "rollup";
import type { FilterPattern } from "@rollup/pluginutils";
import { createFilter } from "@rollup/pluginutils";
import { getSwc } from "../swc/loadSwc.js";
import { buildSourceLoc } from "../utils/source-loc";
import type * as swcTypes from "@swc/wasm-web";
 
export interface JsxSourceOptions {
  /** @default ["**\/*.jsx", "**\/*.tsx"] */
  include?: FilterPattern;
  exclude?: FilterPattern;
  /** @default process.cwd() */
  rootDir?: string;
}
 
const DUMMY_SPAN: swcTypes.Span = { start: 0, end: 0, ctxt: 0 };
 
function buildLineTable(code: string): number[] {
  const table: number[] = [0];
  for (let i = 0; i < code.length; i++) {
    if (code[i] === "\n") table.push(i + 1);
  }
  return table;
}
 
function offsetToLineCol(
  table: number[],
  offset: number,
): { line: number; col: number } {
  let lo = 0;
  let hi = table.length - 1;
  while (lo < hi) {
    const mid = (lo + hi + 1) >> 1;
    if (table[mid] <= offset) lo = mid;
    else hi = mid - 1;
  }
  return { line: lo + 1, col: offset - table[lo] + 1 };
}
 
function walkMutate(
  node: swcTypes.Node,
  visit: (node: swcTypes.Node) => void,
): void {
  if (!node || typeof node !== "object") return;
  if (Array.isArray(node)) {
    for (const item of node) walkMutate(item as swcTypes.Node, visit);
    return;
  }
  visit(node);
  for (const key of Object.keys(node)) {
    if (key === "span") continue;
    walkMutate(
      (node as unknown as Record<string, unknown>)[key] as swcTypes.Node,
      visit,
    );
  }
}
 
/**
 * Rollup plugin that injects `data-source-loc="file:line:col"` onto every JSX
 * opening element at build time.
 *
 * Place this plugin **before** `swc()` in your plugin list so it sees raw
 * TypeScript+JSX before compilation:
 *
 * ```ts
 * plugins: [jsxSource({ rootDir: 'src' }), swc()]
 * ```
 */
export function jsxSource(options: JsxSourceOptions = {}): Plugin {
  const { rootDir = process.cwd(), include, exclude } = options;
 
  const filter = createFilter(include ?? ["**/*.jsx", "**/*.tsx"], exclude);
 
  return {
    name: "jsx-source",
    async transform(code, id) {
      if (!filter(id)) return null;
 
      const swc = await getSwc();
 
      const isTs = /\.tsx?$/.test(id);
      const hasJsx = /\.[jt]sx$/.test(id);
 
      const program = await swc.parse(
        code,
        isTs
          ? { syntax: "typescript", tsx: hasJsx }
          : { syntax: "ecmascript", jsx: hasJsx },
      );
 
      const lineTable = buildLineTable(code);
 
      // Compute relative path for the data-source value
      const sep = id.includes("/") ? "/" : "\\";
      const rootWithSep = rootDir.endsWith(sep) ? rootDir : rootDir + sep;
      const relPath = id.startsWith(rootWithSep)
        ? id.slice(rootWithSep.length)
        : id;
 
      const baseOffset: number = program.span?.start ?? 0;
 
      walkMutate(program as swcTypes.Node, (node) => {
        if (node.type !== "JSXOpeningElement") return;
        const jsxNode = node as swcTypes.JSXOpeningElement;
 
        const { line, col } = offsetToLineCol(
          lineTable,
          jsxNode.span.start - baseOffset,
        );
        const locValue = buildSourceLoc(relPath, line, col);
 
        const sourceAttr: swcTypes.JSXAttribute = {
          type: "JSXAttribute",
          span: DUMMY_SPAN,
          name: {
            type: "Identifier",
            span: DUMMY_SPAN,
            value: "data-source-loc",
            optional: false,
          },
          value: {
            type: "StringLiteral",
            span: DUMMY_SPAN,
            value: locValue,
            raw: JSON.stringify(locValue),
          },
        };
 
        Iif (!Array.isArray(jsxNode.attributes)) jsxNode.attributes = [];
        jsxNode.attributes.unshift(sourceAttr);
      });
 
      const result = await swc.print(program, {
        filename: id,
        sourceMaps: true,
      });
 
      return { code: result.code, map: result.map };
    },
  };
}