UNPKG

@settlemint/sdk-utils

Version:

Shared utilities and helper functions for SettleMint SDK modules

1,005 lines (980 loc) 31.6 kB
import { dirname, join } from "node:path"; import { greenBright, inverse, magentaBright, redBright, whiteBright, yellowBright } from "yoctocolors"; import { spawn } from "node:child_process"; import isInCi from "is-in-ci"; import yoctoSpinner from "yocto-spinner"; import { Table } from "console-table-printer"; import { ZodError, z } from "zod"; import { config } from "@dotenvx/dotenvx"; import { readFile, stat, writeFile } from "node:fs/promises"; import { findUp } from "find-up"; import { glob } from "glob"; import { deepmerge } from "deepmerge-ts"; //#region src/terminal/should-print.ts /** * Returns true if the terminal should print, false otherwise. * @returns true if the terminal should print, false otherwise. */ function shouldPrint() { return process.env.SETTLEMINT_DISABLE_TERMINAL !== "true"; } //#endregion //#region src/terminal/ascii.ts /** * Prints the SettleMint ASCII art logo to the console in magenta color. * Used for CLI branding and visual identification. * * @example * import { ascii } from "@settlemint/sdk-utils/terminal"; * * // Prints the SettleMint logo * ascii(); */ const ascii = () => { if (!shouldPrint()) { return; } console.log(magentaBright(` _________ __ __ .__ _____ .__ __ / _____/ _____/ |__/ |_| | ____ / \\ |__| _____/ |_ \\_____ \\_/ __ \\ __\\ __\\ | _/ __ \\ / \\ / \\| |/ \\ __\\ / \\ ___/| | | | | |_\\ ___// Y \\ | | \\ | /_________/\\_____>__| |__| |____/\\_____>____|____/__|___|__/__| `)); }; //#endregion //#region src/logging/mask-tokens.ts /** * Masks sensitive SettleMint tokens in output text by replacing them with asterisks. * Handles personal access tokens (PAT), application access tokens (AAT), and service account tokens (SAT). * * @param output - The text string that may contain sensitive tokens * @returns The text with any sensitive tokens masked with asterisks * @example * import { maskTokens } from "@settlemint/sdk-utils/terminal"; * * // Masks a token in text * const masked = maskTokens("Token: sm_pat_****"); // "Token: ***" */ const maskTokens = (output) => { return output.replace(/sm_(pat|aat|sat)_[0-9a-zA-Z]+/g, "***"); }; //#endregion //#region src/terminal/cancel.ts /** * Error class used to indicate that the operation was cancelled. * This error is used to signal that the operation should be aborted. */ var CancelError = class extends Error {}; /** * Displays an error message in red inverse text and throws a CancelError. * Used to terminate execution with a visible error message. * Any sensitive tokens in the message are masked before display. * * @param msg - The error message to display * @returns never - Function does not return as it throws an error * @example * import { cancel } from "@settlemint/sdk-utils/terminal"; * * // Exits process with error message * cancel("An error occurred"); */ const cancel = (msg) => { console.log(""); console.log(inverse(redBright(maskTokens(msg)))); console.log(""); throw new CancelError(msg); }; //#endregion //#region src/terminal/execute-command.ts /** * Error class for command execution errors * @extends Error */ var CommandError = class extends Error { /** * Constructs a new CommandError * @param message - The error message * @param code - The exit code of the command * @param output - The output of the command */ constructor(message, code, output) { super(message); this.code = code; this.output = output; } }; /** * Executes a command with the given arguments in a child process. * Pipes stdin to the child process and captures stdout/stderr output. * Masks any sensitive tokens in the output before displaying or returning. * * @param command - The command to execute * @param args - Array of arguments to pass to the command * @param options - Options for customizing command execution * @returns Array of output strings from stdout and stderr * @throws {CommandError} If the process fails to start or exits with non-zero code * @example * import { executeCommand } from "@settlemint/sdk-utils/terminal"; * * // Execute git clone * await executeCommand("git", ["clone", "repo-url"]); * * // Execute silently * await executeCommand("npm", ["install"], { silent: true }); */ async function executeCommand(command, args, options) { const { silent,...spawnOptions } = options ?? {}; const child = spawn(command, args, { ...spawnOptions, env: { ...process.env, ...options?.env } }); process.stdin.pipe(child.stdin); const output = []; return new Promise((resolve, reject) => { child.stdout.on("data", (data) => { const maskedData = maskTokens(data.toString()); if (!silent) { process.stdout.write(maskedData); } output.push(maskedData); }); child.stderr.on("data", (data) => { const maskedData = maskTokens(data.toString()); if (!silent) { process.stderr.write(maskedData); } output.push(maskedData); }); child.on("error", (err) => { process.stdin.unpipe(child.stdin); reject(new CommandError(err.message, "code" in err && typeof err.code === "number" ? err.code : 1, output)); }); child.on("close", (code) => { process.stdin.unpipe(child.stdin); if (code === 0 || code === null || code === 143) { resolve(output); return; } reject(new CommandError(`Command "${command}" exited with code ${code}`, code, output)); }); }); } //#endregion //#region src/terminal/intro.ts /** * Displays an introductory message in magenta text with padding. * Any sensitive tokens in the message are masked before display. * * @param msg - The message to display as introduction * @example * import { intro } from "@settlemint/sdk-utils/terminal"; * * // Display intro message * intro("Starting deployment..."); */ const intro = (msg) => { if (!shouldPrint()) { return; } console.log(""); console.log(magentaBright(maskTokens(msg))); console.log(""); }; //#endregion //#region src/terminal/note.ts /** * Displays a note message with optional warning level formatting. * Regular notes are displayed in normal text, while warnings are shown in yellow. * Any sensitive tokens in the message are masked before display. * * @param message - The message to display as a note * @param level - The note level: "info" (default) or "warn" for warning styling * @example * import { note } from "@settlemint/sdk-utils/terminal"; * * // Display info note * note("Operation completed successfully"); * * // Display warning note * note("Low disk space remaining", "warn"); */ const note = (message, level = "info") => { if (!shouldPrint()) { return; } const maskedMessage = maskTokens(message); console.log(""); if (level === "warn") { console.warn(yellowBright(maskedMessage)); return; } console.log(maskedMessage); }; //#endregion //#region src/terminal/list.ts /** * Displays a list of items in a formatted manner, supporting nested items. * * @param title - The title of the list * @param items - The items to display, can be strings or arrays for nested items * @returns The formatted list * @example * import { list } from "@settlemint/sdk-utils/terminal"; * * // Simple list * list("Use cases", ["use case 1", "use case 2", "use case 3"]); * * // Nested list * list("Providers", [ * "AWS", * ["us-east-1", "eu-west-1"], * "Azure", * ["eastus", "westeurope"] * ]); */ function list(title, items) { const formatItems = (items$1) => { return items$1.map((item) => { if (Array.isArray(item)) { return item.map((subItem) => ` • ${subItem}`).join("\n"); } return ` • ${item}`; }).join("\n"); }; return note(`${title}:\n\n${formatItems(items)}`); } //#endregion //#region src/terminal/outro.ts /** * Displays a closing message in green inverted text with padding. * Any sensitive tokens in the message are masked before display. * * @param msg - The message to display as conclusion * @example * import { outro } from "@settlemint/sdk-utils/terminal"; * * // Display outro message * outro("Deployment completed successfully!"); */ const outro = (msg) => { if (!shouldPrint()) { return; } console.log(""); console.log(inverse(greenBright(maskTokens(msg)))); console.log(""); }; //#endregion //#region src/terminal/spinner.ts /** * Error class used to indicate that the spinner operation failed. * This error is used to signal that the operation should be aborted. */ var SpinnerError = class extends Error { constructor(message, originalError) { super(message); this.originalError = originalError; this.name = "SpinnerError"; } }; /** * Displays a loading spinner while executing an async task. * Shows progress with start/stop messages and handles errors. * Spinner is disabled in CI environments. * * @param options - Configuration options for the spinner * @returns The result from the executed task * @throws Will exit process with code 1 if task fails * @example * import { spinner } from "@settlemint/sdk-utils/terminal"; * * // Show spinner during async task * const result = await spinner({ * startMessage: "Deploying...", * task: async () => { * // Async work here * return "success"; * }, * stopMessage: "Deployed successfully!" * }); */ const spinner = async (options) => { const handleError = (error) => { const errorMessage = maskTokens(error.message); note(redBright(`${errorMessage}\n\n${error.stack}`)); throw new SpinnerError(errorMessage, error); }; if (isInCi || !shouldPrint()) { try { return await options.task(); } catch (err) { return handleError(err); } } const spinner$1 = yoctoSpinner({ stream: process.stdout }).start(options.startMessage); try { const result = await options.task(spinner$1); spinner$1.success(options.stopMessage); await new Promise((resolve) => process.nextTick(resolve)); return result; } catch (err) { spinner$1.error(redBright(`${options.startMessage} --> Error!`)); return handleError(err); } }; //#endregion //#region src/string.ts /** * Capitalizes the first letter of a string. * * @param val - The string to capitalize * @returns The input string with its first letter capitalized * * @example * import { capitalizeFirstLetter } from "@settlemint/sdk-utils"; * * const capitalized = capitalizeFirstLetter("hello"); * // Returns: "Hello" */ function capitalizeFirstLetter(val) { return String(val).charAt(0).toUpperCase() + String(val).slice(1); } /** * Converts a camelCase string to a human-readable string. * * @param s - The camelCase string to convert * @returns The human-readable string * * @example * import { camelCaseToWords } from "@settlemint/sdk-utils"; * * const words = camelCaseToWords("camelCaseString"); * // Returns: "Camel Case String" */ function camelCaseToWords(s) { const result = s.replace(/([a-z])([A-Z])/g, "$1 $2"); const withSpaces = result.replace(/([A-Z])([a-z])/g, " $1$2"); const capitalized = capitalizeFirstLetter(withSpaces); return capitalized.replace(/\s+/g, " ").trim(); } /** * Replaces underscores and hyphens with spaces. * * @param s - The string to replace underscores and hyphens with spaces * @returns The input string with underscores and hyphens replaced with spaces * * @example * import { replaceUnderscoresAndHyphensWithSpaces } from "@settlemint/sdk-utils"; * * const result = replaceUnderscoresAndHyphensWithSpaces("Already_Spaced-Second"); * // Returns: "Already Spaced Second" */ function replaceUnderscoresAndHyphensWithSpaces(s) { return s.replace(/[-_]/g, " "); } /** * Truncates a string to a maximum length and appends "..." if it is longer. * * @param value - The string to truncate * @param maxLength - The maximum length of the string * @returns The truncated string or the original string if it is shorter than the maximum length * * @example * import { truncate } from "@settlemint/sdk-utils"; * * const truncated = truncate("Hello, world!", 10); * // Returns: "Hello, wor..." */ function truncate(value, maxLength) { if (value.length <= maxLength) { return value; } return `${value.slice(0, maxLength)}...`; } //#endregion //#region src/terminal/table.ts /** * Displays data in a formatted table in the terminal. * * @param title - Title to display above the table * @param data - Array of objects to display in table format * @example * import { table } from "@settlemint/sdk-utils/terminal"; * * const data = [ * { name: "Item 1", value: 100 }, * { name: "Item 2", value: 200 } * ]; * * table("My Table", data); */ function table(title, data) { if (!shouldPrint()) { return; } note(title); if (!data || data.length === 0) { note("No data to display"); return; } const columnKeys = Object.keys(data[0]); const table$1 = new Table({ columns: columnKeys.map((key) => ({ name: key, title: whiteBright(camelCaseToWords(key)), alignment: "left" })) }); table$1.addRows(data); table$1.printTable(); } //#endregion //#region src/validation/validate.ts /** * Validates a value against a given Zod schema. * * @param schema - The Zod schema to validate against. * @param value - The value to validate. * @returns The validated and parsed value. * @throws Will throw an error if validation fails, with formatted error messages. * * @example * import { validate } from "@settlemint/sdk-utils/validation"; * * const validatedId = validate(IdSchema, "550e8400-e29b-41d4-a716-446655440000"); */ function validate(schema, value) { try { return schema.parse(value); } catch (error) { if (error instanceof ZodError) { const formattedErrors = error.issues.map((err) => `- ${err.path.join(".")}: ${err.message}`).join("\n"); throw new Error(`Validation error${error.issues.length > 1 ? "s" : ""}:\n${formattedErrors}`); } throw error; } } //#endregion //#region src/validation/access-token.schema.ts /** * Schema for validating application access tokens. * Application access tokens start with 'sm_aat_' prefix. */ const ApplicationAccessTokenSchema = z.string().regex(/^sm_aat_.+$/); /** * Schema for validating personal access tokens. * Personal access tokens start with 'sm_pat_' prefix. */ const PersonalAccessTokenSchema = z.string().regex(/^sm_pat_.+$/); /** * Schema for validating both application and personal access tokens. * Accepts tokens starting with either 'sm_pat_' or 'sm_aat_' prefix. */ const AccessTokenSchema = z.string().regex(/^(sm_pat_.+|sm_aat_.+)$/); //#endregion //#region src/json.ts /** * Attempts to parse a JSON string into a typed value, returning a default value if parsing fails. * * @param value - The JSON string to parse * @param defaultValue - The value to return if parsing fails or results in null/undefined * @returns The parsed JSON value as type T, or the default value if parsing fails * * @example * import { tryParseJson } from "@settlemint/sdk-utils"; * * const config = tryParseJson<{ port: number }>( * '{"port": 3000}', * { port: 8080 } * ); * // Returns: { port: 3000 } * * const invalid = tryParseJson<string[]>( * 'invalid json', * [] * ); * // Returns: [] */ function tryParseJson(value, defaultValue = null) { try { const parsed = JSON.parse(value); if (parsed === undefined || parsed === null) { return defaultValue; } return parsed; } catch (_err) { return defaultValue; } } /** * Extracts a JSON object from a string. * * @param value - The string to extract the JSON object from * @returns The parsed JSON object, or null if no JSON object is found * @throws {Error} If the input string is too long (longer than 5000 characters) * @example * import { extractJsonObject } from "@settlemint/sdk-utils"; * * const json = extractJsonObject<{ port: number }>( * 'port info: {"port": 3000}', * ); * // Returns: { port: 3000 } */ function extractJsonObject(value) { if (value.length > 5e3) { throw new Error("Input too long"); } const result = /\{([\s\S]*)\}/.exec(value); if (!result) { return null; } return tryParseJson(result[0]); } /** * Converts a value to a JSON stringifiable format. * * @param value - The value to convert * @returns The JSON stringifiable value * * @example * import { makeJsonStringifiable } from "@settlemint/sdk-utils"; * * const json = makeJsonStringifiable<{ amount: bigint }>({ amount: BigInt(1000) }); * // Returns: '{"amount":"1000"}' */ function makeJsonStringifiable(value) { if (value === undefined || value === null) { return value; } return tryParseJson(JSON.stringify(value, (_, value$1) => typeof value$1 === "bigint" ? value$1.toString() : value$1)); } //#endregion //#region src/validation/unique-name.schema.ts /** * Schema for validating unique names used across the SettleMint platform. * Only accepts lowercase alphanumeric characters and hyphens. * Used for workspace names, application names, service names etc. * * @example * import { UniqueNameSchema } from "@settlemint/sdk-utils/validation"; * * // Validate a workspace name * const isValidName = UniqueNameSchema.safeParse("my-workspace-123").success; * // true * * // Invalid names will fail validation * const isInvalidName = UniqueNameSchema.safeParse("My Workspace!").success; * // false */ const UniqueNameSchema = z.string().regex(/^[a-z0-9-]+$/); //#endregion //#region src/validation/url.schema.ts /** * Schema for validating URLs. * * @example * import { UrlSchema } from "@settlemint/sdk-utils/validation"; * * // Validate a URL * const isValidUrl = UrlSchema.safeParse("https://console.settlemint.com").success; * // true * * // Invalid URLs will fail validation * const isInvalidUrl = UrlSchema.safeParse("not-a-url").success; * // false */ const UrlSchema = z.string().url(); /** * Schema for validating URL paths. * * @example * import { UrlPathSchema } from "@settlemint/sdk-utils/validation"; * * // Validate a URL path * const isValidPath = UrlPathSchema.safeParse("/api/v1/users").success; * // true * * // Invalid paths will fail validation * const isInvalidPath = UrlPathSchema.safeParse("not-a-path").success; * // false */ const UrlPathSchema = z.string().regex(/^\/(?:[a-zA-Z0-9-_]+(?:\/[a-zA-Z0-9-_]+)*\/?)?$/, { message: "Invalid URL path format. Must start with '/' and can contain letters, numbers, hyphens, and underscores." }); /** * Schema that accepts either a full URL or a URL path. * * @example * import { UrlOrPathSchema } from "@settlemint/sdk-utils/validation"; * * // Validate a URL * const isValidUrl = UrlOrPathSchema.safeParse("https://console.settlemint.com").success; * // true * * // Validate a path * const isValidPath = UrlOrPathSchema.safeParse("/api/v1/users").success; * // true */ const UrlOrPathSchema = z.union([UrlSchema, UrlPathSchema]); //#endregion //#region src/validation/dot-env.schema.ts /** * Use this value to indicate that the resources are not part of the SettleMint platform. */ const STANDALONE_INSTANCE = "standalone"; /** * Use this value to indicate that the resources are not part of the SettleMint platform. */ const LOCAL_INSTANCE = "local"; /** * Schema for validating environment variables used by the SettleMint SDK. * Defines validation rules and types for configuration values like URLs, * access tokens, workspace names, and service endpoints. */ const DotEnvSchema = z.object({ SETTLEMINT_INSTANCE: z.union([ UrlSchema, z.literal(STANDALONE_INSTANCE), z.literal(LOCAL_INSTANCE) ]).default("https://console.settlemint.com"), SETTLEMINT_ACCESS_TOKEN: ApplicationAccessTokenSchema.optional(), SETTLEMINT_PERSONAL_ACCESS_TOKEN: PersonalAccessTokenSchema.optional(), SETTLEMINT_WORKSPACE: UniqueNameSchema.optional(), SETTLEMINT_APPLICATION: UniqueNameSchema.optional(), SETTLEMINT_BLOCKCHAIN_NETWORK: UniqueNameSchema.optional(), SETTLEMINT_BLOCKCHAIN_NETWORK_CHAIN_ID: z.string().optional(), SETTLEMINT_BLOCKCHAIN_NODE: UniqueNameSchema.optional(), SETTLEMINT_BLOCKCHAIN_NODE_JSON_RPC_ENDPOINT: UrlSchema.optional(), SETTLEMINT_BLOCKCHAIN_NODE_OR_LOAD_BALANCER: UniqueNameSchema.optional(), SETTLEMINT_BLOCKCHAIN_NODE_OR_LOAD_BALANCER_JSON_RPC_ENDPOINT: UrlSchema.optional(), SETTLEMINT_HASURA: UniqueNameSchema.optional(), SETTLEMINT_HASURA_ENDPOINT: UrlSchema.optional(), SETTLEMINT_HASURA_ADMIN_SECRET: z.string().optional(), SETTLEMINT_HASURA_DATABASE_URL: z.string().optional(), SETTLEMINT_THEGRAPH: UniqueNameSchema.optional(), SETTLEMINT_THEGRAPH_SUBGRAPHS_ENDPOINTS: z.preprocess((value) => tryParseJson(value, []), z.array(UrlSchema).optional()), SETTLEMINT_THEGRAPH_DEFAULT_SUBGRAPH: z.string().optional(), SETTLEMINT_PORTAL: UniqueNameSchema.optional(), SETTLEMINT_PORTAL_GRAPHQL_ENDPOINT: UrlSchema.optional(), SETTLEMINT_PORTAL_REST_ENDPOINT: UrlSchema.optional(), SETTLEMINT_PORTAL_WS_ENDPOINT: UrlSchema.optional(), SETTLEMINT_HD_PRIVATE_KEY: UniqueNameSchema.optional(), SETTLEMINT_HD_PRIVATE_KEY_FORWARDER_ADDRESS: z.string().optional(), SETTLEMINT_ACCESSIBLE_PRIVATE_KEY: UniqueNameSchema.optional(), SETTLEMINT_MINIO: UniqueNameSchema.optional(), SETTLEMINT_MINIO_ENDPOINT: UrlSchema.optional(), SETTLEMINT_MINIO_ACCESS_KEY: z.string().optional(), SETTLEMINT_MINIO_SECRET_KEY: z.string().optional(), SETTLEMINT_IPFS: UniqueNameSchema.optional(), SETTLEMINT_IPFS_API_ENDPOINT: UrlSchema.optional(), SETTLEMINT_IPFS_PINNING_ENDPOINT: UrlSchema.optional(), SETTLEMINT_IPFS_GATEWAY_ENDPOINT: UrlSchema.optional(), SETTLEMINT_CUSTOM_DEPLOYMENT: UniqueNameSchema.optional(), SETTLEMINT_CUSTOM_DEPLOYMENT_ENDPOINT: UrlSchema.optional(), SETTLEMINT_BLOCKSCOUT: UniqueNameSchema.optional(), SETTLEMINT_BLOCKSCOUT_GRAPHQL_ENDPOINT: UrlSchema.optional(), SETTLEMINT_BLOCKSCOUT_UI_ENDPOINT: UrlSchema.optional(), SETTLEMINT_NEW_PROJECT_NAME: z.string().optional(), SETTLEMINT_LOG_LEVEL: z.enum([ "debug", "info", "warn", "error", "none" ]).default("warn") }); /** * Partial version of the environment variables schema where all fields are optional. * Useful for validating incomplete configurations during development or build time. */ const DotEnvSchemaPartial = DotEnvSchema.partial(); //#endregion //#region src/validation/id.schema.ts /** * Schema for validating database IDs. Accepts both PostgreSQL UUIDs and MongoDB ObjectIDs. * PostgreSQL UUIDs are 32 hexadecimal characters with hyphens (e.g. 123e4567-e89b-12d3-a456-426614174000). * MongoDB ObjectIDs are 24 hexadecimal characters (e.g. 507f1f77bcf86cd799439011). * * @example * import { IdSchema } from "@settlemint/sdk-utils/validation"; * * // Validate PostgreSQL UUID * const isValidUuid = IdSchema.safeParse("123e4567-e89b-12d3-a456-426614174000").success; * * // Validate MongoDB ObjectID * const isValidObjectId = IdSchema.safeParse("507f1f77bcf86cd799439011").success; */ const IdSchema = z.union([z.string().uuid(), z.string().regex(/^[0-9a-fA-F]{24}$/)]); //#endregion //#region src/environment/load-env.ts /** * Loads environment variables from .env files. * To enable encryption with dotenvx (https://www.dotenvx.com/docs) run `bunx dotenvx encrypt` * * @param validateEnv - Whether to validate the environment variables against the schema * @param prod - Whether to load production environment variables * @param path - Optional path to the directory containing .env files. Defaults to process.cwd() * @returns A promise that resolves to the validated environment variables * @throws Will throw an error if validation fails and validateEnv is true * @example * import { loadEnv } from '@settlemint/sdk-utils/environment'; * * // Load and validate environment variables * const env = await loadEnv(true, false); * console.log(env.SETTLEMINT_INSTANCE); * * // Load without validation * const rawEnv = await loadEnv(false, false); */ async function loadEnv(validateEnv, prod, path = process.cwd()) { return loadEnvironmentEnv(validateEnv, !!prod, path); } async function loadEnvironmentEnv(validateEnv, prod, path = process.cwd()) { if (prod) { process.env.NODE_ENV = "production"; } let { parsed } = config({ convention: "nextjs", logLevel: "error", overload: true, quiet: true, path: [join(path, ".env"), join(path, ".env.local")] }); if (!parsed) { parsed = {}; } const defaultEnv = Object.fromEntries(Object.entries(process.env).filter(([_, value]) => typeof value === "string" && value !== "")); try { return validate(validateEnv ? DotEnvSchema : DotEnvSchemaPartial, { ...parsed, ...defaultEnv }); } catch (error) { cancel(error.message); return {}; } } //#endregion //#region src/filesystem/project-root.ts /** * Finds the root directory of the current project by locating the nearest package.json file * * @param fallbackToCwd - If true, will return the current working directory if no package.json is found * @param cwd - The directory to start searching for the package.json file from (defaults to process.cwd()) * @returns Promise that resolves to the absolute path of the project root directory * @throws Will throw an error if no package.json is found in the directory tree * @example * import { projectRoot } from "@settlemint/sdk-utils/filesystem"; * * // Get project root path * const rootDir = await projectRoot(); * console.log(`Project root is at: ${rootDir}`); */ async function projectRoot(fallbackToCwd = false, cwd) { const packageJsonPath = await findUp("package.json", { cwd }); if (!packageJsonPath) { if (fallbackToCwd) { return process.cwd(); } throw new Error("Unable to find project root (no package.json found)"); } return dirname(packageJsonPath); } //#endregion //#region src/filesystem/exists.ts /** * Checks if a file or directory exists at the given path * * @param path - The file system path to check for existence * @returns Promise that resolves to true if the path exists, false otherwise * @example * import { exists } from "@settlemint/sdk-utils/filesystem"; * * // Check if file exists before reading * if (await exists('/path/to/file.txt')) { * // File exists, safe to read * } */ async function exists(path) { try { await stat(path); return true; } catch { return false; } } //#endregion //#region src/filesystem/mono-repo.ts /** * Finds the root directory of a monorepo * * @param startDir - The directory to start searching from * @returns The root directory of the monorepo or null if not found * @example * import { findMonoRepoRoot } from "@settlemint/sdk-utils/filesystem"; * * const root = await findMonoRepoRoot("/path/to/your/project"); * console.log(root); // Output: /path/to/your/project/packages/core */ async function findMonoRepoRoot(startDir) { const lockFilePath = await findUp([ "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb", "bun.lock" ], { cwd: startDir }); if (lockFilePath) { const packageJsonPath = join(dirname(lockFilePath), "package.json"); const hasWorkSpaces = await packageJsonHasWorkspaces(packageJsonPath); return hasWorkSpaces ? dirname(lockFilePath) : null; } let currentDir = startDir; while (currentDir !== "/") { const packageJsonPath = join(currentDir, "package.json"); if (await packageJsonHasWorkspaces(packageJsonPath)) { return currentDir; } const parentDir = dirname(currentDir); if (parentDir === currentDir) { break; } currentDir = parentDir; } return null; } /** * Finds all packages in a monorepo * * @param projectDir - The directory to start searching from * @returns An array of package directories * @example * import { findMonoRepoPackages } from "@settlemint/sdk-utils/filesystem"; * * const packages = await findMonoRepoPackages("/path/to/your/project"); * console.log(packages); // Output: ["/path/to/your/project/packages/core", "/path/to/your/project/packages/ui"] */ async function findMonoRepoPackages(projectDir) { try { const monoRepoRoot = await findMonoRepoRoot(projectDir); if (!monoRepoRoot) { return [projectDir]; } const packageJsonPath = join(monoRepoRoot, "package.json"); const packageJson = tryParseJson(await readFile(packageJsonPath, "utf-8")); const workspaces = packageJson?.workspaces ?? []; const packagePaths = await Promise.all(workspaces.map(async (workspace) => { const matches = await glob(join(monoRepoRoot, workspace, "package.json")); return matches.map((match) => join(match, "..")); })); const allPaths = packagePaths.flat(); return allPaths.length === 0 ? [projectDir] : [monoRepoRoot, ...allPaths]; } catch (_error) { return [projectDir]; } } async function packageJsonHasWorkspaces(packageJsonPath) { if (await exists(packageJsonPath)) { const packageJson = tryParseJson(await readFile(packageJsonPath, "utf-8")); if (packageJson?.workspaces && Array.isArray(packageJson?.workspaces) && packageJson?.workspaces.length > 0) { return true; } } return false; } //#endregion //#region src/environment/write-env.ts /** * Writes environment variables to .env files across a project or monorepo * * @param options - The options for writing the environment variables * @param options.prod - Whether to write production environment variables * @param options.env - The environment variables to write * @param options.secrets - Whether to write to .env.local files for secrets * @param options.cwd - The directory to start searching for the package.json file from (defaults to process.cwd()) * @returns Promise that resolves when writing is complete * @throws Will throw an error if writing fails * @example * import { writeEnv } from '@settlemint/sdk-utils/environment'; * * // Write development environment variables * await writeEnv({ * prod: false, * env: { * SETTLEMINT_INSTANCE: 'https://dev.example.com' * }, * secrets: false * }); * * // Write production secrets * await writeEnv({ * prod: true, * env: { * SETTLEMINT_ACCESS_TOKEN: 'secret-token' * }, * secrets: true * }); */ async function writeEnv({ prod, env, secrets, cwd }) { const projectDir = await projectRoot(true, cwd); if (prod) { process.env.NODE_ENV = "production"; } const targetDirs = await findMonoRepoPackages(projectDir); await Promise.all(targetDirs.map(async (dir) => { const envFile = join(dir, secrets ? `.env${prod ? ".production" : ""}.local` : `.env${prod ? ".production" : ""}`); let { parsed: currentEnv } = await exists(envFile) ? config({ path: envFile, logLevel: "error", quiet: true }) : { parsed: {} }; if (!currentEnv) { currentEnv = {}; } const prunedEnv = pruneCurrentEnv(currentEnv, env); const mergedEnv = deepmerge(prunedEnv, env); await writeFile(envFile, stringify(mergedEnv)); })); } const quote = /[\s"'#]/; function stringifyPair([key, val]) { if (val === undefined) { return undefined; } if (val === null) { return `${key}=""`; } const type = typeof val; if (type === "string") { return `${key}=${quote.test(val) ? JSON.stringify(val) : val}`; } if (type === "boolean" || type === "number") { return `${key}=${val}`; } if (type === "object") { return `${key}=${JSON.stringify(val)}`; } throw new Error(`Unsupported type for key "${key}": ${type}`); } function stringify(obj) { return Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)).map(stringifyPair).filter((value) => value !== undefined).join("\n"); } /** * Prunes the current environment variables from the new environment variables * * @param currentEnv - The current environment variables * @param env - The new environment variables * @returns The new environment variables with the current environment variables removed */ function pruneCurrentEnv(currentEnv, env) { const dotEnvKeys = Object.keys(DotEnvSchema.shape); return Object.fromEntries(Object.entries(currentEnv).filter(([key]) => { if (dotEnvKeys.includes(key) && !env[key]) { return false; } return true; })); } //#endregion export { loadEnv, writeEnv }; //# sourceMappingURL=environment.js.map