"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;