import { parseFiles } from "@ast-grep/napi"; import MagicString from "magic-string"; import { chalk, fs, path } from "zx"; import { errors } from "./errors.js"; import { root } from "./utils.js"; /** * @typedef {import("@ast-grep/napi").SgNode} SgNode */ const export_lentgh = "export".length; export function ast_grep() { const task_queue = []; const task = parseFiles([root("esm")], (err, tree) => { const filename = path.basename(tree.filename(), ".js"); if (filename === "index") { return; } const source = new MagicString(tree.root().text()); source.prepend(`"use strict";\n\n`); if (filename.startsWith("_ts")) { const match = tree.root().find(`export { $NAME as _, $NAME as $ALIAS } from "tslib"`); if (match) { const name = match.getMatch("NAME").text(); const alias = match.getMatch("ALIAS").text(); if (alias !== filename) { report_ts_mismatch(tree.filename(), match); } const range = match.range(); source.update( range.start.index, range.end.index, `exports._ = exports.${alias} = require("tslib").${name};`, ); task_queue.push( fs.writeFile(root("cjs", `${filename}.cjs`), source.toString(), { encoding: "utf-8", }), ); } else { report_noexport(tree.filename()); } return; } // rewrite export named function const match = tree.root().find({ rule: { kind: "export_statement", pattern: "export function $FUNC($$$){$$$}", }, }); if (match) { const func = match.getMatch("FUNC"); const func_name = func.text(); if (func_name !== filename) { report_export_mismatch(tree.filename(), match); } const export_start = match.range().start.index; const export_end = export_start + export_lentgh; source.update( export_start, export_end, `exports._ = exports.${func_name} = ${func_name};`, ); match .findAll({ rule: { pattern: func_name, kind: "identifier", inside: { kind: "assignment_expression", field: "left" }, }, }) .forEach((match) => { const range = match.range(); source.prependLeft(range.start.index, `exports._ = exports.${func_name} = `); }); const export_shortname = `export { ${func_name} as _}`; const export_alias = tree.root().find(export_shortname); if (!export_alias) { task_queue.push( fs.appendFile(tree.filename(), export_shortname, "utf-8"), ); } else { const range = export_alias.range(); source.remove(range.start.index, range.end.index); } } else { report_noexport(tree.filename(tree.filename())); } // rewrite import tree .root() .findAll({ rule: { pattern: `import { $BINDING } from "$SOURCE"` } }) .forEach((match) => { const import_binding = match.getMatch("BINDING").text(); const import_source = match.getMatch("SOURCE").text(); const import_basename = path.basename(import_source, ".js"); if (import_binding !== import_basename) { report_import_mismatch(tree.filename(), match); } const range = match.range(); source.update( range.start.index, range.end.index, `var ${import_binding} = require("./${import_binding}.cjs");`, ); tree .root() .findAll({ rule: { pattern: import_binding, kind: "identifier", inside: { not: { kind: "import_specifier", }, }, }, }) .forEach((match) => { const range = match.range(); const ref_name = match.text(); source.update( range.start.index, range.end.index, `${ref_name}._`, ); }); }); task_queue.push( fs.writeFile(root("cjs", `${filename}.cjs`), source.toString(), { encoding: "utf-8", }), ); }); task_queue.push(task); return task_queue; } /** * @param {string} filename * @param {SgNode} match */ function report_ts_mismatch(filename, match) { const range = match.getMatch("ALIAS").range(); errors.push( [ `${chalk.bold.red("error")}: mismatch exported function name.`, "", `${chalk.blue("-->")} ${filename}:${match.range().start.line + 1}`, "", match.text(), chalk.red( [ " ".repeat(range.start.column), "^".repeat(range.end.column - range.start.column), ] .join(""), ), `${ chalk.bold( "note:", ) } The exported name should be the same as the filename.`, "", ] .join("\n"), ); } /** * @param {string} filename * @param {SgNode} match */ function report_export_mismatch(filename, match) { const func = match.getMatch("FUNC"); const func_range = func.range(); const text = match.text().split("\n"); const offset = func_range.start.line - match.range().start.line; text.splice( offset + 1, text.length, chalk.red( [ " ".repeat(func_range.start.column), "^".repeat(func_range.end.column - func_range.start.column), ] .join(""), ), ); errors.push( [ `${chalk.bold.red("error")}: mismatch exported function name.`, "", `${chalk.blue("-->")} ${filename}:${func_range.start.line + 1}:${func_range.start.column + 1}`, "", ...text, "", `${ chalk.bold( "note:", ) } The exported name should be the same as the filename.`, "", ] .join("\n"), ); } /** * @param {string} filename * @param {SgNode} match */ function report_import_mismatch(filename, match) { const binding_range = match.getMatch("BINDING").range(); const source_range = match.getMatch("SOURCE").range(); errors.push( [ `${chalk.bold.red("error")}: mismatch imported binding name.`, "", `${chalk.blue("-->")} ${filename}:${match.range().start.line + 1}`, "", match.text(), [ " ".repeat(binding_range.start.column), chalk.red("^".repeat(binding_range.end.column - binding_range.start.column)), " ".repeat(source_range.start.column - binding_range.end.column), chalk.blue("-".repeat(source_range.end.column - source_range.start.column)), ] .join(""), `${ chalk.bold( "note:", ) } The imported binding name should be the same as the import source basename.`, "", ] .join("\n"), ); } /** * @param {string} filename */ function report_noexport(filename) { errors.push( [`${chalk.bold.red("error")}: exported name not found`, `${chalk.blue("-->")} ${filename}`].join("\n"), ); }