magic-regexp
Version:
A compiled-away, type-safe, readable RegExp alternative
85 lines (83 loc) • 3.27 kB
JavaScript
import { createContext, runInContext } from "node:vm";
import * as magicRegExp from "magic-regexp";
import MagicString from "magic-string";
import { createUnplugin } from "unplugin";
//#region src/transform.ts
function walkAST(node, enter) {
enter(node);
const record = node;
for (const key of Object.keys(record)) {
const value = record[key];
if (value && typeof value === "object") {
if (Array.isArray(value)) {
for (const child of value) if (child && typeof child.type === "string") walkAST(child, enter);
} else if ("type" in value && typeof value.type === "string") walkAST(value, enter);
}
}
}
const MAGIC_REGEXP_SPECIFIERS = new Set(["magic-regexp", "magic-regexp/further-magic"]);
const MagicRegExpTransformPlugin = createUnplugin(() => {
return {
name: "MagicRegExpTransformPlugin",
enforce: "post",
transformInclude(id) {
const queryIndex = id.indexOf("?");
const isFound = queryIndex >= 0;
const pathname = isFound ? id.slice(0, queryIndex) : id;
const search = isFound ? id.slice(queryIndex) : "";
const type = isFound ? new URLSearchParams(search).get("type") : null;
if (pathname.endsWith(".vue") && (!search || type === "script")) return true;
if (pathname.match(/\.((c|m)?j|t)sx?$/g)) return true;
return false;
},
transform(code, id) {
if (!code.includes("magic-regexp")) return;
const ast = this.parse(code);
const contextMap = { ...magicRegExp };
const wrapperNames = [];
let namespace;
let hasRelevantImport = false;
walkAST(ast, (node) => {
if (node.type !== "ImportDeclaration") return;
if (typeof node.source.value !== "string" || !MAGIC_REGEXP_SPECIFIERS.has(node.source.value)) return;
hasRelevantImport = true;
for (const spec of node.specifiers) if (spec.type === "ImportNamespaceSpecifier") {
namespace = spec.local.name;
contextMap[spec.local.name] = magicRegExp;
} else if (spec.type === "ImportSpecifier") {
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : String(spec.imported.value);
if (importedName in magicRegExp) contextMap[spec.local.name] = magicRegExp[importedName];
if (importedName === "createRegExp") wrapperNames.push(spec.local.name);
}
});
if (!hasRelevantImport) return;
const context = createContext(contextMap);
const s = new MagicString(code);
walkAST(ast, (node) => {
if (node.type !== "CallExpression") return;
const { callee } = node;
const isDirectCall = callee.type === "Identifier" && wrapperNames.includes(callee.name);
let isNamespacedCall = false;
if (callee.type === "MemberExpression") {
const { object, property } = callee;
isNamespacedCall = object.type === "Identifier" && object.name === namespace && property.type === "Identifier" && property.name === "createRegExp";
}
if (!isDirectCall && !isNamespacedCall) return;
const { start, end } = node;
try {
const value = runInContext(code.slice(start, end), context);
s.overwrite(start, end, value.toString());
} catch {}
});
if (s.hasChanged()) return {
code: s.toString(),
map: s.generateMap({
includeContent: true,
source: id
})
};
}
};
});
//#endregion
export { MagicRegExpTransformPlugin };