From 9cf35e92c4b15967199d6fc1cfbf133ced311db7 Mon Sep 17 00:00:00 2001 From: Hugo Richard Date: Sat, 4 Jan 2025 15:02:39 +0100 Subject: [PATCH] feat: Improve monorepo workflow (#396) --- .github/workflows/stale.yml | 23 +++ bun.lockb | Bin 677680 -> 677704 bytes packages/cli/.env.example | 13 +- packages/cli/README.md | 37 ++-- packages/cli/package.json | 3 +- packages/cli/shelve.json | 6 + packages/cli/src/services/file.ts | 19 ++ packages/cli/src/utils/config.ts | 277 ++++++++++++++++++++++-------- packages/types/schema.json | 15 +- packages/types/shelve.json | 6 + packages/types/src/Cli.ts | 19 ++ shelve.json | 4 + 12 files changed, 312 insertions(+), 110 deletions(-) create mode 100644 .github/workflows/stale.yml create mode 100644 packages/cli/shelve.json create mode 100644 packages/types/shelve.json create mode 100644 shelve.json diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..637ec4ee --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,23 @@ +name: stale + +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v9 + with: + exempt-issue-labels: triage,v3 + stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity.' + stale-issue-label: stale + stale-pr-label: stale + days-before-stale: 30 + days-before-close: -1 diff --git a/bun.lockb b/bun.lockb index fc70fecb79519b5f11edff45816f83cc2ef52eed..d9ce53a7fd63355a677eaa6d06b3c6564592a5e6 100755 GIT binary patch delta 33061 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 Gm;Mic8s?h- diff --git a/packages/cli/.env.example b/packages/cli/.env.example index f84d26e1..2b48df72 100644 --- a/packages/cli/.env.example +++ b/packages/cli/.env.example @@ -1,13 +1,2 @@ # Generated by Shelve CLI -NUXT_PRIVATE_RESEND_API_KEY=your_value -NUXT_PRIVATE_ENCRYPTION_KEY=your_value -NUXT_PRIVATE_ADMIN_EMAILS=your_value -TURBO_TOKEN=your_value -NUXT_OAUTH_GOOGLE_CLIENT_SECRET=your_value -NUXT_OAUTH_GITHUB_CLIENT_SECRET=your_value -NUXT_OAUTH_GOOGLE_CLIENT_ID=your_value -NUXT_SESSION_PASSWORD=your_value -DATABASE_URL=your_value -NUXT_PRIVATE_REDIS_URL=your_value -NUXT_OAUTH_GITHUB_CLIENT_ID=your_value -NUXT_PUBLIC_APP_URL=your_value +TEST=your_value \ No newline at end of file diff --git a/packages/cli/README.md b/packages/cli/README.md index 0fab1385..b5697a36 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,4 +1,4 @@ -Shelve +Shelve # Shelve CLI @@ -22,25 +22,24 @@ bun a -d @shelve/cli ## Configuration -Configuration is loaded by [unjs/c12](https://github.com/unjs/c12) from cwd. You can use either `shelve.config.json`, `shelve.config.{ts,js,mjs,cjs}`, but running the CLI without any configuration will create a `shelve.config.json` file. -You have the option to create a `shelve.config.ts` file to enable type checking and autocompletion. The file should contain the following content: - -```ts title="shelve.config.ts" -import { createShelveConfig } from "@shelve/cli" - -export default createShelveConfig({ - project: "my-project", - slug: "nuxtlabs", - token: "my-token", - url: "https://app.shelve.cloud", - confirmChanges: false, - envFileName: '.env', - autoUppercase: true, -}) -``` +Configuration is loaded from cwd. You can use either `shelve.json`, `shelve.config.json` or `.shelverc.json`, but running the CLI without any configuration will create a `shelve.json` file. The CLI also has a json schema for the configuration file. that can be used to validate the configuration file. (see it [here](https://raw.githubusercontent.com/HugoRCD/shelve/main/packages/types/schema.json)) +### Configuration example + +```json +{ + "slug": "nuxtlabs", + "project": "@nuxt/ui", + "confirmChanges": true +} +``` + +### Monorepo configuration + +If you are using a monorepo, Shelve will automatically detect the root of the monorepo and look for the global `shelve.json` file. You can define here common configurations for all the projects in the monorepo (the team slug for example). + ## Usage ```bash @@ -65,6 +64,10 @@ Commands: help [command] display help for command ``` +### Monorepo usage + +If you are using a monorepo, running a command at the root level will execute the command for all the projects in the monorepo that have a `shelve.json` file. +
diff --git a/packages/cli/package.json b/packages/cli/package.json index 5d02b829..3eea620c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@shelve/cli", - "version": "2.12.0", + "version": "3.0.0", "description": "The command-line interface for Shelve", "homepage": "https://shelve.cloud", "bugs": { @@ -43,6 +43,7 @@ "@clack/prompts": "0.9.0", "c12": "2.0.1", "commander": "12.1.0", + "defu": "^6.1.4", "npm-registry-fetch": "18.0.2", "nypm": "0.4.1", "ofetch": "1.4.1", diff --git a/packages/cli/shelve.json b/packages/cli/shelve.json new file mode 100644 index 00000000..7c47cff4 --- /dev/null +++ b/packages/cli/shelve.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/HugoRCD/shelve/main/packages/types/schema.json", + "url": "http://localhost:3000", + "project": "@shelve/cli", + "slug": "shelve" +} diff --git a/packages/cli/src/services/file.ts b/packages/cli/src/services/file.ts index c7a272f7..29880b5b 100644 --- a/packages/cli/src/services/file.ts +++ b/packages/cli/src/services/file.ts @@ -1,4 +1,5 @@ import fs from 'fs' +import { findFile as pkgFindFile } from 'pkg-types' export class FileService { @@ -18,4 +19,22 @@ export class FileService { fs.unlinkSync(filename) } + static async findFile( + patterns: string | string[], + options: { startingFrom: string; stopOnFirst?: boolean } + ): Promise { + const patternArray = Array.isArray(patterns) ? patterns : [patterns] + + for (const pattern of patternArray) { + try { + const result = await pkgFindFile(pattern, options) + if (result && options.stopOnFirst) { + return result + } + } catch { /* empty */ } + } + + return null + } + } diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts index 476366c6..01024894 100644 --- a/packages/cli/src/utils/config.ts +++ b/packages/cli/src/utils/config.ts @@ -1,97 +1,234 @@ +/** + * @fileoverview Configuration management for Shelve CLI + * This file handles the creation, loading, and validation of Shelve configuration. + * + * Configuration Priority (from highest to lowest): + * 1. Environment variables (SHELVE_*) + * 2. Local shelve.json (in current directory) + * 3. Root shelve.json (in monorepo root) + * 4. User configuration (~/.shelve) + * 5. Default values + * + * The configuration is built progressively using defu, meaning each level can + * override the values from lower priority sources. This allows for flexible + * configuration management where: + * - Local config can override root config (in monorepos) + * - Environment variables can override any file-based config + * - Each project in a monorepo can have its own specific settings while sharing common settings + */ + +import { join } from 'path' import { intro, outro } from '@clack/prompts' -import { loadConfig, setupDotenv } from 'c12' -import { readPackageJSON } from 'pkg-types' +import { setupDotenv } from 'c12' +import { findWorkspaceDir, readPackageJSON } from 'pkg-types' +import type { ShelveConfig } from '@shelve/types' import { CreateShelveConfigInput, DEFAULT_URL, SHELVE_JSON_SCHEMA } from '@shelve/types' -import type { Project, ShelveConfig } from '@shelve/types' import { readUser } from 'rc9' -import { FileService, ProjectService } from '../services' +import defu from 'defu' +import { FileService, PkgService, ProjectService } from '../services' import { DEFAULT_ENV_FILENAME } from '../constants' import { BaseService } from '../services/base' import { askSelect, askText } from './prompt' import { handleCancel } from '.' -export async function createShelveConfig(input: CreateShelveConfigInput = {}): Promise { - intro(input.projectName ? `Create configuration for ${input.projectName}` : 'No configuration file found, create a new one') - - let project = input.projectName - let projects: Project[] = [] - - if (!input.slug) input.slug = await askText('Enter the team slug:', 'my-team-slug') - - if (!input.projectName) { - projects = await ProjectService.getProjects(input.slug) - - project = await askSelect('Select the current project:', projects.map(project => ({ - value: project.name, - label: project.name - }))) +export const CONFIG_FILENAMES = [ + 'shelve.json', + 'shelve.config.json', + '.shelverc.json', +] as const + +export type ConfigFileName = (typeof CONFIG_FILENAMES)[number] +export const CONFIG_FILENAMES_ARRAY: string[] = [...CONFIG_FILENAMES] + +/** + * Finds the first existing configuration file from the list of possible filenames + * + * @param directory - Directory to search in (defaults to current directory) + * @returns The path to the first found config file, or null if none exists + */ +function findConfigFile(directory: string = process.cwd()): string | null { + for (const filename of CONFIG_FILENAMES) { + const path = join(directory, filename) + if (FileService.exists(path)) { + return path + } } + return null +} - if (!project) handleCancel('Error: no project selected') +/** + * Creates a new configuration file with the preferred filename + * + * @param config - Configuration content to write + * @param preferredFilename - Preferred filename (defaults to first in CONFIG_FILENAMES) + * @returns The path to the created config file + */ +function createConfigFile(config: string, preferredFilename: ConfigFileName = CONFIG_FILENAMES[0]): string { + FileService.write(preferredFilename, config) + return preferredFilename +} - const configFile = JSON.stringify({ +/** + * Creates a Shelve configuration file (shelve.json) + * This function is called when no configuration exists or when explicitly requested + * + * @param input - Optional initialization parameters for the configuration + * @returns Promise - The created configuration object + * + * Flow: + * 1. Load default configuration from various sources + * 2. Prompt user for team slug if not provided + * 3. Prompt user to select a project if not specified + * 4. Create shelve.json file with the configuration + */ +export async function createShelveConfig(input: CreateShelveConfigInput = {}): Promise { + intro(input.projectName ? `Create configuration for ${ input.projectName }` : 'No configuration file found, create a new one') + + const defaultConfig = await getDefaultConfig() + + const slug = input.slug || defaultConfig.slug || await askText('Enter the team slug:', 'my-team-slug') + const projectName = input.projectName || defaultConfig.project || await selectProject(slug) + + if (!projectName) handleCancel('Error: no project selected') + + const config = JSON.stringify({ $schema: SHELVE_JSON_SCHEMA, - project: project.toLowerCase(), - slug: input.slug, + project: projectName.toLowerCase(), + slug, }, null, 2) - FileService.write('shelve.config.json', configFile) + createConfigFile(config) outro('Configuration file created successfully') - return project + + return defu({ project: projectName, slug }, defaultConfig) } -export async function loadShelveConfig(check: boolean = false): Promise { +/** + * Prompts user to select a project from available projects for the given team slug + * + * @param slug - The team slug to fetch projects for + * @returns Promise - The selected project name + */ +async function selectProject(slug: string): Promise { + const projects = await ProjectService.getProjects(slug) + return askSelect('Select the current project:', projects.map(({ name }) => ({ + value: name, + label: name, + }))) +} + +/** + * Reads and merges configuration from a specific path with default configuration + * + * @param path - Path to the configuration file + * @param isRoot - Whether this is the root configuration (for monorepos) + * @returns Promise - The merged configuration + */ +async function checkConfig(path: string | null, isRoot = false): Promise { + const defaultConfig = await getDefaultConfig() + const config = path ? JSON.parse(FileService.read(path)) : null + return isRoot ? config : defu(config, defaultConfig) +} + +/** + * Generates base configuration by gathering information from multiple sources. + * This serves as the foundation that other configuration sources will override. + * + * Sources (in order of application): + * 1. Default values + * 2. User configuration (~/.shelve) + * 3. Package.json information + * 4. Environment variables + * + * Note: This configuration can still be overridden by root and local shelve.json + * + * @returns Promise - The base configuration object + */ +async function getDefaultConfig(): Promise { // @ts-expect-error we don't want to specify 'cwd' option await setupDotenv({}) - const { name } = await readPackageJSON() - const conf = readUser('.shelve') + const workspaceDir = await findWorkspaceDir() + const pkgService = new PkgService() + const isMonoRepo = await pkgService.isMonorepo() + const allPkg = await pkgService.getAllPackageJsons() + + return { + // @ts-expect-error to provide error message we let project be undefined + project: process.env.SHELVE_PROJECT || name, + // @ts-expect-error to provide error message we let slug be undefined + slug: process.env.SHELVE_TEAM_SLUG, + token: conf.token, + url: process.env.SHELVE_URL || 'https://app.shelve.cloud', + username: conf.username, + email: conf.email, + confirmChanges: false, + envFileName: DEFAULT_ENV_FILENAME, + autoUppercase: true, + monorepo: isMonoRepo ? { paths: allPkg.map(pkg => pkg.dir) } : undefined, + workspaceDir, + isMonoRepo, + isRoot: workspaceDir === process.cwd(), + } +} - /*const workspaceDir = await findWorkspaceDir() - const isMonoRepo = await new PkgService().isMonorepo() - const rootLevel = workspaceDir === process.cwd()*/ - - const { config, configFile } = await loadConfig({ - name: 'shelve', - packageJson: true, - rcFile: false, - globalRc: false, - dotenv: true, - defaults: { - // @ts-expect-error to provide error message we let project be undefined - project: process.env.SHELVE_PROJECT || name, - // @ts-expect-error to provide error message we let slug be undefined - slug: process.env.SHELVE_TEAM_SLUG, - token: conf.token, - url: process.env.SHELVE_URL || 'https://app.shelve.cloud', - username: conf.username, - email: conf.email, - confirmChanges: false, - envFileName: DEFAULT_ENV_FILENAME, - autoUppercase: true, - }, - }) - - /*const shelveConf = await findFile('shelve.json') - console.log('shelveConf', shelveConf)*/ - - if (check) { - if (configFile === 'shelve.config') config.project = await createShelveConfig() - - if (!config.token) await BaseService.getToken() - - if (!config.slug) handleCancel('You need to provide your team slug') - - if (!config.project) handleCancel('Please provide a project name') - - const urlRegex = /^(http|https):\/\/[^ "]+$/ - if (config.url !== DEFAULT_URL && !urlRegex.test(config.url)) { - handleCancel('Please provide a valid url') - } +/** + * Main configuration loader function + * Handles the complete configuration loading process by progressively building + * the configuration object from multiple sources. + * + * Loading Process: + * 1. Start with default values + * 2. Merge with user config (~/.shelve) + * 3. If in monorepo, merge with root shelve.json + * 4. Merge with local shelve.json (if exists) + * 5. Apply environment variables (highest priority) + * + * This progressive merging ensures that more specific configurations + * (local, env vars) can override more general ones (root, defaults). + * + * @param check - Whether to validate and potentially create new configuration + * @returns Promise - The complete, merged configuration + */ +export async function loadShelveConfig(check = false): Promise { + const localConfigPath = findConfigFile() + let localConfig = localConfigPath ? await checkConfig(localConfigPath) : + check ? await createShelveConfig() : await getDefaultConfig() + + if (localConfig.isMonoRepo) { + const rootConfigPath = await FileService.findFile(CONFIG_FILENAMES_ARRAY, { + startingFrom: localConfig.workspaceDir, + stopOnFirst: true, + }).catch(() => null) + + const rootConfig = await checkConfig(rootConfigPath, true) + localConfig = defu(localConfig, rootConfig) } - return config + if (check) await validateConfig(localConfig) + + return localConfig +} + +/** + * Validates the configuration object + * Checks for required fields and format validity: + * - Authentication token + * - Team slug + * - Project name + * - URL format (if custom URL is provided) + * + * @param config - The configuration object to validate + * @throws Error - Will cancel the process if validation fails + */ +async function validateConfig(config: ShelveConfig): Promise { + if (!config.token) await BaseService.getToken() + if (!config.slug) handleCancel('You need to provide your team slug') + if (!config.project) handleCancel('Please provide a project name') + if (config.url !== DEFAULT_URL && !/^(http|https):\/\/[^ "]+$/.test(config.url)) { + handleCancel('Please provide a valid url') + } } diff --git a/packages/types/schema.json b/packages/types/schema.json index 4fd6ca37..1da17dab 100644 --- a/packages/types/schema.json +++ b/packages/types/schema.json @@ -5,24 +5,20 @@ "properties": { "project": { "type": "string", - "description": "The project name", - "default": "process.env.SHELVE_PROJECT || package_json_name" + "description": "The project name" }, "slug": { "type": "string", - "description": "The team slug", - "default": "process.env.SHELVE_TEAM_SLUG" + "description": "The team slug" }, "token": { "type": "string", - "description": "The token to authenticate with Shelve created using the app (https://shelve.cloud/app/tokens) or your own Shelve instance", - "default": "process.env.SHELVE_TOKEN" + "description": "The token to authenticate with Shelve created using the app (https://shelve.cloud/app/tokens) or your own Shelve instance" }, "url": { "type": "string", "description": "The URL of the Shelve instance can be overridden with the `SHELVE_URL` environment variable", - "default": "https://shelve.cloud", - "fallback": "process.env.SHELVE_URL" + "default": "https://shelve.cloud" }, "confirmChanges": { "type": "boolean", @@ -34,6 +30,5 @@ "description": "Name of your env file", "default": ".env" } - }, - "required": ["project", "slug"] + } } diff --git a/packages/types/shelve.json b/packages/types/shelve.json new file mode 100644 index 00000000..759d10dd --- /dev/null +++ b/packages/types/shelve.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/HugoRCD/shelve/main/packages/types/schema.json", + "url": "http://localhost:3000", + "project": "shelve", + "slug": "shelve" +} diff --git a/packages/types/src/Cli.ts b/packages/types/src/Cli.ts index 2e0881ae..9dc8648a 100644 --- a/packages/types/src/Cli.ts +++ b/packages/types/src/Cli.ts @@ -49,6 +49,24 @@ export type ShelveConfig = { * @default true * */ autoUppercase: boolean + /** + * The monorepo configuration + * */ + monorepo?: { + paths: string[] + } + /** + * The workspace directory + * */ + workspaceDir: string + /** + * Whether the project is a monorepo + * */ + isMonoRepo: boolean + /** + * Whether the project is at the root level + * */ + isRoot: boolean } export type CreateEnvFileInput = { @@ -106,6 +124,7 @@ export type GetEnvVariables = { environmentId: number slug: string } + export type CreateShelveConfigInput = { slug?: string projectName?: string diff --git a/shelve.json b/shelve.json new file mode 100644 index 00000000..a1a455e4 --- /dev/null +++ b/shelve.json @@ -0,0 +1,4 @@ +{ + "url": "http://localhost:3000", + "slug": "shelve" +}