unplugin-quansync
Version:
Write async functions, get both async and sync functions
121 lines (117 loc) • 4.08 kB
JavaScript
import { createUnplugin } from "unplugin";
import { createFilter } from "unplugin-utils";
import { babelParse, getLang, isCallOf, isFunctionType, isTypeOf, walkAST, walkImportDeclaration } from "ast-kit";
import { MagicString, generateTransform } from "magic-string-ast";
//#region src/core/index.ts
const THIS_REGEX = /\bthis\b/;
const ARROW_FN_START = `\nreturn function* () {`;
const ARROW_FN_END = `}.call(this)\n`;
function transformQuansync(code, id) {
const lang = getLang(id);
const program = babelParse(code, lang, { createParenthesizedExpressions: true });
const imports = Object.create(null);
for (const node of program.body) if (node.type === "ImportDeclaration") walkImportDeclaration(imports, node);
const macroName = Object.values(imports).find((i) => i.source === "quansync/macro" && i.imported === "quansync")?.local;
if (!macroName) return;
const s = new MagicString(code);
const functionScopes = [];
const nodeStack = [];
function findUpExpressionStatement() {
for (let i = nodeStack.length - 1; i >= 0; i--) {
const node = nodeStack[i];
if (isFunctionType(node) || node.type === "BlockStatement") return;
if (node.type === "ExpressionStatement") return node;
}
}
function prependSemi(stmt) {
if (stmt.semi) return;
s.prependLeft(stmt.start, `;`);
stmt.semi = true;
}
walkAST(program, {
enter(node, parent) {
nodeStack.push(node);
if (node.type === "AwaitExpression" && functionScopes.at(-1)) {
const needParen = isTypeOf(parent, [
"UnaryExpression",
"BinaryExpression",
"LogicalExpression",
"TSAsExpression",
"TSSatisfiesExpression"
]);
s.overwrite(node.start, node.argument.start, `${needParen ? "(" : ""}yield `);
if (needParen) {
s.appendLeft(node.end, ")");
const stmt = findUpExpressionStatement();
if (stmt && stmt.start === node.start) prependSemi(stmt);
}
return;
}
if (!isFunctionType(node)) return;
const inMacroFunction = isCallOf(parent, macroName);
functionScopes.push(inMacroFunction);
if (!inMacroFunction || !node.async) return;
const name = "id" in node && node.id ? node.id.name : "";
const isArrowFunction = node.type === "ArrowFunctionExpression";
const body = s.slice(node.body.start, node.body.end);
const hasParentThis = isArrowFunction && THIS_REGEX.test(body);
if (hasParentThis) {
rewriteFunctionSignature(node, "(", ") => ");
rewriteFunctionBody(node, ARROW_FN_START, ARROW_FN_END);
} else {
rewriteFunctionSignature(node, `function* ${name}(`, ") ");
rewriteFunctionBody(node);
}
},
leave(node) {
nodeStack.pop();
if (isFunctionType(node)) functionScopes.pop();
}
});
return generateTransform(s, id);
function rewriteFunctionSignature(node, start, end) {
const firstParam = node.params[0];
if (firstParam) {
s.overwrite(node.start, firstParam.start, start);
s.overwrite(node.params.at(-1).end, node.body.start, end);
} else s.overwrite(node.start, node.body.start, start + end);
}
function rewriteFunctionBody(node, prefix = "", suffix = "") {
if (node.body.type === "BlockStatement") {
s.appendLeft(node.body.start + 1, prefix);
s.appendLeft(node.body.end - 1, suffix);
} else {
s.appendLeft(node.body.start, `{\n${prefix}return `);
s.appendLeft(node.body.end, `${suffix}\n}`);
}
}
}
//#endregion
//#region src/core/options.ts
function resolveOptions(options) {
return {
include: options.include || [/\.[cm]?[jt]sx?$/],
exclude: options.exclude || [/node_modules/],
enforce: "enforce" in options ? options.enforce : "pre"
};
}
//#endregion
//#region src/index.ts
const Quansync = createUnplugin((rawOptions = {}) => {
const options = resolveOptions(rawOptions);
const filter = createFilter(options.include, options.exclude);
const name = "unplugin-quansync";
return {
name,
enforce: options.enforce,
transformInclude(id) {
return filter(id);
},
transform(code, id) {
if (!code.includes("quansync")) return;
return transformQuansync(code, id);
}
};
});
//#endregion
export { Quansync };