UNPKG

scaffolder-toolkit

Version:

šŸš€ A universal command-line tool for developers to automate project scaffolding and streamline their workflows.

1,651 lines (1,552 loc) • 99.2 kB
import { Command } from 'commander'; import { existsSync as existsSync$1, promises } from 'fs'; import path, { dirname } from 'path'; import os, { homedir } from 'os'; import { fileURLToPath } from 'url'; import chalk from 'chalk'; import ora from 'ora'; import { promisify } from 'node:util'; import childProcess, { spawnSync } from 'node:child_process'; import { execa, execaCommand } from 'execa'; import { select } from '@inquirer/prompts'; const ProgrammingLanguage = { Javascript: "Javascript", Typescript: "Typescript", Nodejs: "Nodejs", }; const JavascriptPackageManagers = { Bun: "bun", Npm: "npm", Yarn: "yarn", Pnpm: "pnpm", }; const PackageManagers = { ...JavascriptPackageManagers, }; const VALID_PACKAGE_MANAGERS = Object.seal(Object.values(PackageManagers)); const VALID_CACHE_STRATEGIES = [ "always-refresh", "never-refresh", "daily", ]; const TextLanguages = { English: "en", French: "fr", }; const SUPPORTED_LANGUAGES = Object.seal(Object.values(TextLanguages)); const ProgrammingLanguageAlias = { js: "javascript", ts: "typescript", node: "nodejs", }; const DisplayModes = { Tree: "tree", Table: "table", }; const genericNodejsTemplate = { description: "A generic, unopinionated Node.js/Typescript project boilerplate.", location: "https://github.com/IT-WIBRC/devkit-node-boilerplate.git", alias: "node", cacheStrategy: "daily", }; const baseJsTemplates = { nodejs: genericNodejsTemplate, vue: { description: "An official Vue.js project.", location: "{pm} create vue@latest", cacheStrategy: "always-refresh", }, nuxt: { description: "An official Nuxt.js project.", location: "{pm} create nuxt@latest", alias: "nx", }, nest: { description: "An official Nest.js project.", location: "{pm} install -g @nestjs/cli && nest new", }, nextjs: { description: "An official Next.js project.", location: "{pm} create next-app@latest", alias: "next", }, express: { description: "A simple Express.js boilerplate from its generator.", location: "https://github.com/expressjs/express-generator.git", alias: "ex", }, fastify: { description: "A highly performant Fastify web framework boilerplate.", location: "https://github.com/fastify/fastify-cli.git", alias: "fy", }, koa: { description: "A Koa.js web framework boilerplate.", location: "https://github.com/koajs/koa-generator.git", }, adonis: { description: "A full-stack Node.js framework (AdonisJS).", location: "{pm} create adonisjs", alias: "ad", }, sails: { description: "A real-time, MVC framework (Sails.js).", location: "{pm} install -g sails && sails new", }, angular: { description: "An official Angular project.", location: "{pm} install -g @angular/cli && ng new", alias: "ng", }, "angular-vite": { description: "An Angular project using Vite via AnalogJS.", location: "{pm} create analog@latest", alias: "ng-v", }, react: { description: "A React project using the recommended Vite setup.", location: "{pm} create vite@latest -- --template react", alias: "rt", }, svelte: { description: "A Svelte project using SvelteKit.", location: "{pm} create svelte@latest", }, qwik: { description: "An official Qwik project.", location: "{pm} create qwik@latest", }, astro: { description: "A new Astro project.", location: "{pm} create astro@latest", }, solid: { description: "An official SolidJS project.", location: "{pm} create solid@latest", }, remix: { description: "An official Remix project.", location: "{pm} create remix@latest", }, }; const defaultCliConfig = { templates: { javascript: { templates: baseJsTemplates, }, typescript: { templates: baseJsTemplates, }, nodejs: { templates: baseJsTemplates, }, }, settings: { defaultPackageManager: PackageManagers.Bun, cacheStrategy: "daily", language: TextLanguages.English, }, }; const CONFIG_FILE_NAMES = [".devkitrc", ".devkit.json"]; const jsFiles = { lockFiles: ["package-lock.json", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"], }; const FILE_NAMES = { packageJson: "package.json", node_modules: "node_modules", common: { git: ".git", }, javascript: { ...jsFiles, }, typescript: { ...jsFiles, }, nodejs: { ...jsFiles, }, }; async function readJson(filePath) { const fileContent = await promises.readFile(filePath, { encoding: "utf8", }); return JSON.parse(fileContent); } async function writeJson(filePath, data) { const jsonString = JSON.stringify(data, null, 2); await promises.writeFile(filePath, jsonString); } const existsSync = existsSync$1; async function copy(source, destination, options = {}) { const filterFunction = options.filter || (() => true); const stats = await promises.stat(source); if (stats.isDirectory()) { if (!filterFunction(source)) { return; } await promises.mkdir(destination, { recursive: true }); const entries = await promises.readdir(source); for (const entry of entries) { const srcPath = path.join(source, entry); const destPath = path.join(destination, entry); await copy(srcPath, destPath, options); } } else if (stats.isFile()) { if (!filterFunction(source)) { return; } await promises.copyFile(source, destination); } } async function pathExists(filePath) { try { await promises.access(filePath); return true; // oxlint-disable-next-line no-unused-vars } catch (error) { return false; } } async function stat(filePath) { return await promises.stat(filePath); } async function ensureDir(dirPath) { await promises.mkdir(dirPath, { recursive: true }); } async function remove(path) { await promises.rm(path, { recursive: true, force: true }); } const writeFile = writeJson; var fs = { readJson, writeJson, existsSync, copy, pathExists, stat, ensureDir, remove, writeFile, }; async function findUp({ files, cwd, limit, }) { let currentDir = path.resolve(cwd ?? process.cwd()); const filesToFind = Array.isArray(files) ? files : [files]; while (true) { for (const file of filesToFind) { const filePath = path.join(currentDir, file); try { const stats = await fs.stat(filePath); if (stats.isDirectory() || stats.isFile()) { return filePath; } // oxlint-disable-next-line no-unused-vars } catch (e) { // File does not exist, continue search } } if (currentDir === limit) { break; } const parentDir = path.dirname(currentDir); if (parentDir === currentDir || currentDir === homedir()) { break; } currentDir = parentDir; } return null; } class DevkitError extends Error { constructor(message, options) { super(message, options); this.name = "DevkitError"; } } class ConfigError extends DevkitError { filePath; constructor(message, filePath, options) { super(message, options); this.filePath = filePath; this.name = "ConfigError"; } } class GitError extends DevkitError { url; constructor(message, url, options) { super(message, options); this.url = url; this.name = "GitError"; } } async function findFileInDirectory(directory, fileNames) { for (const fileName of fileNames) { const filePath = path.join(directory, fileName); if (await fs.pathExists(filePath)) { return filePath; } } return null; } const MONOREPO_INDICATORS = [ "pnpm-workspace.yaml", "lerna.json", ]; const NODE_MODULES = "node_modules"; async function findMonorepoRoot() { const foundFile = await findUp({ files: [...MONOREPO_INDICATORS, NODE_MODULES], }); if (!foundFile) { return null; } const rootDir = path.dirname(foundFile); const fileName = path.basename(foundFile); if (MONOREPO_INDICATORS.includes(fileName) || fileName === NODE_MODULES) { return rootDir; } return null; } async function findProjectRoot() { const filePath = await findUp({ files: [NODE_MODULES], }); if (!filePath) { return null; } return path.dirname(filePath); } async function findPackageRoot() { const __filename = fileURLToPath(import.meta.url); const startDir = dirname(__filename); const filePath = await findUp({ files: [FILE_NAMES.packageJson], cwd: startDir, }); if (!filePath) { throw new DevkitError("Package root not found. Cannot determine the root of the devkit."); } return path.dirname(filePath); } const allConfigFiles = [...CONFIG_FILE_NAMES]; async function findGlobalConfigFile() { const homeDir = os.homedir(); return findFileInDirectory(homeDir, allConfigFiles); } async function findLocalConfigFile() { const monorepoRoot = await findMonorepoRoot(); const projectRoot = await findProjectRoot(); const searchLimit = monorepoRoot || projectRoot; if (!searchLimit) { return findUp({ files: [...CONFIG_FILE_NAMES], cwd: process.cwd(), limit: process.cwd(), }); } return findUp({ files: [...CONFIG_FILE_NAMES], cwd: process.cwd(), limit: searchLimit, }); } async function getConfigFilepath(isGlobal = false) { const allConfigFiles = [...CONFIG_FILE_NAMES]; if (isGlobal) { return (await findGlobalConfigFile()) || ""; } const localConfigPath = await findUp({ files: [...allConfigFiles], cwd: process.cwd(), }); if (localConfigPath) { return localConfigPath; } return path.join(process.cwd(), allConfigFiles[1]); } async function getConfigPathSources(options) { const localConfigPath = await findLocalConfigFile(); const globalConfigPath = await findGlobalConfigFile(); const isConfigPathExist = async (path) => path ? await fs.pathExists(path) : false; const hasLocal = await isConfigPathExist(localConfigPath); const hasGlobal = await isConfigPathExist(globalConfigPath); let finalLocalPath = null; let finalGlobalPath = null; const shouldMergeAll = !!options.mergeAll; if (shouldMergeAll) { finalLocalPath = hasLocal ? localConfigPath : null; finalGlobalPath = hasGlobal ? globalConfigPath : null; } else if (options.forceLocal) { finalLocalPath = hasLocal ? localConfigPath : null; finalGlobalPath = null; } else if (options.forceGlobal) { finalLocalPath = null; finalGlobalPath = hasGlobal ? globalConfigPath : null; } else { if (hasLocal) { finalLocalPath = localConfigPath; finalGlobalPath = null; } else if (hasGlobal) { finalLocalPath = null; finalGlobalPath = globalConfigPath; } } return { localPath: finalLocalPath, globalPath: finalGlobalPath, }; } const stripAnsi = (str) => { return str.replace( // oxlint-disable-next-line no-control-regex /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ""); }; function getTimestamp() { const date = new Date(); const time = date.toTimeString().split(" ")[0]; return chalk.dim(`[${time}]`); } function formatError(message, errorType) { const timestamp = getTimestamp(); const typeTag = chalk.bold.red(`āŒ${timestamp}::[${errorType}]`); const coloredMessage = chalk.redBright(`${message}`); return `${typeTag}>> ${coloredMessage}`; } function _logTable(data) { if (!Array.isArray(data) || data.length <= 1 || data[0].length === 0) { return; } const numCols = data[0].length; // oxlint-disable-next-line no-new-array const colWidths = new Array(numCols).fill(0); const PADDING = 3; for (const row of data) { for (let i = 0; i < numCols; i++) { const cell = row[i] || ""; const cellTextLength = stripAnsi(cell).length; if (cellTextLength > colWidths[i]) { colWidths[i] = cellTextLength; } } } data.forEach((row, rowIndex) => { let line = ""; for (let i = 0; i < numCols; i++) { const cell = row[i] || ""; const targetWidth = colWidths[i]; const cellTextLength = stripAnsi(cell).length; const paddingSpaces = " ".repeat(targetWidth - cellTextLength); const columnSeparator = " "; line += cell + paddingSpaces + columnSeparator.repeat(PADDING); } console.log(line.trimEnd()); if (rowIndex === 0) { const totalWidth = colWidths.reduce((sum, width) => sum + width, 0) + (numCols - 1) * PADDING; console.log(chalk.dim("-".repeat(totalWidth))); } }); } const logger = { info(message) { console.log(chalk.blue(message)); }, success(message) { console.log(chalk.green(`\nāœ” ${message}`)); }, warning(message) { console.log(chalk.yellow(`āš ļø ${message}`)); }, error(message, errorType = "UNKNOWN") { console.error(formatError(message, errorType)); }, log(message) { console.log(message); }, spinner(text) { return ora(text); }, dimmed(message) { console.log(chalk.dim(message.trim())); }, table(data) { _logTable(data); }, colors: { white: (message) => chalk.white(message), blue: (message) => chalk.blue(message), green: (message) => chalk.green(message), yellow: (message) => chalk.yellow(message), red: (message) => chalk.red(message), cyan: (message) => chalk.cyan(message), dim: (message) => chalk.dim(message), bold: (message) => chalk.bold(message), italic: (message) => chalk.italic(message), boldBlue: (message) => chalk.bold.blue(message), cyanDim: (message) => chalk.cyan.dim(message), yellowBold: (message) => chalk.bold.yellow(message), redBright: (message) => chalk.redBright(message), greenBright: (message) => chalk.greenBright(message), magenta: (message) => chalk.magenta(message), magentaBright: (message) => chalk.magentaBright(message), }, }; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var lcid$1 = {}; var invertKv; var hasRequiredInvertKv; function requireInvertKv () { if (hasRequiredInvertKv) return invertKv; hasRequiredInvertKv = 1; invertKv = object => { if (typeof object !== 'object' || object === null) { throw new TypeError('Expected an object'); } const result = {}; for (const [key, value] of Object.entries(object)) { result[value] = key; } for (const symbol of Object.getOwnPropertySymbols(object)) { const value = object[symbol]; result[value] = symbol; } return result; }; return invertKv; } var require$$1 = { "4": "zh_CHS", "1025": "ar_SA", "1026": "bg_BG", "1027": "ca_ES", "1028": "zh_TW", "1029": "cs_CZ", "1030": "da_DK", "1031": "de_DE", "1032": "el_GR", "1033": "en_US", "1034": "es_ES", "1035": "fi_FI", "1036": "fr_FR", "1037": "he_IL", "1038": "hu_HU", "1039": "is_IS", "1040": "it_IT", "1041": "ja_JP", "1042": "ko_KR", "1043": "nl_NL", "1044": "nb_NO", "1045": "pl_PL", "1046": "pt_BR", "1047": "rm_CH", "1048": "ro_RO", "1049": "ru_RU", "1050": "hr_HR", "1051": "sk_SK", "1052": "sq_AL", "1053": "sv_SE", "1054": "th_TH", "1055": "tr_TR", "1056": "ur_PK", "1057": "id_ID", "1058": "uk_UA", "1059": "be_BY", "1060": "sl_SI", "1061": "et_EE", "1062": "lv_LV", "1063": "lt_LT", "1064": "tg_TJ", "1065": "fa_IR", "1066": "vi_VN", "1067": "hy_AM", "1069": "eu_ES", "1070": "wen_DE", "1071": "mk_MK", "1074": "tn_ZA", "1076": "xh_ZA", "1077": "zu_ZA", "1078": "af_ZA", "1079": "ka_GE", "1080": "fo_FO", "1081": "hi_IN", "1082": "mt_MT", "1083": "se_NO", "1086": "ms_MY", "1087": "kk_KZ", "1088": "ky_KG", "1089": "sw_KE", "1090": "tk_TM", "1092": "tt_RU", "1093": "bn_IN", "1094": "pa_IN", "1095": "gu_IN", "1096": "or_IN", "1097": "ta_IN", "1098": "te_IN", "1099": "kn_IN", "1100": "ml_IN", "1101": "as_IN", "1102": "mr_IN", "1103": "sa_IN", "1104": "mn_MN", "1105": "bo_CN", "1106": "cy_GB", "1107": "kh_KH", "1108": "lo_LA", "1109": "my_MM", "1110": "gl_ES", "1111": "kok_IN", "1114": "syr_SY", "1115": "si_LK", "1118": "am_ET", "1121": "ne_NP", "1122": "fy_NL", "1123": "ps_AF", "1124": "fil_PH", "1125": "div_MV", "1128": "ha_NG", "1130": "yo_NG", "1131": "quz_BO", "1132": "ns_ZA", "1133": "ba_RU", "1134": "lb_LU", "1135": "kl_GL", "1144": "ii_CN", "1146": "arn_CL", "1148": "moh_CA", "1150": "br_FR", "1152": "ug_CN", "1153": "mi_NZ", "1154": "oc_FR", "1155": "co_FR", "1156": "gsw_FR", "1157": "sah_RU", "1158": "qut_GT", "1159": "rw_RW", "1160": "wo_SN", "1164": "gbz_AF", "2049": "ar_IQ", "2052": "zh_CN", "2055": "de_CH", "2057": "en_GB", "2058": "es_MX", "2060": "fr_BE", "2064": "it_CH", "2067": "nl_BE", "2068": "nn_NO", "2070": "pt_PT", "2077": "sv_FI", "2080": "ur_IN", "2092": "az_AZ", "2094": "dsb_DE", "2107": "se_SE", "2108": "ga_IE", "2110": "ms_BN", "2115": "uz_UZ", "2128": "mn_CN", "2129": "bo_BT", "2141": "iu_CA", "2143": "tmz_DZ", "2155": "quz_EC", "3073": "ar_EG", "3076": "zh_HK", "3079": "de_AT", "3081": "en_AU", "3082": "es_ES", "3084": "fr_CA", "3098": "sr_SP", "3131": "se_FI", "3179": "quz_PE", "4097": "ar_LY", "4100": "zh_SG", "4103": "de_LU", "4105": "en_CA", "4106": "es_GT", "4108": "fr_CH", "4122": "hr_BA", "4155": "smj_NO", "5121": "ar_DZ", "5124": "zh_MO", "5127": "de_LI", "5129": "en_NZ", "5130": "es_CR", "5132": "fr_LU", "5179": "smj_SE", "6145": "ar_MA", "6153": "en_IE", "6154": "es_PA", "6156": "fr_MC", "6203": "sma_NO", "7169": "ar_TN", "7177": "en_ZA", "7178": "es_DO", "7194": "sr_BA", "7227": "sma_SE", "8193": "ar_OM", "8201": "en_JA", "8202": "es_VE", "8218": "bs_BA", "8251": "sms_FI", "9217": "ar_YE", "9225": "en_CB", "9226": "es_CO", "9275": "smn_FI", "10241": "ar_SY", "10249": "en_BZ", "10250": "es_PE", "11265": "ar_JO", "11273": "en_TT", "11274": "es_AR", "12289": "ar_LB", "12297": "en_ZW", "12298": "es_EC", "13313": "ar_KW", "13321": "en_PH", "13322": "es_CL", "14337": "ar_AE", "14346": "es_UR", "15361": "ar_BH", "15370": "es_PY", "16385": "ar_QA", "16394": "es_BO", "17417": "en_MY", "17418": "es_SV", "18441": "en_IN", "18442": "es_HN", "19466": "es_NI", "20490": "es_PR", "21514": "es_US", "31748": "zh_CHT" }; var hasRequiredLcid; function requireLcid () { if (hasRequiredLcid) return lcid$1; hasRequiredLcid = 1; const invertKv = /*@__PURE__*/ requireInvertKv(); const all = require$$1; const inverted = invertKv(all); lcid$1.from = lcidCode => { if (typeof lcidCode !== 'number') { throw new TypeError('Expected a number'); } return all[lcidCode]; }; lcid$1.to = localeId => { if (typeof localeId !== 'string') { throw new TypeError('Expected a string'); } const lcidCode = inverted[localeId]; if (lcidCode) { return Number(inverted[localeId]); } }; lcid$1.all = new Proxy( inverted, { get(target, name) { const lcid = target[name]; if (lcid) { return Number(lcid); } } } ); return lcid$1; } var lcidExports = /*@__PURE__*/ requireLcid(); var lcid = /*@__PURE__*/getDefaultExportFromCjs(lcidExports); // Mini wrapper around `child_process` to make it behave a little like `execa`. const execFile = promisify(childProcess.execFile); /** @param {string} command @param {string[]} arguments_ @returns {Promise<import('child_process').ChildProcess>} */ async function exec(command, arguments_) { const subprocess = await execFile(command, arguments_, {encoding: 'utf8'}); subprocess.stdout = subprocess.stdout.trim(); return subprocess; } const defaultOptions = {spawn: true}; const defaultLocale = 'en-US'; async function getStdOut(command, args) { return (await exec(command, args)).stdout; } function getEnvLocale(env = process.env) { return env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE; } function parseLocale(string) { const env = {}; for (const definition of string.split('\n')) { const [key, value] = definition.split('='); env[key] = value.replace(/^"|"$/g, ''); } return getEnvLocale(env); } function getLocale(string) { return (string && string.replace(/[.:].*/, '')); } async function getLocales() { return getStdOut('locale', ['-a']); } function getSupportedLocale(locale, locales = '') { return locales.includes(locale) ? locale : defaultLocale; } async function getAppleLocale() { const results = await Promise.all([ getStdOut('defaults', ['read', '-globalDomain', 'AppleLocale']), getLocales(), ]); return getSupportedLocale(results[0], results[1]); } async function getUnixLocale() { return getLocale(parseLocale(await getStdOut('locale'))); } async function getWinLocale() { const stdout = await getStdOut('wmic', ['os', 'get', 'locale']); const lcidCode = Number.parseInt(stdout.replace('Locale', ''), 16); return lcid.from(lcidCode); } function normalise(input) { return input.replace(/_/, '-'); } const cache$1 = new Map(); async function osLocale(options = defaultOptions) { if (cache$1.has(options.spawn)) { return cache$1.get(options.spawn); } let locale; try { const envLocale = getEnvLocale(); if (envLocale || options.spawn === false) { locale = getLocale(envLocale); } else if (process.platform === 'win32') { locale = await getWinLocale(); } else if (process.platform === 'darwin') { locale = await getAppleLocale(); } else { locale = await getUnixLocale(); } } catch {} const normalised = normalise(locale || defaultLocale); cache$1.set(options.spawn, normalised); return normalised; } async function findLocalesDir() { const packageRoot = await findPackageRoot(); return path.join(packageRoot, "locales"); } let translations = {}; let activeLanguage = "en"; function getSupportedLanguage(lang) { if (!lang) return undefined; const supportedLanguages = Object.values(TextLanguages); const validatedLang = lang?.split(/[_.-]/)[0]?.toLowerCase(); return supportedLanguages.includes(validatedLang) ? validatedLang : undefined; } async function loadTranslations(configLang) { const userLang = getSupportedLanguage(configLang); const rawSystemLocale = await osLocale(); const systemLang = getSupportedLanguage(rawSystemLocale); const languageToLoad = userLang || systemLang || "en"; activeLanguage = languageToLoad; try { const localesDir = await findLocalesDir(); const filePath = path.join(localesDir, `${languageToLoad}.json`); translations = await fs.readJson(filePath); // oxlint-disable-next-line no-unused-vars } catch (error) { const localesDir = await findLocalesDir(); const fallbackPath = path.join(localesDir, "en.json"); try { translations = await fs.readJson(fallbackPath); activeLanguage = "en"; } catch (e) { throw new DevkitError(`Failed to load translations from both ${languageToLoad}.json and the fallback en.json`, { cause: e }); } } } function resolveNestedKey(obj, key) { const parts = key.split("."); let current = obj; for (const part of parts) { if (current && typeof current === "object" && part in current) { current = current[part]; } else { return undefined; } } return typeof current === "string" ? current : undefined; } function t(key, variables = {}) { const translatedString = resolveNestedKey(translations, key) || key; let result = translatedString; for (const [varName, varValue] of Object.entries(variables)) { result = result.replace(`{${varName}}`, varValue); } return result; } async function readSingleConfig(path) { if (path && (await fs.pathExists(path))) { try { return (await fs.readJson(path)); } catch (e) { if (e instanceof Error) { logger.error(t("errors.config.read_fail_path", { path }), "ERR"); logger.warning(t("warnings.not_found")); } else { logger.error(t("errors.generic.unexpected"), "UNKNOWN"); } return null; } } return null; } async function readConfigSources(options = {}) { const { localPath, globalPath } = await getConfigPathSources(options); const [localConfig, globalConfig] = await Promise.all([ readSingleConfig(localPath), readSingleConfig(globalPath), ]); const configFound = !!localConfig || !!globalConfig; return { default: structuredClone(defaultCliConfig), global: structuredClone(globalConfig), local: structuredClone(localConfig), configFound, }; } async function getProjectVersion() { try { const packageRoot = await findPackageRoot(); if (!packageRoot) { throw new Error(t("errors.system.package_root_not_found")); } const packageJsonPath = path.join(packageRoot, FILE_NAMES.packageJson); const packageJson = await fs.readJson(packageJsonPath); return packageJson.version; } catch (error) { const errorMessage = t("errors.system.version_read_fail"); if (error instanceof Error) { logger.error(`${errorMessage}: ${error.message}`, "INFO"); } else { logger.error(errorMessage, "INFO"); } if (error instanceof Error && error.stack) { logger.dimmed(error.stack); } return "0.0.0"; } } function handleErrorAndExit(error, spinner) { spinner?.stop(); if (error instanceof ConfigError) { logger.error(`${t("errors.config.read_fail")}: ${error.message}`, "CONFIG"); if (error.filePath) { logger.dimmed(`File path: ${error.filePath}`); } } else if (error instanceof GitError) { logger.error(`${t("errors.system.git_generic")}: ${error.message}`, "GIT"); if (error.url) { logger.dimmed(`Repository URL: ${error.url}`); } } else if (error instanceof DevkitError) { logger.error(`${t("errors.generic.devkit_specific")}: ${error.message}`, "DEV"); } else if (error instanceof Error) { logger.error(`${t("errors.generic.unexpected")}: ${error.message}`, "ERR"); } else { logger.error(t("errors.generic.unknown"), "UNKNOWN"); } const cause = error instanceof Error ? error.cause : undefined; if (cause instanceof Error) { logger.dimmed(`Cause: ${cause.message}`); } process.exit(1); } function validatePackageManager(value) { const validPackageManagers = Object.values(PackageManagers); if (!validPackageManagers.includes(value)) { throw new DevkitError(t("errors.validation.invalid_value", { key: "defaultPackageManager", options: validPackageManagers.join(", "), })); } } function validateCacheStrategy(value) { const validStrategies = VALID_CACHE_STRATEGIES; if (!validStrategies.includes(value)) { throw new DevkitError(t("errors.validation.invalid_value", { key: "cacheStrategy", options: validStrategies.join(", "), })); } } function validateLanguage(value) { const validLanguages = Object.values(TextLanguages); if (!validLanguages.includes(value)) { throw new DevkitError(t("errors.validation.invalid_value", { key: "language", options: validLanguages.join(", "), })); } } function validateProgrammingLanguage(value) { const validLanguages = Object.values(ProgrammingLanguage).map((value) => value.toLowerCase()); if (!validLanguages.includes(value)) { throw new DevkitError(t("errors.validation.invalid_value", { key: "Programming Language", options: validLanguages.join(", "), })); } } function validateDisplayMode(value) { const validDisplayMode = Object.values(DisplayModes).map((value) => value.toLowerCase()); if (!validDisplayMode.includes(value)) { throw new DevkitError(t("errors.validation.invalid_value", { key: "mode", options: validDisplayMode.join(", "), })); } } var cjs; var hasRequiredCjs; function requireCjs () { if (hasRequiredCjs) return cjs; hasRequiredCjs = 1; var isMergeableObject = function isMergeableObject(value) { return isNonNullObject(value) && !isSpecial(value) }; function isNonNullObject(value) { return !!value && typeof value === 'object' } function isSpecial(value) { var stringValue = Object.prototype.toString.call(value); return stringValue === '[object RegExp]' || stringValue === '[object Date]' || isReactElement(value) } // see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25 var canUseSymbol = typeof Symbol === 'function' && Symbol.for; var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7; function isReactElement(value) { return value.$$typeof === REACT_ELEMENT_TYPE } function emptyTarget(val) { return Array.isArray(val) ? [] : {} } function cloneUnlessOtherwiseSpecified(value, options) { return (options.clone !== false && options.isMergeableObject(value)) ? deepmerge(emptyTarget(value), value, options) : value } function defaultArrayMerge(target, source, options) { return target.concat(source).map(function(element) { return cloneUnlessOtherwiseSpecified(element, options) }) } function getMergeFunction(key, options) { if (!options.customMerge) { return deepmerge } var customMerge = options.customMerge(key); return typeof customMerge === 'function' ? customMerge : deepmerge } function getEnumerableOwnPropertySymbols(target) { return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(target).filter(function(symbol) { return Object.propertyIsEnumerable.call(target, symbol) }) : [] } function getKeys(target) { return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)) } function propertyIsOnObject(object, property) { try { return property in object } catch(_) { return false } } // Protects from prototype poisoning and unexpected merging up the prototype chain. function propertyIsUnsafe(target, key) { return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet, && !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain, && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable. } function mergeObject(target, source, options) { var destination = {}; if (options.isMergeableObject(target)) { getKeys(target).forEach(function(key) { destination[key] = cloneUnlessOtherwiseSpecified(target[key], options); }); } getKeys(source).forEach(function(key) { if (propertyIsUnsafe(target, key)) { return } if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) { destination[key] = getMergeFunction(key, options)(target[key], source[key], options); } else { destination[key] = cloneUnlessOtherwiseSpecified(source[key], options); } }); return destination } function deepmerge(target, source, options) { options = options || {}; options.arrayMerge = options.arrayMerge || defaultArrayMerge; options.isMergeableObject = options.isMergeableObject || isMergeableObject; // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() // implementations can use it. The caller may not replace it. options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified; var sourceIsArray = Array.isArray(source); var targetIsArray = Array.isArray(target); var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray; if (!sourceAndTargetTypesMatch) { return cloneUnlessOtherwiseSpecified(source, options) } else if (sourceIsArray) { return options.arrayMerge(target, source, options) } else { return mergeObject(target, source, options) } } deepmerge.all = function deepmergeAll(array, options) { if (!Array.isArray(array)) { throw new Error('first argument should be an array') } return array.reduce(function(prev, next) { return deepmerge(prev, next, options) }, {}) }; var deepmerge_1 = deepmerge; cjs = deepmerge_1; return cjs; } var cjsExports = /*@__PURE__*/ requireCjs(); var deepmerge = /*@__PURE__*/getDefaultExportFromCjs(cjsExports); function mergeCliConfigs(configs) { const mergeOptions = { arrayMerge: (_, sourceArray) => sourceArray, }; const validConfigs = configs.filter((config) => !!config); if (validConfigs.length === 0) { return {}; } const mergedConfig = validConfigs.reduce((accumulator, currentConfig) => { return deepmerge(accumulator, currentConfig, mergeOptions); }); return mergedConfig; } async function getMergedConfig(mergeAll = true) { const { local, global, default: defaultConfig, } = await readConfigSources({ mergeAll: mergeAll, }); return mergeCliConfigs([defaultConfig, global, local]); } var merger = /*#__PURE__*/Object.freeze({ __proto__: null, getMergedConfig: getMergedConfig, mergeCliConfigs: mergeCliConfigs }); const mapLanguageAliasToCanonicalKey = (inputLang) => { const lowerInput = inputLang.toLowerCase(); if (Object.prototype.hasOwnProperty.call(ProgrammingLanguageAlias, lowerInput)) { return ProgrammingLanguageAlias[lowerInput]; } const canonicalKeys = Object.values(ProgrammingLanguageAlias); if (canonicalKeys.includes(lowerInput)) { return lowerInput; } return lowerInput; }; const CONSTRAINED_VALUES_MAP = { cacheStrategy: VALID_CACHE_STRATEGIES, packageManager: VALID_PACKAGE_MANAGERS, language: SUPPORTED_LANGUAGES, supportedLanguage: Object.values(ProgrammingLanguage) .map((v) => v.toLowerCase()) .concat(...Object.keys(ProgrammingLanguageAlias)), mode: Object.values(DisplayModes).map((v) => v.toLowerCase()), }; function generateDynamicHelpText(key, i18nKey) { const values = CONSTRAINED_VALUES_MAP[key]; const valueList = values .map((v) => (typeof v === "string" ? v : String(v))) .map((v) => `\`${v}\``) .join(", "); return t(i18nKey, { options: valueList }); } const getScaffolder = async (language) => { if (["javascript", "typescript", "nodejs"].includes(language)) { const { scaffoldProject } = await import('./javascript-CHP3gzBq.js'); return scaffoldProject; } throw new DevkitError(t("errors.scaffolding.language_not_found", { language })); }; function setupNewCommand(options) { const { program } = options; program .command("new") .alias("nw") .description(t("commands.new.command.description")) .argument("<language>", generateDynamicHelpText("language", "commands.new.project.language.argument")) .argument("<projectName>", t("commands.new.project.name.argument")) .requiredOption("-t, --template <string>", t("commands.new.project.template.option.description")) .action(async (language, projectName, cmdOptions) => { const { template } = cmdOptions; const scaffoldSpinner = logger .spinner(logger.colors.cyan(t("messages.status.scaffolding_project", { projectName, template: template, }))) .start(); try { language = mapLanguageAliasToCanonicalKey(language); validateProgrammingLanguage(language); const config = await getMergedConfig(true); const languageTemplates = config.templates[language]; if (!languageTemplates) { throw new DevkitError(t("errors.scaffolding.language_not_found", { language })); } const templateConfig = languageTemplates.templates[template] || Object.values(languageTemplates.templates).find((t) => t.alias === template); if (!templateConfig) { throw new DevkitError(t("errors.template.not_found", { template })); } const scaffoldAppropriateProject = await getScaffolder(language); scaffoldSpinner.stop(); await scaffoldAppropriateProject({ projectName, templateConfig, packageManager: templateConfig.packageManager || config.settings.defaultPackageManager, cacheStrategy: templateConfig.cacheStrategy || config.settings.cacheStrategy || "daily", }); scaffoldSpinner.succeed(logger.colors.green(t("messages.success.new_project", { projectName }))); } catch (error) { handleErrorAndExit(error, scaffoldSpinner); } }); } const SCHEMA_PATH = "./node_modules/scaffolder-toolkit/devkit-schema.json"; async function saveConfig$2(config, filePath) { try { await fs.writeJson(filePath, { $schema: SCHEMA_PATH, ...config, }); } catch (error) { throw new DevkitError(t("errors.config.save_fail", { file: filePath }), { cause: error, }); } } async function saveCliConfig(config, isGlobal = false) { const filePath = await getConfigFilepath(isGlobal); await saveConfig$2(config, filePath); } async function saveGlobalConfig(config) { const targetPath = await getConfigFilepath(true); await saveConfig$2(config, targetPath); } async function saveLocalConfig(config) { const targetPath = await getConfigFilepath(); await saveConfig$2(config, targetPath); } async function execute(command, args, options) { return execa(command, args, options); } async function executeCommand(commandString, options) { return execaCommand(commandString, options); } const isAbsolutePath = (p) => { if (path.isAbsolute(p)) { return true; } if (/^[a-zA-Z]:\\/.test(p)) { return true; } if (/^\\\\[^\\\\]+\\[^\\\\]+/.test(p)) { return true; } return false; }; const normalizePath = (templatePath) => { if (templatePath.startsWith("file://")) { const url = new URL(templatePath); return decodeURI(url.pathname); } if (!isAbsolutePath(templatePath)) { return path.join(process.cwd(), templatePath); } return templatePath; }; const checkGitHubRepoExists = async (url) => { try { const { exitCode } = await execute("git", ["ls-remote", url, "HEAD"], { reject: false, }); return exitCode === 0; // oxlint-disable-next-line no-unused-vars } catch (error) { return false; } }; const isFromGitHub = (location) => { return (["https://github.com/", "git@github.com"].some((url) => location.startsWith(url)) && location.endsWith(".git")); }; const validateLocation = async (location, spinner) => { const isGithubUrl = isFromGitHub(location); if (isGithubUrl) { spinner?.start(t("messages.status.template_adding")); const isValid = await checkGitHubRepoExists(location); if (!isValid) { spinner?.fail(); handleErrorAndExit(new DevkitError(t("errors.validation.github_repo", { url: location })), spinner); return; } spinner?.succeed(); } else { const filePath = normalizePath(location); if (!fs.existsSync(filePath)) { handleErrorAndExit(new DevkitError(t("errors.validation.local_path", { path: filePath })), spinner); } } }; function validateAlias(alias) { if (!alias.trim()) { throw new DevkitError(t("errors.validation.alias_empty")); } if (alias.trim().length < 2) { throw new DevkitError(t("errors.validation.alias_too_short")); } } function validateDescription(description) { if (!description.trim()) { throw new DevkitError(t("errors.validation.description_empty")); } const wordCount = description.replaceAll(" ", "").length; if (wordCount < 10) { throw new DevkitError(t("errors.validation.description_too_short")); } } async function validateAndSaveTemplate(templateDetails, targetConfig, isGlobal, addSpinner) { const { description, alias, cacheStrategy, packageManager, language, templateName, location, } = templateDetails; validateProgrammingLanguage(language); let languageConfig = targetConfig.templates[language]; if (!languageConfig) { targetConfig.templates[language] = { templates: {} }; languageConfig = targetConfig.templates[language]; } await validateLocation(location, addSpinner); validateDescription(description); if (packageManager) { validatePackageManager(packageManager); } if (cacheStrategy) { validateCacheStrategy(cacheStrategy); } if (languageConfig?.templates[templateName]) { throw new DevkitError(t("errors.template.exists", { template: templateName })); } if (alias) { validateAlias(alias); const aliasExists = Object.values(languageConfig?.templates).some((t) => t.alias === alias); if (aliasExists) { throw new DevkitError(t("errors.validation.alias_exists", { alias: alias })); } } const newTemplate = { description: description, location: location, alias: alias, cacheStrategy: cacheStrategy, packageManager: packageManager, }; languageConfig.templates[templateName] = newTemplate; await saveCliConfig(targetConfig, isGlobal); addSpinner.succeed(logger.colors.green(t("messages.success.template_added", { templateName }))); } async function getTargetConfigForModification$1(isGlobal) { const sources = await readConfigSources({ forceGlobal: isGlobal, forceLocal: !isGlobal, }); const targetConfig = isGlobal ? sources.global : sources.local; if (targetConfig) { return targetConfig; } return sources.default; } function setupAddCommand(configCommand) { configCommand .command("add <language> <templateName>") .alias("a") .description(generateDynamicHelpText("supportedLanguage", "commands.template.add.description")) .option("-d, --description <string>", t("commands.template.add.options.description"), "") .option("-o, --location <string>", t("commands.template.add.prompts.location"), "") .option("-a, --alias <string>", t("commands.template.add.options.alias"), "") .option("-c, --cache-strategy <string>", generateDynamicHelpText("cacheStrategy", "commands.template.add.options.cache"), "") .option("-p, --package-manager <string>", generateDynamicHelpText("packageManager", "commands.template.add.options.package_manager"), "") .action(async (language, templateName, cmdOptions, childCommand) => { const { description, location, alias, cacheStrategy, packageManager } = cmdOptions; const parentOpts = childCommand?.parent?.opts(); const isGlobal = !!parentOpts?.global; const spinner = logger .spinner(logger.colors.cyan(t("messages.status.template_adding", { templateName }))) .start(); try { if (!description || !location) { throw new DevkitError(t("errors.command.missing_required_options", { fields: "--description, --location", })); } language = mapLanguageAliasToCanonicalKey(language); validateProgrammingLanguage(language); const config = await getTargetConfigForModification$1(isGlobal); const templateDetails = { language, templateName, description, location, alias: alias ? alias : undefined, cacheStrategy: cacheStrategy ? cacheStrategy : undefined, packageManager: packageManager ? packageManager : undefined, }; await validateAndSaveTemplate(templateDetails, config, !!isGlobal, spinner); } catch (error) { handleErrorAndExit(error, spinner); } }); } async function saveConfig$1(targetConfig, isGlobal) { if (isGlobal) { await saveGlobalConfig(targetConfig); } else { await saveLocalConfig(targetConfig); } } async function getTargetConfigForModification(isGlobal) { const sources = await readConfigSources({ forceGlobal: isGlobal, forceLocal: !isGlobal, }); const targetConfig = isGlobal ? sources.global : sources.local; if (!targetConfig) { if (isGlobal) { throw new DevkitError(t("errors.config.global_not_found")); } else { throw new DevkitError(t("errors.config.local_not_found")); } } return targetConfig; } function resolveTemplateNames(templateNames, templatesMap) { const actualTemplateNames = Object.values(templatesMap); const uniqueActualTemplateNames = [...new Set(actualTemplateNames)]; if (templateNames.includes("*")) { return { templatesToActOn: uniqueActualTemplateNames, notFound: templateNames.length > 1 ? templateNames.filter((name) => name !== "*") : [], }; } const templatesToActOn = []; const notFound = []; for (const name of templateNames) { const actualName = templatesMap[name]; if (actualName) { if (!templatesToActOn.includes(actualName)) { templatesToActOn.push(actualName); } } else { notFound.push(name); } } return { templatesToActOn, notFound }; } async function getTemplateNamesToActOn(language, templateNames, isGlobal) { validateProgrammingLanguage(language); const targetConfig = await getTargetConfigForModification(isGlobal); const languageTemplates = targetConfig?.templates?.[language]?.templates; if (!languageTemplates) { throw new DevkitError(t("errors.template.language_not_found", { language: language })); } const templatesMap = Object.entries(languageTemplates).reduce((acc, [name, template]) => { acc[name] = name; if (template?.alias) { acc[template.alias] = name; } return acc; }, {}); const { templatesToActOn, notFound } = resolveTemplateNames(templateNames, templatesMap); return { targetConfig, languageTemplates, templatesToActOn, notFound }; } function setupRemoveCommand(configCommand) { configCommand .command("remove <language> <templateName...>") .alias("rm") .description(generateDynamicHelpText("supportedLanguage", "commands.template.remove.command.description")) .action(async (language, templateNames, _, childCommand) => { const parentOpts = childCommand?.parent?.opts(); const isGlobal = !!parentOpts?.global; const spinner = logger .spinner() .start(logger.colors.cyan(t("messages.status.template_removing"))); try { if (templateNames.length === 0) { throw new DevkitError(t("errors.validation.template_name_required")); } language = mapLanguageAliasToCanonicalKey(language); const { targetConfig, languageTemplates, templatesToActOn, notFound, } = await getTemplateNamesToActOn(language, templateNames, isGlobal); if (templatesToActOn.length === 0) { throw new DevkitError(t("errors.template.not_found", { template: notFound.join(", "), })); } const templatesToKeep = Object.fromEntries(Object.entries(languageTemplates).filter(([key]) => !templatesToActOn.includes(key))); targetConfig.templates[language].templates = templatesToKeep; await saveConfig$1(targetConfig, !!isGlobal); spinner.succeed(t("messages.success.template_removed", { count: templatesToActOn.length.toString(), templateName: templatesToActOn.join(", "), language, })); if (notFound.length > 0) { logger.warning(logger.colors.yellow(t("warnings.template.list_not_found", { templates: notFound.join(", "),