rewriter.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 });
const fs_1 = __importStar(require("fs"));
const path_1 = __importDefault(require("path"));
const minimatch_1 = __importDefault(require("minimatch"));
const options_1 = require("./types/options");
const instance_1 = __importDefault(require("./instance"));
const node_version_1 = __importDefault(require("./node-version"));
const npm_version_1 = __importDefault(require("./npm-version"));
const utils_1 = require("./utils");
const configuration_1 = __importDefault(require("./configuration"));
/**
 * Rewriter is the top level namespace in a synvert snippet.
 *
 * One rewriter checks if the dependency version matches, and it can contain one or many Instances,
 * which define the behavior what files need to find and what codes need to rewrite.
 * @borrows Rewriter#withinFiles as Rewriter#withinFile
 */
class Rewriter {
    /**
     * Register a rewriter with its group and name.
     * @static
     * @param {string} group - the rewriter group.
     * @param {string} name - the unique rewriter name.
     * @param {Rewriter} rewriter - the rewriter to register.
     */
    static register(group, name, rewriter) {
        this.rewriters[group] = this.rewriters[group] || {};
        this.rewriters[group][name] = rewriter;
    }
    /**
     * Fetch a rewriter by group and name.
     * @static
     * @param {string} group rewriter group.
     * @param {string} name rewriter name.
     * @returns {Rewriter} the matching rewriter.
     */
    static fetch(group, name) {
        if (this.rewriters[group] && this.rewriters[group][name]) {
            return this.rewriters[group][name];
        }
    }
    /**
     * Clear all registered rewriters.
     */
    static clear() {
        this.rewriters = {};
    }
    /**
     * Create a Rewriter
     * @param {string} group - group name
     * @param {string} name - snippet name
     * @param {Function} func - a function defines the behaviors of the rewriter
     */
    constructor(group, name, func) {
        this.group = group;
        this.name = name;
        this.func = func;
        this.subSnippets = [];
        this.affectedFiles = new Set();
        this.options = {
            sourceType: options_1.SourceType.MODULE,
            parser: options_1.Parser.TYPESCRIPT,
            runInstance: true,
            writeToFile: true,
        };
        this.testResults = [];
        this.withinFileSync = this.withinFilesSync.bind(this);
        this.withinFile = this.withinFiles.bind(this);
        Rewriter.register(group, name, this);
    }
    /**
     * Sync to process the rewriter.
     */
    processSync() {
        this.affectedFiles = new Set();
        this.func.call(this, this);
    }
    /**
     * Async to process the rewriter.
     * @async
     */
    process() {
        return __awaiter(this, void 0, void 0, function* () {
            this.affectedFiles = new Set();
            yield this.func.call(this, this);
        });
    }
    /**
     * Sync to process rewriter with sandbox mode.
     * It will run the func but doesn't change any file.
     */
    processWithSandboxSync() {
        this.options.runInstance = false;
        this.func.call(this, this);
    }
    /**
     * Async to process rewriter with sandbox mode.
     * It will run the func but doesn't change any file.
     * @async
     */
    processWithSandbox() {
        return __awaiter(this, void 0, void 0, function* () {
            this.options.runInstance = false;
            yield this.func.call(this, this);
        });
    }
    /**
     * Sync to test the rewriter.
     * @returns {TestResultExt[]} test results
     */
    testSync() {
        this.options.writeToFile = false;
        this.func.call(this, this);
        return this.testResults;
    }
    /**
     * Async to test the rewriter.
     * @async
     * @returns {TestResultExt[]} test results
     */
    test() {
        return __awaiter(this, void 0, void 0, function* () {
            this.options.writeToFile = false;
            yield this.func.call(this, this);
            return this.testResults;
        });
    }
    /**
     * Add an affected file.
     * @param {string} filePath - file path
     */
    addAffectedFile(filePath) {
        this.affectedFiles.add(filePath);
    }
    get parser() {
        return this.options.parser;
    }
    /*******
     * DSL *
     *******/
    /**
     * Configure the rewriter.
     * @example
     * configure({ parser: "typescript" })
     * @param {RewriterOptions} options
     * @param {string} [options.sourceType] - script or module
     * @param {string} [options.parser] - typescript or espree
     */
    configure(options) {
        if (options.sourceType) {
            this.options.sourceType = options.sourceType;
        }
        if (options.parser) {
            this.options.parser = options.parser;
        }
    }
    description(description) {
        if (description) {
            this.desc = this.heredoc(description);
        }
        else {
            return this.desc;
        }
    }
    /**
     * Check if node version is greater than or equal to the specified node version.
     * @example
     * ifNode("10.14.0");
     * @param {string} version - specified node version.
     */
    ifNode(version) {
        this.nodeVersion = new node_version_1.default(version);
    }
    /**
     * Compare version of the specified npm.
     * @example
     * ifNpm("react", ">= 18.0");
     * @param {string} name - npm name.
     * @param {string} version - equal, less than or greater than specified version, e.g. '>= 2.0.0',
     */
    ifNpm(name, version) {
        this.npmVersion = new npm_version_1.default(name, version);
    }
    /**
     * Sync to call anther snippet.
     * @example
     * new Synvert.Rewriter("jquery", "migrate", () => {
     *   this.addSnippetSync("jquery", "deprecate-event-shorthand");
     *   this.addSnippetSync("jquery", "deprecate-ready-event");
     *   this.addSnippetSync("https://github.com/synvert-hq/synvert-snippets-javascript/blob/main/lib/javascript/no-useless-constructor.js")
     *   this.addSnippetSync("/Users/flyerhzm/.synvert-javascript/lib/javascript/no-useless-constructor.js")
     *   this.addSnippetSync("javascript/no-useless-constructor")
     * });
     * @param {string} group - group of another rewriter, if there's no name parameter, the group can be http url, file path or snippet name.
     * @param {string} name - name of another rewriter.
     */
    addSnippetSync(group, name) {
        let rewriter = null;
        if (typeof name === "string") {
            rewriter =
                Rewriter.fetch(group, name) || (0, utils_1.evalSnippetSync)([group, name].join("/"));
        }
        else {
            rewriter = (0, utils_1.evalSnippetSync)(group);
        }
        if (!rewriter || !(rewriter instanceof Rewriter))
            return;
        rewriter.options = this.options;
        if (!rewriter.options.writeToFile) {
            const results = rewriter.testSync();
            this.mergeTestResults(results);
        }
        else if (rewriter.options.runInstance) {
            rewriter.processSync();
        }
        else {
            rewriter.processWithSandboxSync();
        }
        this.subSnippets.push(rewriter);
    }
    /**
     * Async to call anther snippet.
     * @async
     * @example
     * new Synvert.Rewriter("jquery", "migrate", async () => {
     *   await this.addSnippet("jquery", "deprecate-event-shorthand");
     *   await this.addSnippet("jquery", "deprecate-ready-event");
     *   await this.addSnippet("https://github.com/synvert-hq/synvert-snippets-javascript/blob/main/lib/javascript/no-useless-constructor.js")
     *   await this.addSnippet("/Users/flyerhzm/.synvert-javascript/lib/javascript/no-useless-constructor.js")
     *   await this.addSnippet("javascript/no-useless-constructor")
     * });
     * @param {string} group - group of another rewriter, if there's no name parameter, the group can be http url, file path or snippet name.
     * @param {string} name - name of another rewriter.
     */
    addSnippet(group, name) {
        return __awaiter(this, void 0, void 0, function* () {
            let rewriter = null;
            if (typeof name === "string") {
                rewriter =
                    Rewriter.fetch(group, name) ||
                        (yield (0, utils_1.evalSnippet)([group, name].join("/")));
            }
            else {
                rewriter = yield (0, utils_1.evalSnippet)(group);
            }
            if (!rewriter || !(rewriter instanceof Rewriter))
                return;
            rewriter.options = this.options;
            if (!rewriter.options.writeToFile) {
                const results = yield rewriter.test();
                this.mergeTestResults(results);
            }
            else if (rewriter.options.runInstance) {
                yield rewriter.process();
            }
            else {
                yield rewriter.processWithSandbox();
            }
            this.subSnippets.push(rewriter);
        });
    }
    /**
     * Sync to find specified files.
     * It creates an Instance to rewrite code.
     * @example
     * new Synvert.Rewriter("javascript", "no-unused-imports", () => {
     *   this.withinFilesSync('**\/*.js', function () {
     *   })
     * })
     * @param {string} filePattern - pattern to find files, e.g. lib/*.js
     * @param {Function} func - a function rewrites code in the matching files.
     */
    withinFilesSync(filePattern, func) {
        if (!this.options.runInstance)
            return;
        if (this.nodeVersion && !this.nodeVersion.match())
            return;
        if (this.npmVersion && !this.npmVersion.match())
            return;
        if (this.options.writeToFile) {
            if ((0, utils_1.isValidFileSync)(configuration_1.default.rootPath) &&
                (0, minimatch_1.default)(configuration_1.default.rootPath, filePattern)) {
                const instance = new instance_1.default(this, filePattern, func);
                return instance.processSync();
            }
            (0, utils_1.globSync)(filePattern).forEach((filePath) => {
                const instance = new instance_1.default(this, filePath, func);
                instance.processSync();
            });
        }
        else {
            if ((0, utils_1.isValidFileSync)(configuration_1.default.rootPath) &&
                (0, minimatch_1.default)(configuration_1.default.rootPath, filePattern)) {
                const instance = new instance_1.default(this, filePattern, func);
                const result = instance.testSync();
                this.mergeTestResults([result]);
                return;
            }
            const filePaths = (0, utils_1.globSync)(filePattern);
            const results = filePaths.map((filePath) => {
                const instance = new instance_1.default(this, filePath, func);
                return instance.testSync();
            });
            this.mergeTestResults(results);
        }
    }
    /**
     * Async to find specified files.
     * It creates an Instance to rewrite code.
     * @async
     * @example
     * new Synvert.Rewriter("javascript", "no-unused-imports", async () => {
     *   await this.withinFiles('**\/*.js', async function () {
     *   })
     * })
     * @param {string} filePattern - pattern to find files, e.g. lib/*.js
     * @param {Function} func - a function rewrites code in the matching files.
     */
    withinFiles(filePattern, func) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.options.runInstance)
                return;
            if (this.nodeVersion && !(yield this.nodeVersion.match()))
                return;
            if (this.npmVersion && !(yield this.npmVersion.match()))
                return;
            if (this.options.writeToFile) {
                if ((yield (0, utils_1.isValidFile)(configuration_1.default.rootPath)) &&
                    (0, minimatch_1.default)(configuration_1.default.rootPath, filePattern)) {
                    const instance = new instance_1.default(this, filePattern, func);
                    return yield instance.process();
                }
                const filePaths = yield (0, utils_1.glob)(filePattern);
                yield Promise.all(filePaths.map((filePath) => {
                    const instance = new instance_1.default(this, filePath, func);
                    return instance.process();
                }));
            }
            else {
                if ((yield (0, utils_1.isValidFile)(configuration_1.default.rootPath)) &&
                    (0, minimatch_1.default)(configuration_1.default.rootPath, filePattern)) {
                    const instance = new instance_1.default(this, filePattern, func);
                    const result = yield instance.test();
                    this.mergeTestResults([result]);
                    return;
                }
                const filePaths = yield (0, utils_1.glob)(filePattern);
                const results = yield Promise.all(filePaths.map((filePath) => {
                    const instance = new instance_1.default(this, filePath, func);
                    return instance.test();
                }));
                this.mergeTestResults(results);
            }
        });
    }
    /**
     * Sync to add a new file.
     * @param {string} fileName - file name
     * @param {string} content - file body
     */
    addFileSync(fileName, content) {
        if (!this.options.runInstance)
            return;
        if (!this.options.writeToFile) {
            const result = {
                affected: true,
                conflicted: false,
                filePath: fileName,
                actions: [{ type: "add_file", start: 0, end: 0, newCode: content }],
            };
            this.testResults.push(result);
            return;
        }
        const filePath = path_1.default.join(configuration_1.default.rootPath, fileName);
        if ((0, utils_1.isValidFileSync)(filePath)) {
            return;
        }
        fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
        fs_1.default.writeFileSync(filePath, content);
    }
    /**
     * Async to add a new file.
     * @async
     * @param {string} fileName - file name
     * @param {string} content - file body
     */
    addFile(fileName, content) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.options.runInstance)
                return;
            if (!this.options.writeToFile) {
                const result = {
                    affected: true,
                    conflicted: false,
                    filePath: fileName,
                    actions: [{ type: "add_file", start: 0, end: 0, newCode: content }],
                };
                this.testResults.push(result);
                return;
            }
            const filePath = path_1.default.join(configuration_1.default.rootPath, fileName);
            if (yield (0, utils_1.isValidFile)(filePath)) {
                return;
            }
            yield fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
            yield fs_1.promises.writeFile(filePath, content);
        });
    }
    /**
     * Sync to remove a file.
     * @param {string} fileName - file name
     */
    removeFileSync(fileName) {
        if (!this.options.runInstance)
            return;
        if (!this.options.writeToFile) {
            const result = {
                affected: true,
                conflicted: false,
                filePath: fileName,
                actions: [{ type: "remove_file", start: 0, end: -1 }],
            };
            this.testResults.push(result);
            return;
        }
        const filePath = path_1.default.join(configuration_1.default.rootPath, fileName);
        if ((0, utils_1.isValidFileSync)(filePath)) {
            fs_1.default.rmSync(filePath);
        }
    }
    /**
     * Async to remove a file.
     * @async
     * @param {string} fileName - file name
     */
    removeFile(fileName) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.options.runInstance)
                return;
            if (!this.options.writeToFile) {
                const result = {
                    affected: true,
                    conflicted: false,
                    filePath: fileName,
                    actions: [{ type: "remove_file", start: 0, end: -1 }],
                };
                this.testResults.push(result);
                return;
            }
            const filePath = path_1.default.join(configuration_1.default.rootPath, fileName);
            if (yield (0, utils_1.isValidFile)(filePath)) {
                yield fs_1.promises.rm(filePath);
            }
        });
    }
    /**
     * Sync to rename filepath to new filepath.
     * @param {string} filePattern - pattern to find files, e.g. *.scss
     * @param {string|Function} convertFunc - new file path string or function to convert file path to new file path.
     */
    renameFileSync(filePattern, convertFunc) {
        if (!this.options.runInstance)
            return;
        const filePaths = (0, utils_1.globSync)(filePattern);
        if (!this.options.writeToFile) {
            filePaths.forEach((filePath) => {
                const newFilePath = typeof convertFunc === "string" ? convertFunc : convertFunc(filePath);
                const result = {
                    affected: true,
                    conflicted: false,
                    filePath,
                    newFilePath,
                    actions: [{ type: "rename_file", start: 0, end: -1 }],
                };
                this.testResults.push(result);
            });
            return;
        }
        filePaths.forEach((filePath) => {
            const newFilePath = typeof convertFunc === "string" ? convertFunc : convertFunc(filePath);
            fs_1.default.renameSync(path_1.default.join(configuration_1.default.rootPath, filePath), path_1.default.join(configuration_1.default.rootPath, newFilePath));
        });
    }
    /**
     * Rename filepath to new filepath.
     * @param {string} filePattern - pattern to find files, e.g. *.scss
     * @param {string|Function} convertFunc - new file path string or function to convert file path to new file path.
     */
    renameFile(filePattern, convertFunc) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.options.runInstance)
                return;
            const filePaths = yield (0, utils_1.glob)(filePattern);
            if (!this.options.writeToFile) {
                filePaths.forEach((filePath) => {
                    const newFilePath = typeof convertFunc === "string" ? convertFunc : convertFunc(filePath);
                    const result = {
                        affected: true,
                        conflicted: false,
                        filePath,
                        newFilePath,
                        actions: [{ type: "rename_file", start: 0, end: -1 }],
                    };
                    this.testResults.push(result);
                });
                return;
            }
            filePaths.map((filePath) => __awaiter(this, void 0, void 0, function* () {
                const newFilePath = typeof convertFunc === "string" ? convertFunc : convertFunc(filePath);
                yield fs_1.promises.rename(path_1.default.join(configuration_1.default.rootPath, filePath), path_1.default.join(configuration_1.default.rootPath, newFilePath));
            }));
        });
    }
    /**
     * Merge test results.
     * @param {TestResultExt[]} results test results to be merged
     */
    mergeTestResults(results) {
        this.testResults = [
            ...this.testResults,
            ...results.filter((result) => result.affected),
        ];
    }
    heredoc(text) {
        let addNewLine = false;
        const lines = text.split("\n");
        if (lines.length > 0 && lines[0] === "") {
            lines.shift();
        }
        if (lines.length > 0 && lines[lines.length - 1].trim() === "") {
            lines.pop();
            addNewLine = true;
        }
        const indent = lines[0].search(/[^ ]/);
        return (lines.map((line) => line.slice(indent)).join("\n") +
            (addNewLine ? "\n" : ""));
    }
}
/**
 * Store all rewriters grouped by group name, e.g.  `{ jquery: { 'deprecate-event-shorthand': <Rewriter> } }`
 * @static
 */
Rewriter.rewriters = {};
exports.default = Rewriter;