UNPKG

@foal/cli

Version:

CLI tool for FoalTS

556 lines (555 loc) 20.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Generator = exports.ClientError = void 0; // std const assert_1 = require("assert"); const path_1 = require("path"); /** * Error thrown by the Generator which aims to be pretty * printed without the stacktrace. * * @export * @class ClientError * @extends {Error} */ class ClientError extends Error { name = 'ClientError'; } exports.ClientError = ClientError; /** * This class provides more methods that Node std "generator". * It also allows to create an isolated directory during directory. * * @export * @class Generator */ class Generator { fileSystem; logger; currentDir = ''; testDir = 'test-generators/subdir'; logs = true; constructor(fileSystem, logger) { this.fileSystem = fileSystem; this.logger = logger; } /** * Do not show create and update logs. * * @returns {this} * @memberof Generator */ hideLogs() { this.logs = false; return this; } /** * Changes the current working directory. * * @param {string} path - Relative path of the directory. * @returns {this} * @memberof Generator */ cd(path) { this.currentDir = (0, path_1.join)(this.currentDir, path); return this; } /** * Changes the current working directory to the project root * directory. * * It searches for the closer package.json containing @foal/core * as dependency. * * @returns {this} * @memberof Generator */ cdProjectRootDir() { // "/" on Unix, C:\ on Windows const root = (0, path_1.parse)(process.cwd()).root; while (!this.exists('package.json')) { if ((0, path_1.join)(process.cwd(), this.parse('.')) === root) { throw new ClientError('This project is not a FoalTS project. No package.json found.'); } this.cd('..'); } const packagePath = (0, path_1.join)(this.currentDir, 'package.json'); const content = this.fileSystem.readFile(packagePath); let pkg; try { pkg = JSON.parse(content); } catch (error) { throw new ClientError(`The file package.json is not a valid JSON. ${error.message}`); } if (!pkg.dependencies || !pkg.dependencies['@foal/core']) { throw new ClientError('This project is not a FoalTS project. The dependency @foal/core is missing in package.json.'); } return this; } /** * Checks if a file or directory exists. * * @param {string} path - The path relative to the client directory. * @returns {boolean} * @memberof Generator */ exists(path) { const relativePath = (0, path_1.join)(this.currentDir, path); return this.fileSystem.exists(relativePath); } /** * Recursively ensures that a directory exists. If the directory structure does not * exist, it is created. * * @param {string} path - The path relative to the client directory. * @returns {this} * @memberof Generator */ ensureDir(path) { const relativePath = (0, path_1.join)(this.currentDir, path); this.fileSystem.mkdir(relativePath); return this; } /** * Recursively ensures that a directory exists if the condition is true. * If the directory structure does not exist, it is created. * * @param {boolean} condition - The condition. * @param {string} path - The path relative to the client directory. * @returns {this} * @memberof Generator */ ensureDirOnlyIf(condition, path) { if (condition) { this.ensureDir(path); } return this; } /** * Ensures that the file exists. If the file does not exist, it is created. * * @param {string} path - The path relative to the client directory. * @returns {this} * @memberof Generator */ ensureFile(path) { const relativePath = (0, path_1.join)(this.currentDir, path); if (!this.fileSystem.exists(relativePath)) { this.logCreate(path); this.fileSystem.writeFile(relativePath, ''); } return this; } /** * Copies a file from the `templates` directory. * * @param {string} src - The source path relative to the `templates/` directory. * @param {string} dest - The destination path relative to the client directory. * @returns {this} * @memberof Generator */ copyTemplate(src, dest) { if (!this.fileSystem.existsTemplate(src)) { throw new Error(`The template "${src}" does not exist.`); } this.logCreate(dest); this.fileSystem.copyFileFromTemplates(src, (0, path_1.join)(this.currentDir, dest)); return this; } /** * Copies a file from the `templates` directory if the condition is true. * * @param {boolean} condition - The condition. * @param {string} src - The source path relative to the `templates/` directory. * @param {string} dest - The destination path relative to the client directory. * @returns {this} * @memberof Generator */ copyTemplateOnlyIf(condition, src, dest) { if (condition) { this.copyTemplate(src, dest); } return this; } /** * Copies and renders a template from the `templates` directory. * * @param {string} src - The source path relative to the `templates/` directory. * @param {string} dest - The destination path relative to the client directory. * @param {*} locals - The template variables. * @returns {this} * @memberof Generator */ render(src, dest, locals) { if (!this.fileSystem.existsTemplate(src)) { throw new Error(`The template "${src}" does not exist.`); } let content = this.fileSystem.readFileFromTemplates(src); for (const key in locals) { content = content.split(`/* ${key} */`).join(locals[key]); } this.logCreate(dest); this.fileSystem.writeFile((0, path_1.join)(this.currentDir, dest), content); return this; } /** * Copies and renders a template from the `templates` directory if the condition is true. * * @param {boolean} condition - The condition. * @param {string} src - The source path relative to the `templates/` directory. * @param {string} dest - The destination path relative to the client directory. * @param {*} locals - The template variables. * @returns {this} * @memberof Generator */ renderOnlyIf(condition, src, dest, locals) { if (condition) { this.render(src, dest, locals); } return this; } /** * Reads and modifies the content of a file. * * @param {string} path - The path relative to the client directory. * @param {(content: string) => string} callback - The callback that modifies the content. * @returns {this} * @memberof Generator */ modify(path, callback) { const relativePath = (0, path_1.join)(this.currentDir, path); if (!this.fileSystem.exists(relativePath)) { throw new ClientError(`Impossible to modify "${path}": the file does not exist.`); } const content = this.fileSystem.readFile(relativePath); this.logUpdate(path); this.fileSystem.writeFile(relativePath, callback(content)); return this; } /** * Reads and modifies the content of a file if the condition is true. * * @param {boolean} condition - The condition. * @param {string} path - The path relative to the client directory. * @param {(content: string) => string} callback - The callback that modifies the content. * @returns {this} * @memberof Generator */ modifyOnlyfIf(condition, path, callback) { if (condition) { this.modify(path, callback); } return this; } /** * Add a named import at the bottom of the file. * * @param {string} path - The file path relative to the client directory. * @param {string} specifier - The import specifier. * @param {string} source - The import source. * @returns {this} * @memberof Generator */ addNamedExportIn(path, specifier, source) { this.modify(path, content => `${content}export { ${specifier} } from '${source}';\n`); return this; } /** * Adds or extends a named import at the beginning of the file. * * If an import already exists with this source path, it is completed. * If it does not already exist, it is added at the end of all imports. * * @param {string} path - The file path relative to the client directory. * @param {string} specifier - The import specifier. * @param {string} source - The import source. * @returns {this} * @memberof Generator */ addOrExtendNamedImportIn(path, specifier, source, options) { const initialLogs = this.logs; if (options) { this.logs = options.logs; } this.modify(path, content => { // TODO: add tests to support double quotes. const regex = /import (.*) from '(.*)';/g; let endPos = 0; let specifierAlreadyExists = false; const replacedContent = content.replace(regex, (match, p1, p2, offset) => { endPos = offset + match.length; const namedImportRegex = new RegExp(`import {(.*)} from \'(.*)\';`); return match.replace(namedImportRegex, (subString, specifiersStr, path) => { if (path !== source) { return subString; } const specifiers = specifiersStr .split(',') .map(imp => imp.trim()); if (specifiers.includes(specifier)) { specifierAlreadyExists = true; return subString; } const newSpecifiers = specifiers .concat(specifier) .sort((a, b) => a.localeCompare(b)) .join(', '); return `import { ${newSpecifiers} } from '${source}';`; }); }); if (specifierAlreadyExists) { return content; } if (replacedContent !== content) { return replacedContent; } const newImport = `import { ${specifier} } from '${source}';`; if (endPos === 0) { return `${newImport}\n\n${content}`; } return content.substr(0, endPos) + '\n' + newImport + content.substr(endPos); }); this.logs = initialLogs; return this; } /** * Creates or adds an element to the array property of a class. * * If the class does not exist, this method does nothing. * * @param {string} path - The file path relative to the client directory. * @param {string} className - The class name. * @param {string} propertyName - The property name. * @param {string} element - The item to add to the array. * @returns {this} * @memberof Generator */ addOrExtendClassArrayPropertyIn(path, propertyName, element, options) { const initialLogs = this.logs; if (options) { this.logs = options.logs; } this.modify(path, content => content.replace(new RegExp(`class (\\w*|\\w* implements \\w*) {(.*)}`, 's'), (match, className, p2) => { if (/^(\s)*$/.test(p2)) { return `class ${className} {\n ${propertyName} = [\n ${element}\n ];\n}`; } const replacedMatch = match.replace(new RegExp(`( *)${propertyName} = \\[(.*)\\];`, 's'), (_, spaces, content) => { const items = content .replace(/,\n/g, '\n') .split('\n') .map(e => e.trim()) .concat(element) .map(e => `${spaces}${spaces}${e}`); const cleanItems = []; for (const item of items) { if (item.trim() !== '') { cleanItems.push(item); } } return `${spaces}${propertyName} = [\n${cleanItems.join(',\n')}\n${spaces}];`; }); if (replacedMatch !== match) { return replacedMatch; } return `class ${className} {\n ${propertyName} = [\n ${element}\n ];\n${p2}}`; })); this.logs = initialLogs; return this; } /** * Returns true if the project package.json has this dependency. * Returns false otherwise. * * @param {string} name - The name of the dependency. * @returns {boolean} * @memberof Generator */ projectHasDependency(name) { const initialCurrentDir = this.currentDir; this.cdProjectRootDir(); const packagePath = (0, path_1.join)(this.currentDir, 'package.json'); const pkg = JSON.parse(this.fileSystem.readFile(packagePath)); this.currentDir = initialCurrentDir; return pkg.dependencies.hasOwnProperty(name); } /** * Returns the dependencies of the project package.json. * * @returns {{ name: string, version: string }[]} */ getProjectDependencies() { const initialCurrentDir = this.currentDir; this.cdProjectRootDir(); const packagePath = (0, path_1.join)(this.currentDir, 'package.json'); const pkg = JSON.parse(this.fileSystem.readFile(packagePath)); this.currentDir = initialCurrentDir; return Object.keys(pkg.dependencies) .map(name => ({ name, version: pkg.dependencies[name] })); } /** * Sets or updates a dependency in the project package.json. * * @returns {this} */ setOrUpdateProjectDependency(name, version) { const initialCurrentDir = this.currentDir; this.cdProjectRootDir(); const packagePath = (0, path_1.join)(this.currentDir, 'package.json'); const pkg = JSON.parse(this.fileSystem.readFile(packagePath)); pkg.dependencies[name] = version; this.fileSystem.writeFile(packagePath, JSON.stringify(pkg, null, 2)); this.currentDir = initialCurrentDir; return this; } /** * Set or updates a dependency in the project package.json if the condition is true. * * @returns {this} */ setOrUpdateProjectDependencyOnlyIf(condition, name, version) { if (condition) { this.setOrUpdateProjectDependency(name, version); } return this; } /** * Returns the dev dependencies of the project package.json. * * @returns {{ name: string, version: string }[]} */ getProjectDevDependencies() { const initialCurrentDir = this.currentDir; this.cdProjectRootDir(); const packagePath = (0, path_1.join)(this.currentDir, 'package.json'); const pkg = JSON.parse(this.fileSystem.readFile(packagePath)); this.currentDir = initialCurrentDir; return Object.keys(pkg.devDependencies) .map(name => ({ name, version: pkg.devDependencies[name] })); } /** * Sets or updates a dev dependency in the project package.json. * * @returns {this} */ setOrUpdateProjectDevDependency(name, version) { const initialCurrentDir = this.currentDir; this.cdProjectRootDir(); const packagePath = (0, path_1.join)(this.currentDir, 'package.json'); const pkg = JSON.parse(this.fileSystem.readFile(packagePath)); pkg.devDependencies[name] = version; this.fileSystem.writeFile(packagePath, JSON.stringify(pkg, null, 2)); this.currentDir = initialCurrentDir; return this; } /************************ Testing Methods ************************/ /** * Throws an error if the file or directory does not exist. * * @param {string} path - The path relative to the client directory. * @returns {this} * @memberof Generator */ assertExists(path) { const relativePath = (0, path_1.join)(this.currentDir, path); if (!this.fileSystem.exists(relativePath)) { throw new Error(`The file "${path}" does not exist.`); } return this; } /** * Throws an error if the file or directory exists. * * @param {string} path - The path relative to the client directory. * @returns {this} * @memberof Generator */ assertNotExists(path) { const relativePath = (0, path_1.join)(this.currentDir, path); if (this.fileSystem.exists(relativePath)) { throw new Error(`The file "${path}" should not exist.`); } return this; } /** * Throws an error if the directory is not empty. * * @param {string} path - The path relative to the client directory. * @memberof Generator */ assertEmptyDir(path) { if (!this.fileSystem.isDirectoryEmpty((0, path_1.join)(this.currentDir, path))) { throw new Error(`The directory "${path}" should be empty.`); } } /** * Throws an error if the two files are different. * * @param {string} actual - The path relative to the client directory. * @param {string} expected - The path relative to the `specs/` directory. * @param {{ binary: boolean }} [{ binary }={ binary: true }] - Specify if the file is binary. * @returns {this} * @memberof Generator */ assertEqual(actual, expected, { binary } = { binary: false }) { if (!this.fileSystem.existsSpec(expected)) { throw new Error(`The spec file "${expected}" does not exist.`); } if (binary) { (0, assert_1.deepStrictEqual)(this.fileSystem.readBinaryFile((0, path_1.join)(this.currentDir, actual)), this.fileSystem.readBinaryFileFromSpecs(expected)); } else { (0, assert_1.strictEqual)(this.fileSystem.readFile((0, path_1.join)(this.currentDir, actual)), this.fileSystem.readFileFromSpecs(expected)); } return this; } /** * Copies a file from the `fixtures` directory. * * @param {string} src - The source path relative to the `fixtures/` directory. * @param {string} dest - The destination path relative to the client directory. * @returns {this} * @memberof Generator */ copyFixture(src, dest) { if (!this.fileSystem.existsFixture(src)) { throw new Error(`The fixture file "${src}" does not exist.`); } this.fileSystem.copyFileFromFixtures(src, (0, path_1.join)(this.currentDir, dest)); return this; } /** * Removes a file. * * @param {string} path - The path relative to the client directory. * @memberof Generator */ rmfile(path) { const relativePath = (0, path_1.join)(this.currentDir, path); this.fileSystem.deleteFile(relativePath); } isTestingEnvironment() { return process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST === 'true'; } parse(path) { if (this.isTestingEnvironment()) { return (0, path_1.join)(this.testDir, this.currentDir, path); } return (0, path_1.join)(this.currentDir, path); } logCreate(path) { path = (0, path_1.join)(this.currentDir, path); if (!this.isTestingEnvironment() && this.logs) { this.logger.info(path, 'create'); } } logUpdate(path) { path = (0, path_1.join)(this.currentDir, path); if (!this.isTestingEnvironment() && this.logs) { this.logger.info(path, 'update'); } } } exports.Generator = Generator;