From 43f889a810704449d41a38b3507be2c4ab8a54f4 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Tue, 17 Dec 2024 09:57:24 +0100 Subject: [PATCH] feat: move interfaces to constants --- src/commands/components/actions.ts | 173 ++++++++++++++++++--------- src/commands/components/constants.ts | 80 +++++++++++++ src/commands/components/index.ts | 50 +++++++- 3 files changed, 243 insertions(+), 60 deletions(-) diff --git a/src/commands/components/actions.ts b/src/commands/components/actions.ts index ed911df..83293f8 100644 --- a/src/commands/components/actions.ts +++ b/src/commands/components/actions.ts @@ -1,66 +1,11 @@ import { ofetch } from 'ofetch' import { handleAPIError, handleFileSystemError, slugify } from '../../utils' import { regionsDomain } from '../../constants' -import { join } from 'node:path' +import { join, parse } from 'node:path' import { resolvePath, saveToFile } from '../../utils/filesystem' -import type { PullComponentsOptions } from './constants' - -export interface SpaceComponent { - name: string - display_name: string - created_at: string - updated_at: string - id: number - schema: Record - image?: string - preview_field?: string - is_root?: boolean - is_nestable?: boolean - preview_tmpl?: string - all_presets?: Record - preset_id?: number - real_name?: string - component_group_uuid?: string - color: null - internal_tags_list: string[] - interntal_tags_ids: number[] - content_type_asset_preview?: string -} - -export interface SpaceComponentGroup { - name: string - id: number - uuid: string - parent_id: number - parent_uuid: string -} - -export interface ComponentsSaveOptions { - path?: string - filename?: string - separateFiles?: boolean - suffix?: string -} +import type { PullComponentsOptions, SpaceComponent, SpaceComponentGroup, SpaceComponentPreset, SpaceData } from './constants' +import { readdir, readFile } from 'node:fs/promises' -export interface SpaceComponentPreset { - id: number - name: string - preset: Record - component_id: number - space_id: number - created_at: string - updated_at: string - image: string - color: string - icon: string - description: string -} - -export interface SpaceData { - components: SpaceComponent[] - groups: SpaceComponentGroup[] - presets: SpaceComponentPreset[] -} /** * Resolves the nested folder structure based on component group hierarchy. * @param groupUuid - The UUID of the component group. @@ -167,3 +112,115 @@ export const saveComponentsToFiles = async ( handleFileSystemError('write', error as Error) } } + +export const readComponentsFiles = async (componentsPath: string, options: { filter?: string, separateFiles?: boolean }): Promise => { + const { filter, separateFiles } = options + const resolvedPath = resolvePath(componentsPath, 'components') + const regex = filter ? new RegExp(filter) : null + + const spaceData: SpaceData = { + components: [], + groups: [], + presets: [], + } + + try { + if (!separateFiles) { + // Read from consolidated files + const files = await readdir(resolvedPath, { recursive: !separateFiles }) + + // Add regex patterns to match file structures + const componentsPattern = /^components\..+\.json$/ + const groupsPattern = /^groups\..+\.json$/ + const presetsPattern = /^presets\..+\.json$/ + + for (const file of files) { + if (!file.endsWith('.json') || !componentsPattern.test(file) && !groupsPattern.test(file) && !presetsPattern.test(file)) { continue } + const { name } = parse(file) + + console.log('File:', file) + + try { + const content = await readFile(join(resolvedPath, file), 'utf-8') + const data = JSON.parse(content) + + if (componentsPattern.test(file)) { + spaceData.components = regex + ? data.filter((c: SpaceComponent) => regex.test(c.name)) + : data + } + else if (groupsPattern.test(file)) { + spaceData.groups = data + } + else if (presetsPattern.test(file)) { + spaceData.presets = data + } + } + catch (error) { + // Ignore file not found errors + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error + } + } + } + return spaceData + } + + // Read from separate files + const files = await readdir(resolvedPath, { recursive: true }) + + // First, build groups from directory structure + const groupPaths = files + .filter(file => file.includes('/')) + .map(file => parse(file).dir) + .filter((dir, index, self) => self.indexOf(dir) === index) + + spaceData.groups = groupPaths.map((path, index) => ({ + name: path.split('/').pop() || '', + id: index + 1, + uuid: `group-${index + 1}`, + parent_id: 0, + parent_uuid: '', + })) + + // Then process files + for (const file of files) { + if (!file.endsWith('.json')) { continue } + + // Skip consolidated files in separate files mode + if (/(?:components|groups|presets)\..+\.json$/.test(file)) { continue } + + const { dir, name } = parse(file) + const isPreset = name.includes('.presets.') + const baseName = name.split('.')[0] + + // Skip if filter is set and doesn't match the base component name + if (regex && !regex.test(baseName)) { continue } + + const content = await readFile(join(resolvedPath, file), 'utf-8') + const data = JSON.parse(content) + + if (isPreset) { + spaceData.presets.push(...data) + } + else { + // Regular component file + const component = Array.isArray(data) ? data[0] : data + if (dir) { + // If component is in a directory, find the corresponding group + const group = spaceData.groups.find(g => g.name === dir.split('/').pop()) + if (group) { + component.component_group_uuid = group.uuid + } + } + spaceData.components.push(component) + } + } + + return spaceData + } + catch (error) { + handleFileSystemError('read', error as Error) + return spaceData + } +} diff --git a/src/commands/components/constants.ts b/src/commands/components/constants.ts index 010e09c..5e03207 100644 --- a/src/commands/components/constants.ts +++ b/src/commands/components/constants.ts @@ -1,5 +1,61 @@ import type { CommandOptions } from '../../types' +export interface SpaceComponent { + name: string + display_name: string + created_at: string + updated_at: string + id: number + schema: Record + image?: string + preview_field?: string + is_root?: boolean + is_nestable?: boolean + preview_tmpl?: string + all_presets?: Record + preset_id?: number + real_name?: string + component_group_uuid?: string + color: null + internal_tags_list: string[] + interntal_tags_ids: number[] + content_type_asset_preview?: string +} + +export interface SpaceComponentGroup { + name: string + id: number + uuid: string + parent_id: number + parent_uuid: string +} + +export interface ComponentsSaveOptions { + path?: string + filename?: string + separateFiles?: boolean + suffix?: string +} + +export interface SpaceComponentPreset { + id: number + name: string + preset: Record + component_id: number + space_id: number + created_at: string + updated_at: string + image: string + color: string + icon: string + description: string +} + +export interface SpaceData { + components: SpaceComponent[] + groups: SpaceComponentGroup[] + presets: SpaceComponentPreset[] +} /** * Interface representing the options for the `pull-components` command. */ @@ -33,3 +89,27 @@ export interface PullComponentsOptions extends CommandOptions { */ separateFiles?: boolean } + +export interface PushComponentsOptions extends CommandOptions { + /** + * The path to read the components file to. + * Defaults to `.storyblok/components`. + * @default `.storyblok/components` + */ + path?: string + /** + * The space ID. + * @required true + */ + space: string + /** + * The regex filter to apply to the components before pushing. + * @default `.*` + */ + filter?: string + /** + * Indicates whether to read each component to a separate file. + * @default false + */ + separateFiles?: boolean +} diff --git a/src/commands/components/index.ts b/src/commands/components/index.ts index f072489..1ce1adc 100644 --- a/src/commands/components/index.ts +++ b/src/commands/components/index.ts @@ -3,8 +3,8 @@ import { colorPalette, commands } from '../../constants' import { session } from '../../session' import { getProgram } from '../../program' import { CommandError, handleError, konsola } from '../../utils' -import { fetchComponentGroups, fetchComponentPresets, fetchComponents, saveComponentPresetsToFiles, saveComponentsToFiles } from './actions' -import type { PullComponentsOptions } from './constants' +import { fetchComponentGroups, fetchComponentPresets, fetchComponents, readComponentsFiles, saveComponentsToFiles } from './actions' +import type { PullComponentsOptions, PushComponentsOptions } from './constants' const program = getProgram() // Get the shared singleton instance @@ -73,3 +73,49 @@ componentsCommand handleError(error as Error, verbose) } }) + +componentsCommand + .command('push') + .description(`Push your space's components schema as json`) + .option('--fi, --filter ', 'Regex filter to apply to the components before pushing') + .option('--sf, --separate-files', 'Read from separate files instead of consolidated files') + .action(async (options: PushComponentsOptions) => { + konsola.title(` ${commands.COMPONENTS} `, colorPalette.COMPONENTS, 'Pushing components...') + // Global options + const verbose = program.opts().verbose + const { space, path } = componentsCommand.opts() + const { filter } = options + + const { state, initializeSession } = session() + await initializeSession() + + if (!state.isLoggedIn || !state.password || !state.region) { + handleError(new CommandError(`You are currently not logged in. Please login first to get your user info.`), verbose) + return + } + if (!space) { + handleError(new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`), verbose) + return + } + + try { + const spaceData = await readComponentsFiles(path, { + filter, + separateFiles: options.separateFiles, + }) + + console.log('Components:', spaceData.components.length) + console.log('Groups:', spaceData.groups.length) + console.log('Presets:', spaceData.presets.length) + + console.log('Components:', spaceData.components.map(c => c.name)) + + if (filter) { + console.log('Applied filter:', filter) + console.log('Filtered components:', spaceData.components.map(c => c.name)) + } + } + catch (error) { + handleError(error as Error, verbose) + } + })