utils.js

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isValidFile = exports.isValidFileSync = exports.loadSnippet = exports.loadSnippetSync = exports.evalHelper = exports.evalHelperSync = exports.evalSnippet = exports.evalSnippetSync = exports.glob = exports.globSync = exports.rewriteSnippetToSyncVersion = exports.rewriteSnippetToAsyncVersion = exports.arrayBody = void 0;
const typescript_1 = __importStar(require("typescript"));
const fs_1 = __importStar(require("fs"));
const child_process_1 = require("child_process");
const path_1 = __importDefault(require("path"));
const fast_glob_1 = __importDefault(require("fast-glob"));
const sync_fetch_1 = __importDefault(require("sync-fetch"));
const url_1 = require("url");
const node_query_1 = __importDefault(require("@synvert-hq/node-query"));
const node_mutation_1 = __importDefault(require("@synvert-hq/node-mutation"));
const errors_1 = require("./errors");
const configuration_1 = __importDefault(require("./configuration"));
const REWRITER_METHODS = "addFile removeFile renameFile withinFiles withinFile addSnippet";
const SCOPE_METHODS = "withinNode withNode findNode gotoNode";
const CONDITION_METHODS = "ifExistNode unlessExistNode ifOnlyExistNode ifAllNodes";
// delete is a reserved word, we define another expression in GLOBAL_DSL_QUERY
const ACTION_METHODS = "group append prepend insert insertAfter insertBefore remove replace replaceWith noop";
const ALL_METHODS = `configure description ifNode ifNpm ${REWRITER_METHODS} ${SCOPE_METHODS} ${CONDITION_METHODS} ${ACTION_METHODS} callHelper wrapWithQuotes appendSemicolon addLeadingSpaces indent`;
const arrayBody = (node, nodeQueryAdapter) => {
    switch (nodeQueryAdapter.getNodeType(node)) {
        case "SourceFile":
            return node["statements"];
        case "ClassDeclaration":
            return node["members"];
        case "ClassDefinition":
            return node["body"]["body"];
        case "MethodDefinition":
            return node["value"]["body"]["body"];
        default:
            return node["body"];
    }
};
exports.arrayBody = arrayBody;
const NEW_REWRITER_WITH_FUNCTION_QUERY = new node_query_1.default(`.NewExpression[expression=Synvert.Rewriter][arguments.length=3][arguments.2=.FunctionExpression[modifiers=undefined]]`, { adapter: "typescript" });
const NEW_HELPER_WITH_FUNCTION_QUERY = new node_query_1.default(`.NewExpression[expression=Synvert.Helper][arguments.length=2][arguments.1=.FunctionExpression[modifiers=undefined]]`, { adapter: "typescript" });
const NEW_INSTANCE_WITH_FUNCTION_QUERY = new node_query_1.default(`.CallExpression[expression=.PropertyAccessExpression[expression=.ThisKeyword]
    [name IN (withinFiles withinFile)]][arguments.length=2][arguments.1=.FunctionExpression[modifiers=undefined]]`, { adapter: "typescript" });
const SCOPES_AND_CONDITIONS_QUERY = new node_query_1.default(`.CallExpression[expression=.PropertyAccessExpression[expression=.ThisKeyword]
    [name IN (${SCOPE_METHODS} ${CONDITION_METHODS})]]
    [arguments.-1.nodeType IN (FunctionExpression ArrowFunction)][arguments.-1.modifiers=undefined]`, { adapter: "typescript" });
const GROUPS_QUERY = new node_query_1.default(`.CallExpression[expression=.PropertyAccessExpression[expression=.ThisKeyword]
    [name=group]]
    [arguments.-1.nodeType IN (FunctionExpression ArrowFunction)][arguments.-1.modifiers=undefined]`, { adapter: "typescript" });
const ASYNC_METHODS_QUERY = new node_query_1.default(`.CallExpression[expression=.PropertyAccessExpression[expression=.ThisKeyword]
    [name IN (${REWRITER_METHODS} callHelper group ${SCOPE_METHODS} ${CONDITION_METHODS})]]`, { adapter: "typescript" });
/**
 * Rewrite javascript snippet to async version.
 */
const rewriteSnippetToAsyncVersion = (snippet, insertRequire = true) => {
    let newSnippet = addProperScopeToSnippet(snippet);
    if (insertRequire) {
        newSnippet = insertRequireSynvertCoreToSnippet(newSnippet);
    }
    const node = parseCode(newSnippet);
    const mutation = new node_mutation_1.default(newSnippet, {
        adapter: "typescript",
    });
    NEW_REWRITER_WITH_FUNCTION_QUERY.queryNodes(node).forEach((node) => mutation.insert(node, "async ", { at: "beginning", to: "arguments.-1" }));
    NEW_HELPER_WITH_FUNCTION_QUERY.queryNodes(node).forEach((node) => mutation.insert(node, "async ", { at: "beginning", to: "arguments.-1" }));
    NEW_INSTANCE_WITH_FUNCTION_QUERY.queryNodes(node).forEach((node) => mutation.insert(node, "async ", { at: "beginning", to: "arguments.-1" }));
    SCOPES_AND_CONDITIONS_QUERY.queryNodes(node).forEach((node) => mutation.insert(node, "async ", { at: "beginning", to: "arguments.-1" }));
    GROUPS_QUERY.queryNodes(node).forEach((node) => mutation.insert(node, "async ", { at: "beginning", to: "arguments.-1" }));
    ASYNC_METHODS_QUERY.queryNodes(node).forEach((node) => {
        if (node.parent.kind != typescript_1.SyntaxKind.AwaitExpression) {
            mutation.insert(node, "await ", { at: "beginning" });
        }
    });
    const { affected, newSource } = mutation.process();
    return affected ? newSource : newSnippet;
};
exports.rewriteSnippetToAsyncVersion = rewriteSnippetToAsyncVersion;
const SYNC_METHODS_QUERY = new node_query_1.default(`.CallExpression[expression=.PropertyAccessExpression[expression=.ThisKeyword]
    [name IN (${REWRITER_METHODS} callHelper ${SCOPE_METHODS} ${CONDITION_METHODS})]]`, { adapter: "typescript" });
/**
 * Rewrite javascript snippet to sync version.
 */
const rewriteSnippetToSyncVersion = (snippet, insertRequire = true) => {
    let newSnippet = addProperScopeToSnippet(snippet);
    if (insertRequire) {
        newSnippet = insertRequireSynvertCoreToSnippet(newSnippet);
    }
    const node = parseCode(newSnippet);
    const mutation = new node_mutation_1.default(newSnippet, {
        adapter: "typescript",
    });
    SYNC_METHODS_QUERY.queryNodes(node).forEach((node) => mutation.insert(node, "Sync", { at: "end", to: "expression" }));
    const { affected, newSource } = mutation.process();
    return affected ? newSource : newSnippet;
};
exports.rewriteSnippetToSyncVersion = rewriteSnippetToSyncVersion;
const NOT_HAS_REQUIRE_SYNVERT_CORE_QUERY = new node_query_1.default(`:not_has(.FirstStatement[declarationList=.VariableDeclarationList[declarations.length=1][declarations.0=.VariableDeclaration[name=Synvert][initializer=.CallExpression[expression=require][arguments.length=1][arguments.0=.StringLiteral[text="@synvert-hq/synvert-core"]]]]])`, { adapter: "typescript" });
const insertRequireSynvertCoreToSnippet = (snippet) => {
    const node = parseCode(snippet);
    const mutation = new node_mutation_1.default(snippet, {
        adapter: "typescript",
    });
    NOT_HAS_REQUIRE_SYNVERT_CORE_QUERY.queryNodes(node).forEach((node) => {
        mutation.insert(node, `const Synvert = require("@synvert-hq/synvert-core");\n`, {
            at: "beginning",
        });
    });
    const { affected, newSource } = mutation.process();
    return affected ? newSource : snippet;
};
const NEW_REWRITER_WITH_ARROW_FUNCTION_QUERY = new node_query_1.default(`.NewExpression[expression=Synvert.Rewriter][arguments.length=3][arguments.2=.ArrowFunction]`, { adapter: "typescript" });
const NEW_HELPER_WITH_ARROW_FUNCTION_QUERY = new node_query_1.default(`.NewExpression[expression=Synvert.Helper][arguments.length=2][arguments.1=.ArrowFunction]`, { adapter: "typescript" });
const NEW_INSTANCE_WITH_ARROW_FUNCTION_QUERY = new node_query_1.default(`.CallExpression[expression IN (withinFiles withinFile)][arguments.length=2][arguments.1=.ArrowFunction]`, { adapter: "typescript" });
const GLOBAL_DSL_QUERY = new node_query_1.default(`.CallExpression[expression IN (${ALL_METHODS})],
  .DeleteExpression[expression=.ParenthesizedExpression[expression.nodeType IN (StringLiteral ArrayLiteralExpression)]],
  .DeleteExpression[expression=.ParenthesizedExpression[expression=.BinaryExpression[left.nodeType IN (StringLiteral ArrayLiteralExpression)][right.nodeType=ObjectLiteralExpression]]]`, { adapter: "typescript" });
const addProperScopeToSnippet = (snippet) => {
    const node = parseCode(snippet);
    const mutation = new node_mutation_1.default(snippet, {
        adapter: "typescript",
    });
    NEW_REWRITER_WITH_ARROW_FUNCTION_QUERY.queryNodes(node).forEach((node) => {
        mutation.delete(node, "arguments.2.equalsGreaterThanToken");
        mutation.insert(node, "function ", { at: "beginning", to: "arguments.2" });
    });
    NEW_HELPER_WITH_ARROW_FUNCTION_QUERY.queryNodes(node).forEach((node) => {
        mutation.delete(node, "arguments.1.equalsGreaterThanToken");
        mutation.insert(node, "function ", { at: "beginning", to: "arguments.1" });
    });
    NEW_INSTANCE_WITH_ARROW_FUNCTION_QUERY.queryNodes(node).forEach((node) => {
        mutation.delete(node, "arguments.1.equalsGreaterThanToken");
        mutation.insert(node, "function ", { at: "beginning", to: "arguments.1" });
    });
    GLOBAL_DSL_QUERY.queryNodes(node).forEach((node) => {
        mutation.insert(node, "this.", { at: "beginning" });
    });
    const { affected, newSource } = mutation.process();
    return affected ? newSource : snippet;
};
const parseCode = (snippet) => {
    return typescript_1.default.createSourceFile("test.js", snippet, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.JS);
};
function runShellCommandSync(command, args, input) {
    const child = (0, child_process_1.spawnSync)(command, args, {
        cwd: configuration_1.default.rootPath,
        input: input,
    });
    let output = child.stdout ? child.stdout.toString() : "";
    let error = child.stderr ? child.stderr.toString() : "";
    return { stdout: output, stderr: error };
}
/**
 * Sync to glob matching files.
 * @param {string} filePattern file pattern
 * @returns {string[]} matching files
 */
const globSync = (filePattern) => {
    const onlyPaths = configuration_1.default.onlyPaths.length > 0 ? configuration_1.default.onlyPaths : [""];
    const fsStats = fast_glob_1.default.sync(onlyPaths.map((onlyPath) => path_1.default.join(onlyPath, filePattern)), {
        ignore: configuration_1.default.skipPaths,
        cwd: configuration_1.default.rootPath,
        onlyFiles: true,
        unique: true,
        stats: true,
    });
    const allPaths = fsStats
        .filter((fsStat) => fsStat.stats.size < configuration_1.default.maxFileSize)
        .map((fsStat) => fsStat.path);
    if (configuration_1.default.respectGitignore) {
        const { stdout } = runShellCommandSync("git", ["check-ignore", "--stdin"], allPaths.join("\n"));
        const ignoredPaths = new Set(stdout.split("\n").filter(Boolean));
        return allPaths.filter((path) => !ignoredPaths.has(path));
    }
    return allPaths;
};
exports.globSync = globSync;
function runShellCommand(command, args, input) {
    return new Promise((resolve) => {
        const child = (0, child_process_1.spawn)(command, args, { cwd: configuration_1.default.rootPath });
        if (child.stdin && input) {
            child.stdin.write(input);
            child.stdin.end();
        }
        let output = "";
        if (child.stdout) {
            child.stdout.on("data", (data) => {
                output += data;
            });
        }
        let error = "";
        if (child.stderr) {
            child.stderr.on("data", (data) => {
                error += data;
            });
        }
        child.on("error", (e) => {
            return resolve({ stdout: "", stderr: e.message });
        });
        child.on("exit", () => {
            return resolve({ stdout: output, stderr: error });
        });
    });
}
/**
 * Async to glob matching files.
 * @async
 * @param {string} filePattern file pattern
 * @returns {Promise<string[]>} matching files
 */
const glob = (filePattern) => __awaiter(void 0, void 0, void 0, function* () {
    const onlyPaths = configuration_1.default.onlyPaths.length > 0 ? configuration_1.default.onlyPaths : [""];
    const fsStats = yield (0, fast_glob_1.default)(onlyPaths.map((onlyPath) => path_1.default.join(onlyPath, filePattern)), {
        ignore: configuration_1.default.skipPaths,
        cwd: configuration_1.default.rootPath,
        onlyFiles: true,
        unique: true,
        stats: true,
    });
    const allPaths = fsStats
        .filter((fsStat) => fsStat.stats.size < configuration_1.default.maxFileSize)
        .map((fsStat) => fsStat.path);
    if (configuration_1.default.respectGitignore) {
        const { stdout } = yield runShellCommand("git", ["check-ignore", "--stdin"], allPaths.join("\n"));
        const ignoredPaths = new Set(stdout.split("\n").filter(Boolean));
        return allPaths.filter((path) => !ignoredPaths.has(path));
    }
    return allPaths;
});
exports.glob = glob;
/**
 * Sync to eval the snippet by name.
 * @param {string} snippetName - snippet name, it can be a http url, file path or short snippet name.
 * @returns {Rewriter} a Rewriter object
 */
const evalSnippetSync = (snippetName) => {
    return eval((0, exports.loadSnippetSync)(snippetName));
};
exports.evalSnippetSync = evalSnippetSync;
/**
 * Async to eval the snippet by name.
 * @async
 * @param {string} snippetName - snippet name, it can be a http url, file path or short snippet name.
 * @returns {Promise<Rewriter>} a Rewriter object
 */
const evalSnippet = (snippetName) => __awaiter(void 0, void 0, void 0, function* () {
    return eval(yield (0, exports.loadSnippet)(snippetName));
});
exports.evalSnippet = evalSnippet;
/**
 * Sync to eval the helper by name.
 * @param {string} helperName - helper name, it can be a http url, file path or helper name.
 * @returns {Helper} a Helper object
 */
const evalHelperSync = (helperName) => {
    return eval((0, exports.loadSnippetSync)(helperName));
};
exports.evalHelperSync = evalHelperSync;
/**
 * Async to eval the helper by name.
 * @async
 * @param {string} helperName - helper name, it can be a http url, file path or helper name.
 * @returns {Promise<Helper>} a Helper object
 */
const evalHelper = (helperName) => __awaiter(void 0, void 0, void 0, function* () {
    return eval(yield (0, exports.loadSnippet)(helperName));
});
exports.evalHelper = evalHelper;
/**
 * Sync to load snippet by snippet name.
 * @param {string} snippetName - snippet name, it can be a http url, file path or short snippet name.
 * @param {boolean} insertRequire - insert require statement or not
 * @returns {string} snippet helper content
 * @throws {SnippetNotFoundError} snippet not found
 */
const loadSnippetSync = (snippetName, insertRequire = true) => {
    const snippetContent = loadSnippetContentSync(snippetName);
    return (0, exports.rewriteSnippetToSyncVersion)(snippetContent, insertRequire);
};
exports.loadSnippetSync = loadSnippetSync;
/**
 * Sync to load snippet by snippet name.
 * @async
 * @param {string} snippetName - snippet name, it can be a http url, file path or short snippet name.
 * @param {boolean} insertRequire - insert require statement or not
 * @returns {Promise<string>} snippet helper content
 * @throws {SnippetNotFoundError} snippet not found
 */
const loadSnippet = (snippetName, insertRequire = true) => __awaiter(void 0, void 0, void 0, function* () {
    const snippetContent = yield loadSnippetContent(snippetName);
    return (0, exports.rewriteSnippetToAsyncVersion)(snippetContent, insertRequire);
});
exports.loadSnippet = loadSnippet;
/**
 * Sync to check if it is a valid file path.
 * @param {string} path - file path
 * @returns {boolean} gets true it is a valid file
 */
const isValidFileSync = (path) => {
    try {
        const stats = fs_1.default.statSync(path);
        return stats.isFile();
    }
    catch (_a) {
        return false;
    }
};
exports.isValidFileSync = isValidFileSync;
/**
 * Async to check if it is a valid file path.
 * @async
 * @param {string} path - file path
 * @returns {Promise<boolean>} gets true it is a valid file
 */
const isValidFile = (path) => __awaiter(void 0, void 0, void 0, function* () {
    try {
        const stats = yield fs_1.promises.stat(path);
        return stats.isFile();
    }
    catch (_a) {
        return false;
    }
});
exports.isValidFile = isValidFile;
const isValidUrl = (urlString) => {
    try {
        const url = new url_1.URL(urlString);
        return url.protocol === "http:" || url.protocol === "https:";
    }
    catch (_a) {
        return false;
    }
};
const formatUrl = (url) => convertToGithubRawUrl(url);
const snippetExpandPath = (snippetName) => path_1.default.join(snippetsHome(), "lib", `${snippetName}.js`);
const remoteSnippetExistsSync = (snippetPath) => (0, sync_fetch_1.default)(snippetPath).status === 200;
const remoteSnippetExists = (snippetPath) => __awaiter(void 0, void 0, void 0, function* () {
    const response = yield fetch(snippetPath);
    return response.status === 200;
});
const remoteSnippetUrl = (snippetName) => `https://github.com/synvert-hq/synvert-snippets-javascript/blob/main/lib/${snippetName}.js`;
const snippetsHome = () => {
    return (process.env.SYNVERT_SNIPPETS_HOME ||
        path_1.default.join(process.env.HOME, ".synvert-javascript"));
};
const convertToGithubRawUrl = (url) => {
    if (url.startsWith("https://github.com/")) {
        return url
            .replace("//github.com/", "//raw.githubusercontent.com/")
            .replace("/blob/", "/");
    }
    if (url.startsWith("https://gist.github.com")) {
        return (url.replace("gist.github.com/", "gist.githubusercontent.com/") + "/raw");
    }
    return url;
};
const loadSnippetContent = (snippetName) => __awaiter(void 0, void 0, void 0, function* () {
    if (isValidUrl(snippetName)) {
        const snippetUrl = formatUrl(snippetName);
        if (yield remoteSnippetExists(snippetUrl)) {
            const response = yield fetch(snippetUrl);
            return yield response.text();
        }
        throw new errors_1.SnippetNotFoundError(`${snippetName} not found`);
    }
    else if (yield (0, exports.isValidFile)(snippetName)) {
        return yield fs_1.promises.readFile(snippetName, "utf-8");
    }
    else {
        const snippetPath = snippetExpandPath(snippetName);
        if (yield (0, exports.isValidFile)(snippetPath)) {
            return yield fs_1.promises.readFile(snippetPath, "utf-8");
        }
        const snippetUrl = formatUrl(remoteSnippetUrl(snippetName));
        if (yield remoteSnippetExists(snippetUrl)) {
            const response = yield fetch(snippetUrl);
            return yield response.text();
        }
        throw new errors_1.SnippetNotFoundError(`${snippetName} not found`);
    }
});
const loadSnippetContentSync = (snippetName) => {
    if (isValidUrl(snippetName)) {
        const snippetUrl = formatUrl(snippetName);
        if (remoteSnippetExistsSync(snippetUrl)) {
            return (0, sync_fetch_1.default)(snippetUrl).text();
        }
        throw new errors_1.SnippetNotFoundError(`${snippetName} not found`);
    }
    else if ((0, exports.isValidFileSync)(snippetName)) {
        return fs_1.default.readFileSync(snippetName, "utf-8");
    }
    else {
        const snippetPath = snippetExpandPath(snippetName);
        if ((0, exports.isValidFileSync)(snippetPath)) {
            return fs_1.default.readFileSync(snippetPath, "utf-8");
        }
        const snippetUrl = formatUrl(remoteSnippetUrl(snippetName));
        if (remoteSnippetExistsSync(snippetUrl)) {
            return (0, sync_fetch_1.default)(snippetUrl).text();
        }
        throw new errors_1.SnippetNotFoundError(`${snippetName} not found`);
    }
};