UNPKG

@gobstones/gobstones-scripts

Version:

Scripts to abstract away build configuration of Gobstones Project's libraries and modules.

1,123 lines (1,110 loc) 131 kB
'use strict'; var childProcess = require('child_process'); var path = require('path'); var fs$1 = require('fs-extra'); var tsconfigJs = require('tsconfig.js'); var fs = require('fs'); var colors = require('ansi-colors'); var commandExists = require('command-exists'); var commonTags = require('common-tags'); /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * An error that occurs when there is a problem in the filesystem state. * * @group Errors */ class FileSystemError extends Error { /** * Create a FileSystemError. * * @param message - A string with the error message. */ constructor(message) { super(message); // It restores the prototype chain Object.setPrototypeOf(this, new.target.prototype); } } /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * @module Config * @author Alan Rodas Bonjour <alanrodas@gmail.com> */ /** * The tool's version number * * This needs to be here in order to avoid reading the package.json, which * is not reliable between different implementations of package managers * and that may lead to false readings. */ const version = '0.9.3'; /** * The tool's test server address. * * This line is added to the .npmrc file of projects * when they are created in test mode. It can also be used * in any project that attempts to use the version from * the verdaccio server instead of the regular one. */ const testServer = '@gobstones:registry=http://localhost:4567'; /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * ---------------------------------------------------- * @module Helpers * @author Alan Rodas Bonjour <alanrodas@gmail.com> * * @internal * ---------------------------------------------------- */ /** * Answers if the platform is windows. * * @returns `true` if the current OS is Windows, `false` otherwise. */ const isWindows = () => { var _a; return process && (process.platform === 'win32' || /^(msys|cygwin)$/.test((_a = process.env.OSTYPE) !== null && _a !== void 0 ? _a : '')); }; /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * ---------------------------------------------------- * @module Helpers * @author Alan Rodas Bonjour <alanrodas@gmail.com> * * @internal * ---------------------------------------------------- */ /** * The different available log levels. */ var LogLevel; (function (LogLevel) { LogLevel["Error"] = "error"; LogLevel["Warn"] = "warn"; LogLevel["Info"] = "info"; LogLevel["Debug"] = "debug"; })(LogLevel || (LogLevel = {})); /** * This class provides a centralize way to report messages in the terminal through * the application, including messages that are always printed, debug information, * error messages and others. */ class Logger { /** Create a new logger */ constructor(level) { this.level = level; this._on = true; } /** * Turn this logger on. If already on, nothing happens. * * @returns the receiver logger. */ on() { this._on = true; return this; } /** * Turn this logger off. If already off, nothing happens. * * @returns the receiver logger. */ off() { this._on = false; return this; } /** * Log a message as an error, if the level allows it and the logger is on. * * @param msg - The message to print. * @param style - A style on which to print the message * * @returns the receiver logger. */ error(msg, style) { this.print(msg, style, LogLevel.Error); return this; } /** * Log a message as an warning, if the level allows it and the logger is on. * * @param msg - The message to print. * @param style - A style on which to print the message * * @returns the receiver logger. */ warn(msg, style) { this.print(msg, style, LogLevel.Warn); return this; } /** * Log a message as information, if the level allows it and the logger is on. * * @param msg - The message to print. * @param style - A style on which to print the message * * @returns the receiver logger. */ info(msg, style) { this.print(msg, style, LogLevel.Info); return this; } /** * Log a message as debug information, if the level allows it and the logger is on. * * @param msg - The message to print. * @param style - A style on which to print the message * * @returns the receiver logger. */ debug(msg, style) { this.print(msg, style, LogLevel.Debug); return this; } /** * Log a message regardless of the active level, but only if the logger is on. * * @param msg - The message to print. * @param style - A style on which to print the message * * @returns the receiver logger. */ log(msg, style) { this.print(msg, style); return this; } /** * Print the given message in the terminal, if the log level allows it and the logger is on. * * @param msg - The message to print * @param style - The style to use for printing. * @param actualLevel - The actual level on which the message should be printed. */ print(msg, style, actualLevel) { if (this._on && (!actualLevel || this.isLevelGeqThan(actualLevel, this.level))) { if (style) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call msg = colors[style](msg); } // eslint-disable-next-line no-console console.log(msg); } } /** * Answers if the first level is greater or equal than the second one. * * @param level1 - The first level to compare * @param level2 - The second level to compare. * * @returns `true` if the first level is greater or equal than the second, `false` otherwise. */ isLevelGeqThan(level1, level2) { return (level1 === LogLevel.Error || (level1 === LogLevel.Warn && level2 !== LogLevel.Error) || (level1 === LogLevel.Info && level2 !== LogLevel.Error && level2 !== LogLevel.Warn) || level2 === LogLevel.Debug); } } /** * The default {@link Logger}. */ const logger = new Logger(LogLevel.Error); /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * ---------------------------------------------------- * @module Helpers * @author Alan Rodas Bonjour <alanrodas@gmail.com> * * @internal * ---------------------------------------------------- */ /** * A package json reader creates a wrapper around the action of reading a * JSON file and accessing the properties of it in an easy fashion. * If reading the file fails, the returned reader will always return undefined * for any property intended to be read. */ class PackageJsonReader { /** * Create a new PackageJsonReader. * * @param packageJsonLocation - The location of the file to read. */ constructor(packageJsonLocation) { logger.debug(`[PackageJsonReader]: Attempting to read package.json configuration at: ${packageJsonLocation}`, 'yellow'); try { const contents = fs.readFileSync(packageJsonLocation); this._contents = JSON.parse(contents.toString()); } catch (_a) { logger.debug(`[PackageJsonReader]: File package.json not found at location`, 'yellow'); this._contents = undefined; } } /** * Return the associated value for a given key. */ getValueAt(key) { let nextValue = this._contents; const keys = key.split('.'); for (const k of keys) { nextValue = nextValue ? nextValue[k] : undefined; } return nextValue; } } /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * ---------------------------------------------------- * @module Helpers * @author Alan Rodas Bonjour <alanrodas@gmail.com> * * @internal * ---------------------------------------------------- */ /** * Get the path to one of the bin scripts exported by a package. * If the bin name is different from the package name, it should * be given, if not given, the name of the binary is assumed to be the same * of that of the package. * * @param projectRootPath - The currently identified project's root path. * @param packageManagerDefinition - The definition to all available package managers. * @param packageName - The name of the package holding the binary. * @param binName - The name of the binary that it's intended to be retrieved. * * @returns The information for the binary execution, or undefined if no binary was found. */ const getBin = (projectRootPath, packageManagerDefinition, packageName, binName) => { if (!binName) { binName = packageName; } // Hold the result of the file found for caching it at the end. let result = {}; logger.debug(`[getBin]: Attempting to find binary file "${binName}" for the package "${packageName}"`, 'magenta'); try { logger.debug(`[getBin]: Trying to retrieve the package.json information through require.resolve`, 'magenta'); // Attempt to get the bin by resolving the package.json const packageJsonPath = require.resolve(`${packageName}/package.json`); const packageRootDir = path.dirname(packageJsonPath); const binFile = getBinFile(packageJsonPath, packageRootDir, binName); result = { scriptFile: binFile, command: `node --experimental-vm-modules ${binFile}`, mode: 'node' }; } catch (_a) { logger.debug(`[getBin]: Could not detect through require.resolve`, 'magenta'); logger.debug(`[getBin]: Attempting to find binary in binary folders of current package manager`, 'magenta'); for (const modulesFolder of packageManagerDefinition.modulesFolders) { logger.debug(`[getBin]: Verifying at: ${modulesFolder}`); const packageNamePath = path.join(projectRootPath, modulesFolder, packageName, 'package.json'); if (fs.existsSync(packageNamePath)) { logger.debug(`[getBin]: Found a package.json file at: ${packageNamePath}`, 'magenta'); const packageRootDir = path.dirname(packageNamePath); const binFile = getBinFile(packageNamePath, packageRootDir, binName); result = { scriptFile: binFile, command: `node --experimental-vm-modules ${binFile}`, mode: 'node' }; break; } else { logger.debug(`[getBin]: No package with name found. Attempting to find native binary.`, 'magenta'); const binFile = path.join(projectRootPath, modulesFolder, '.bin', binName || packageName); if (fs.existsSync(binFile)) { // We found a possible binary, but in some cases // this are actually node files that were not reported. // We need to check the heading to see that. const contents = fs.readFileSync(binFile).toString(); const heading = contents.length > 0 ? contents.split('\n')[0] : ''; if (heading.includes('node')) { // If the heading mentions node, it's a javascript file. logger.debug(`[getBin]: Found a javascript executable at: ${binFile}`, 'magenta'); result = { scriptFile: binFile, command: `node --experimental-vm-modules ${binFile}`, mode: 'node' }; break; } // If not, it's a binary file, but there may be // many depending on the OS. Verify to see if we have // a powershell, command, or sh. if (isWindows()) { if (fs.existsSync(`${binFile}.ps1`)) { logger.debug(`[getBin]: Found a powershell executable at: ${binFile}.ps1`, 'magenta'); result = { scriptFile: `${binFile}.ps1`, command: `powershell ${binFile}.ps1`, mode: 'pwsh' }; break; } if (fs.existsSync(`${binFile}.cmd`)) { logger.debug(`[getBin]: Found a windows command line executable at: ${binFile}.cmd`, 'magenta'); result = { scriptFile: `${binFile}.cmd`, command: `${binFile}.cmd`, mode: 'cmd' }; break; } } // Default to shell script logger.debug(`[getBin]: Found a shell script at: ${binFile}`, 'magenta'); result = { scriptFile: binFile, command: `sh ${binFile}`, mode: 'sh' }; break; } } } } if (Object.keys(result).length === 0) { logger.debug(`[getBin]: Could not find a binary file`, 'magenta'); return undefined; } return Object.assign(result, { packageName, binName }); }; /** * Returns the bin file for a given package and binary file. * * @param packageJsonPath - The path of the requested package's package.json * @param packageRootDir - The path to root of the package that contains the binary directory * @param binName - The name of the binary that we want the path of. * * @returns The path to the binary file. * * @internal */ const getBinFile = (packageJsonPath, packageRootDir, binName) => { logger.debug(`[getBin] Attempting to find the binary file: ${binName} using root: ${packageJsonPath}`, 'magenta'); const pkgJsonReader = new PackageJsonReader(packageJsonPath); let pkgBinDefinition = pkgJsonReader.getValueAt('bin'); const binRelativeToPackgeInJson = pkgBinDefinition && typeof pkgBinDefinition === 'object' ? (pkgBinDefinition = pkgBinDefinition[binName]) : pkgBinDefinition; logger.debug(`[getBin] Found a package.json with bin file declared as: ${binRelativeToPackgeInJson}`, 'magenta'); const fullBinPath = path.join(packageRootDir, binRelativeToPackgeInJson); logger.debug(`[getBin] Path for binary found as: ${fullBinPath}`, 'magenta'); return fullBinPath; }; /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * ---------------------------------------------------- * @module Helpers * @author Alan Rodas Bonjour <alanrodas@gmail.com> * * @internal * ---------------------------------------------------- */ /** * Returns the `@gobstones/gobstones-scripts` root path. That is, the path to * the module in the users `node_modules` folder. * * @param os - The OS that is currently discovered as environment. * @param projectRootPath - The currently identified project's root path. * * @returns The root path of the `gobstones-scripts` project. */ const getGobstonesScriptsRootPath = (os, projectRootPath) => { logger.debug(`[getGobstonesScriptsRootPath]: Attempting to recover the gobstones-scripts folder location`, 'green'); // Return if previously calculated let detectedScriptRootPath = ''; try { logger.debug(`[getGobstonesScriptsRootPath]: Attempting through require.resolve method`, 'green'); // First attempt by using require.resolve // This method is not reliable and may fail // depending on setup detectedScriptRootPath = path.join(path.dirname(require.resolve('@gobstones/gobstones-scripts/package.json'))); logger.debug(`[getGobstonesScriptsRootPath]: Found through require.resolve at: ${detectedScriptRootPath}`, 'green'); return detectedScriptRootPath; } catch (_a) { logger.debug(`[getGobstonesScriptsRootPath]: require.resolve method failed`, 'green'); // First, check if we are running from the project itself, that may be // the same folder, or a folder further up let possibleRootPath = projectRootPath; const pathStartBySystem = os === 'windows' ? ((possibleRootPath === null || possibleRootPath === void 0 ? void 0 : possibleRootPath.match(/^[A-Z]:\\/)) ? possibleRootPath.substring(0, 3) : 'C:\\') : '/'; logger.debug(`[getGobstonesScriptsRootPath]: Verifying if "${possibleRootPath}" is the gobstones-scripts root folder`, 'green'); while (possibleRootPath !== pathStartBySystem) { const pkgReader = new PackageJsonReader(path.join(possibleRootPath, 'package.json')); if (pkgReader.getValueAt('name') === '@gobstones/gobstones-scripts') { // Found detectedScriptRootPath = possibleRootPath; logger.debug(`[getGobstonesScriptsRootPath]: Found gobstones-scripts at: ${possibleRootPath}`, 'green'); return detectedScriptRootPath; } possibleRootPath = path.dirname(possibleRootPath); logger.debug(`[getGobstonesScriptsRootPath]: Not found. Attempting with next folder: ${possibleRootPath}`, 'green'); } logger.debug(`[getGobstonesScriptsRootPath]: Reached root directory and not found`, 'green'); // Return the path to a particular location // based on the root folder string, the // file does not necessarily exists. const getRootPath = (command) => { try { const processGettedPath = childProcess.execSync(command).toString().trim(); return path.join(processGettedPath || '', '@gobstones', 'gobstones-scripts'); } catch (_a) { return ''; } }; // If running globally, try to find a configuration // of the particular tool. That means, get the directory of // tool installed (which may fail if not installed), and // ensure the directory exists. const attemptForCommandWithLocations = (command, locations) => { if (commandExists.sync(command)) { logger.debug(`[getGobstonesScriptsRootPath]: Attempting to see if gobstones-scripts ` + `is installed globally through ${command}`, 'green'); for (const location of locations) { logger.debug(`[getGobstonesScriptsRootPath]: Attempting with location from "${location}"`, 'green'); const rootPath = getRootPath(location); if (rootPath && fs.existsSync(rootPath)) { logger.debug(`[getGobstonesScriptsRootPath]: Found. Setting gobstones-scripts root to: ${rootPath}`, 'green'); return rootPath; } logger.debug(`[getGobstonesScriptsRootPath]: Not found, attempting next location.`, 'green'); } } logger.debug(`[getGobstonesScriptsRootPath]: Not found at any location for command ${command}`, 'green'); return undefined; }; let indexToLocation = 0; const actionsToFindLocation = [ () => attemptForCommandWithLocations('npm', [ 'npm root --location=project', 'npm root --location=user', 'npm root --location=global' ]), () => attemptForCommandWithLocations('pnpm', ['pnpm root', 'pnpm root --global']) ]; while (!detectedScriptRootPath && indexToLocation < actionsToFindLocation.length) { detectedScriptRootPath = actionsToFindLocation[indexToLocation]() || ''; indexToLocation++; } if (detectedScriptRootPath) { return detectedScriptRootPath; } else { logger.debug(`[getGobstonesScriptsRootPath]: Could not find location through any of the commands.`, 'green'); } // Also get a default path, which requires // the project root path, and attempt to find // the project under node_modules. const defaultPath = path.join(projectRootPath, 'node_modules', '@gobstones', 'gobstones-scripts'); if (fs.existsSync(defaultPath)) { logger.debug(`[getGobstonesScriptsRootPath]: Returning default at: ${defaultPath}`, 'green'); return defaultPath; } } logger.debug(`[getGobstonesScriptsRootPath]: Could not find the gobstones-script project library`, 'green'); throw Error('cannot find script root'); }; /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * ---------------------------------------------------- * @module Helpers * @author Alan Rodas Bonjour <alanrodas@gmail.com> * * @internal * ---------------------------------------------------- */ /** * Return the package manager in use based different features. First, by * identifying the current runner through the `npm_config_user_agent` environment * variable. It such variable is not set, which is common for global runs, * attempts to identify the runner by locating the global `gobstones-scripts` * command. If no match is found, defaults to `npm`. * * @param availablePackageManagers - The list of all available package managers. * @param defaultPackageManager The default package manager to use. * * @returns The package manager in use */ const getInUsePackageManager = (availablePackageManagers, defaultPackageManager = 'npm') => { logger.debug(`[getInUsePackageManager]: Attempting to locate package manager in use`, 'gray'); const userAgent = process.env.npm_config_user_agent; let whichFile = ''; if (!userAgent) { logger.debug(`[getInUsePackageManager]: No node user agent found, checking if gobstones-scripts was run globally`, 'gray'); try { logger.debug(`[getInUsePackageManager]: Attempting to find global gobstones-script binary through "which" command`, 'gray'); whichFile = childProcess .spawnSync('which gobstones-scripts', { shell: true }) .output.toString() .replace(/,/g, '') .trim(); } catch (_a) { logger.debug(`[getInUsePackageManager]: "which" may not be installed in this system.`, 'cyan'); } } const value = userAgent !== null && userAgent !== void 0 ? userAgent : whichFile; logger.debug(`[getInUsePackageManager]: Found gobstones-scripts at: ${value}`, 'gray'); let result; for (const pm of Object.keys(availablePackageManagers)) { if (value === null || value === void 0 ? void 0 : value.includes(availablePackageManagers[pm].cmd)) { if (commandExists.sync(availablePackageManagers[pm].cmd)) { result = availablePackageManagers[pm].cmd; break; } } } if (!result) { logger.debug(`[getInUsePackageManager]: Could not determine package manager. Using default: ${defaultPackageManager}`, 'gray'); return defaultPackageManager; } else { logger.debug(`[getInUsePackageManager]: Detected package manager in use as: ${result}`, 'cyan'); return result; } }; /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * ---------------------------------------------------- * @module Helpers * @author Alan Rodas Bonjour <alanrodas@gmail.com> * * @internal * ---------------------------------------------------- */ /** * Returns the current project's root path by attempting to identify it. * If it cannot be identified, it returns the current working directory * as the project's root path. * * @param os - The OS that is currently discovered as environment. * * @returns The project's root path. */ const getProjectRootPath = (os) => { logger.debug(`[getProjectRootPath]: Attempting to recover the project root path`, 'cyan'); // The obvious choice is the the current directory let possibleRootPath = process.env.PWD ? process.env.PWD : process.cwd() ? process.cwd() : path.resolve('.'); const pathStartBySystem = os === 'windows' ? // eslint-disable-next-line no-null/no-null possibleRootPath.match(/^[A-Z]:\\/) !== null ? possibleRootPath.substring(0, 3) : 'C:\\' : '/'; // Attempt to find the root in current directory and above // until no more directories are found logger.debug(`[getProjectRootPath]: Trying to check if "${possibleRootPath}" is the project's root path`, 'cyan'); while (possibleRootPath && possibleRootPath !== pathStartBySystem) { // A valid root is one such that there is a package.json // with or without gobstones-scripts config section. if (fs.existsSync(path.join(possibleRootPath, 'package.json'))) { // Found logger.debug(`[getProjectRootPath]: Found a package.json file at: "${possibleRootPath}"`, 'cyan'); logger.debug(`[getProjectRootPath]: Project's root path is: "${possibleRootPath}"`, 'cyan'); return possibleRootPath; } // Go up possibleRootPath = path.dirname(possibleRootPath); logger.debug(`[getProjectRootPath]: No package.json found, attempting with new folder: ${possibleRootPath}"`, 'cyan'); } possibleRootPath = process.env.PWD ? process.env.PWD : process.cwd() ? process.cwd() : path.resolve('.'); logger.debug(`[getProjectRootPath]: Reached root directory and no project root found.`, 'cyan'); logger.debug(`[getProjectRootPath]: Considering current working directory as project's root`, 'cyan'); logger.debug(`[getProjectRootPath]: Project's root path set to: ${possibleRootPath}`, 'cyan'); return possibleRootPath; }; /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * ---------------------------------------------------- * @module Helpers * @author Alan Rodas Bonjour <alanrodas@gmail.com> * * @internal * ---------------------------------------------------- */ /** * Locate the tooling file to use for the given type definition. * * @param projectRoot - The root directory for the project. * @param gobstonesScriptsProjectsRoot - The root directory for the gobstones-scripts package. * @param fileDef - The file definition to locate the tooling for. * * @returns The tooling file path for the given file definition, or undefined if none could be found. */ const getToolingFile = (projectRoot, gobstonesScriptsProjectsRoot, fileDef) => { logger.debug(`[getToolingFile] Attempting to determine which configuration file to use for: ${fileDef.name}`, 'blue'); if (fs.existsSync(path.join(projectRoot, fileDef.projectLocation[0]))) { logger.debug(`[getToolingFile] Found file at the root of the project: ${path.join(projectRoot, fileDef.projectLocation[0])}`, 'blue'); return path.join(projectRoot, fileDef.projectLocation[0]); } else if (fs.existsSync(path.join(gobstonesScriptsProjectsRoot, fileDef.gobstonesScriptsLocation[0]))) { logger.debug(`[getToolingFile] Found file at the gobstones-scripts library: ${path.join(gobstonesScriptsProjectsRoot, fileDef.gobstonesScriptsLocation[0])}`, 'blue'); return path.join(gobstonesScriptsProjectsRoot, fileDef.gobstonesScriptsLocation[0]); } logger.debug(`[getToolingFile] No file found. ${fileDef.name === 'tsConfigJSON' ? 'This is Ok in this case, not to worry.' : 'A file should have been found. Something wrong happened.'}`, 'blue'); return undefined; }; /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * ---------------------------------------------------- * @module Helpers * @author Alan Rodas Bonjour <alanrodas@gmail.com> * * @internal * ---------------------------------------------------- */ /** * Answers if the platform is macOS. * * @returns `true` if the current OS is MacOS, `false` otherwise. */ const isMacos = () => process && process.platform === 'darwin'; /* * ***************************************************************************** * Copyright (C) National University of Quilmes 2018-2024 * Gobstones (TM) is a trademark of the National University of Quilmes. * * This program is free software distributed under the terms of the * GNU Affero General Public License version 3. * Additional terms added in compliance to section 7 of such license apply. * * You may read the full license at https://gobstones.github.io/gobstones-guidelines/LICENSE. * ***************************************************************************** */ /** * @module Config * @author Alan Rodas Bonjour <alanrodas@gmail.com> */ // ========================================== // #endregion Script File Types // ========================================== // ========================================== // #region Config // ========================================== /** * This class represents the main configuration object generated by the application. * The configuration is automatically loaded once the {@link init} method is called. * This object is also the main entry point to obtain configuration options of the tool * as to obtain the located directories, tooling files and obtain location for the * executable scripts for different tools. */ class Config { // ------------------------------------------ // #endregion Private Properties // ------------------------------------------ /** * Create a new instance of the configuration. */ constructor() { /* * We need to set the logger on if debug was sent from the CLI, * as this i the first thing that happens, even after running any code. * This is clearly a violation of the concern of 'config', but * there doesn't seem to be a better way without recurring to * dynamic imports, which implies changing the build system. */ if (process.argv.includes('-D') || process.argv.includes('--debug')) { logger.level = LogLevel.Debug; logger.on(); } logger.debug('[config] Creating configuration object'); // has not yet been initialized this._lastInitializationValues = undefined; this._binaryFilesCache = {}; } // ------------------------------------------ // #region Accessing // ------------------------------------------ /** Returns the subpart of the configuration corresponding to package managers. */ get packageManagers() { return this._packageManagers; } /** Returns subpart of the configuration corresponding to the environment. */ get environment() { return this._environment; } /** Returns the subpart of the configuration corresponding to the different path locations. */ get locations() { return this._locations; } /** Returns the subpart of the configuration corresponding to the current execution environment. */ get executionEnvironment() { return this._executionEnvironment; } /** Returns the subpart of the configuration corresponding to the different project types. */ get projectTypes() { return this._projectTypes; } /** The subpart of the configuration corresponding to the different project type filtered files. */ get filteredProjectTypes() { return this._filteredProjectTypes; } get packageManager() { return this._packageManagers[this._executionEnvironment.packageManager]; } get projectType() { return this._projectTypes[this._executionEnvironment.projectType]; } get projectTypeFilteredFiles() { return this._filteredProjectTypes[this._executionEnvironment.projectType]; } // ------------------------------------------ // #endregion Accessing // ------------------------------------------ // ------------------------------------------ // #region Initialization // ------------------------------------------ /** * Orchestrate the initialization of the Config object. * This initialization is needed in order to access any of the * sub-configuration sections, except for retrieving * executable scripts. */ init(apiGivenProjectType, apiGivenPackageManager, debug, test, useLocalTsconfigJson) { if (!this._lastInitializationValues) { logger.debug(`[config] Initializing configuration from scratch`); this._initAvailablePackageManagers(); this._detectEnvironment(); this._initializeLocations(); this._loadProjectTypeDefinitions(); this._loadProcessedProjectTypeDefinitions(); } if (!this._lastInitializationValues || (apiGivenProjectType && this._lastInitializationValues.apiGivenProjectType !== apiGivenProjectType) || (apiGivenPackageManager && this._lastInitializationValues.apiGivenPackageManager !== apiGivenPackageManager) || (debug && this._lastInitializationValues.debug !== debug) || (test && this._lastInitializationValues.test !== test)) { logger.debug(`[config] Already initialized, updating CLI/API parameters`); this._initializeExecutionEnvironment(apiGivenProjectType, apiGivenPackageManager, debug, test, useLocalTsconfigJson); this._lastInitializationValues = this._executionEnvironment; } return this; } // ------------------------------------------ // #endregion Initialization // ------------------------------------------ // ------------------------------------------ // #region Public API // ------------------------------------------ /** * Change the current directory of the process to another one. * Additionally, update the global configuration to match. * * @param dir - The directory to change to */ changeDir(dir) { process.chdir(dir); this._environment.workingDirectory = dir; this._locations.projectRoot = dir; return dir; } /** * Return the information for executing a binary file, if it can be found * by the configuration system. Additionally, and differently from the * simple {@link getBin} helper, this method provides caching, as to not * attempt to find the element twice. * * @param packageName - The package name that contains the binary file. * @param binName - The binary file to execute. * * @returns The executable to run, or undefined if not found. */ getBinary(packageName, binName) { if (!this._binaryFilesCache[`${packageName}-${binName}`]) { this._binaryFilesCache[`${packageName}-${binName}`] = getBin(this._locations.projectRoot, this.packageManager, packageName, binName); } return this._binaryFilesCache[`${packageName}-${binName}`]; } // ------------------------------------------ // #endregion Public API // ------------------------------------------ /* ******************************************************** */ // ------------------------------------------ // #region Private Initialization // ------------------------------------------ /** Initialize the different available package managers. */ _initAvailablePackageManagers() { this._packageManagers = { npm: { name: 'npm', cmd: 'npm', install: 'npm install', run: 'npx', modulesFolders: ['node_modules'], binFolders: ['node_modules/bin'] }, yarn: { name: 'yarn', cmd: 'yarn', install: 'yarn install', run: 'npx', modulesFolders: ['node_modules'], binFolders: ['node_modules/bin'] }, pnpm: { name: 'pnpm', cmd: 'pnpm', install: 'pnpm install', run: 'pnpm exec', modulesFolders: ['node_modules', 'node_modules/@gobstones/gobstones-scripts/node_modules'], binFolders: ['node_modules/bin', 'node_modules/@gobstones/gobstones-scripts/node_modules/bin'] } }; } /** Initialize the current environment from detected information. */ _detectEnvironment() { var _a, _b; this._environment = { toolVersion: version, toolTestServer: testServer, operatingSystem: isWindows() ? 'windows' : isMacos() ? 'macos' : 'posix', workingDirectory: (_b = (_a = process.env.PWD) !== null && _a !== void 0 ? _a : process.cwd()) !== null && _b !== void 0 ? _b : path.resolve('.'), detectedPackageManager: getInUsePackageManager(this._packageManagers, 'npm') }; } /** * Initialize the different locations by attempting to detect the current * folder containing a project and the folder containing the gobstones-scripts Library. */ _initializeLocations() { const projectRoot = getProjectRootPath(this._environment.operatingSystem); const gobstonesScriptsRoot = getGobstonesScriptsRootPath(this._environment.operatingSystem, projectRoot); this._locations = { projectRoot, gobstonesScriptsRoot, gobstonesScriptsProjectsRoot: path.join(gobstonesScriptsRoot, 'project-types') }; } /** * Initialize the current execution environment. This is obtained by a * mix between possible CLI/API given parameters (such as using -t or -m), * given as input, and the information read in the current's project package.json, * as well as defaults in case no configuration is provided. * * Priority is given to CLI/API given, then the package.json configuration * and lastly defaults. * * @param apiGivenProjectType - The CLI/API given value for project type to use, if any. * @param apiGivenPackageManager - The CLI/API given value for package manager to use, if any. * @param debug - The CLI/API given value to know if we are running in debug mode. * @param test - The CLI/API given value to know if we are running in test mode. * @param useLocalTsconfigJson - The CLI/API given value to know if we should use the default tsconfig.json file. */ _initializeExecutionEnvironment(apiGivenProjectType, apiGivenPackageManager, debug, test, useLocalTsconfigJson) { var _a, _b, _c, _d, _e, _f; if (apiGivenProjectType && !Object.keys(this._projectTypes).includes(apiGivenProjectType)) { throw new Error('Invalid project type'); } if (apiGivenPackageManager && !Object.keys(this._packageManagers).includes(apiGivenPackageManager)) { throw new Error('Invalid package manager'); } const pkgReader = new PackageJsonReader(path.join(this._locations.projectRoot, 'package.json')); this._executionEnvironment = { projectType: ((_a = apiGivenProjectType !== null && apiGivenProjectType !== void 0 ? apiGivenProjectType : pkgReader.getValueAt('config.gobstones-scripts.type')) !== null && _a !== void 0 ? _a : 'Library'), packageManager: ((_b = apiGivenPackageManager !== null && apiGivenPackageManager !== void 0 ? apiGivenPackageManager : pkgReader.getValueAt('config.gobstones-scripts.manager')) !== null && _b !== void 0 ? _b : 'npm'), useFullPaths: ((_c = pkgReader.getValueAt('config.gobstones-scripts.use-full-paths')) !== null && _c !== void 0 ? _c : false), useLocalTsconfigJson: ((_d = useLocalTsconfigJson !== null && useLocalTsconfigJson !== void 0 ? useLocalTsconfigJson : pkgReader.getValueAt('config.gobstones-scripts.use-local-tsconfig-json')) !== null && _d !== void 0 ? _d : fs.existsSync(path.join(this._locations.projectRoot, 'package.json'))), debug: ((_e = debug !== null && debug !== void 0 ? debug : pkgReader.getValueAt('config.gobstones-scripts.debug')) !== null && _e !== void 0 ? _e : false), test: ((_f = test !== null && test !== void 0 ? test : pkgReader.getValueAt('config.gobstones-scripts.test')) !== null && _f !== void 0 ? _f : false) }; } /** * Initialize the different project type definitions with all their * file information. */ _loadProjectTypeDefinitions() { this._projectTypes = { Library: this._joinProjectTypeDefinitions(this._getCommonProjectTypeDefinition('Library', ['src', 'test', 'packageJson', 'typescript', 'rollup', 'nps'], ['jestproxies', 'vite', 'stories', 'storybook', 'demos'])), CLILibrary: this._joinProjectTypeDefinitions(this._getCommonProjectTypeDefinition('CLILibrary', ['src', 'test', 'packageJson', 'typescript', 'rollup', 'nps'], ['jestproxies', 'vite', 'stories', 'storybook', 'demos'])), ReactLibrary: this._joinProjectTypeDefinitions(this._getCommonProjectTypeDefinition('ReactLibrary', [ 'src', 'test', 'packageJson', 'typescript', 'rollup', 'nps', 'typedoc', 'jestproxies', 'vite', 'stories', 'storybook' ], ['demos'])), NonCode: this._joinProjectTypeDefinitions(this._getCommonProjectTypeDefinition('NonCode', ['src', 'test', 'packageJson'], [ 'eslint', 'tsConfigJSON', 'typescript', 'rollup', 'typedoc', 'jest', 'jestproxies', 'vite', 'stories', 'storybook', 'demos' ]), { // On node code, nps should always be copied to the project's root nps: this._fileDefinitionWithTooling('nps', 'NonCode', ['nps'], [], { gobstonesScriptsLocation: ['<projectTypePath>/package-scripts.js'], projectLocation: ['package-scripts.js'], copyOnInit: true, isOverridable: true }) }) }; } /** * Returns the file information for all files that are common to any * project. Expects the route of the project's subfolder. * * @param projectTypePath - The route of the project's subfolder (e.g. 'CLILibrary' or 'NonCode') * @param noCommonFiles - The filenames to search in the project specific folder, instead of the common. * @param excludedFiles - Files from the common folder to exclude from this project definition. * * @returns A partial ProjectTypeDefinition. */ _getCommonProjectTypeDefinition(projectTypePath, noCommonFiles, excludedFiles) { return { // only on init src: this._fileDefinition('src', projectTypePath, noCommonFiles, excludedFiles, { gobstonesScriptsLocation: ['<projectTypePath>/src'], projectLocation: ['src'], copyOnInit: true }), test: this._fileDefinition('test', projectTypePath, noCommonFiles, excludedFiles, { gobstonesScriptsLocation: ['<projectTypePath>/test'], projectLocation: ['test'], copyOnInit: true }), changelog: this._fileDefinition('changelog', projectTypePath, noCommonFiles, exclud