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
JavaScript
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(", "),