diff --git a/src/commands/components/actions.test.ts b/src/commands/components/actions.test.ts index 2847a48..5292d1c 100644 --- a/src/commands/components/actions.test.ts +++ b/src/commands/components/actions.test.ts @@ -98,10 +98,13 @@ describe('pull components actions', () => { interntal_tags_ids: [1], }] - await saveComponentsToFiles('12345', components, { path: '/path/to/components' }) + await saveComponentsToFiles('12345', { components, groups: [], presets: [] }, { + path: '/path/to/components', + verbose: false, + }) const files = vol.readdirSync('/path/to/components') - expect(files).toEqual(['components.12345.json']) + expect(files).toEqual(['components.json']) }) it('should save components to files with custom filename', async () => { @@ -121,10 +124,14 @@ describe('pull components actions', () => { interntal_tags_ids: [1], }] - await saveComponentsToFiles('12345', components, { path: '/path/to/components2', filename: 'custom' }) + await saveComponentsToFiles('12345', { components, groups: [], presets: [] }, { + path: '/path/to/components2', + filename: 'custom', + verbose: false, + }) const files = vol.readdirSync('/path/to/components2') - expect(files).toEqual(['custom.12345.json']) + expect(files).toEqual(['custom.json']) }) it('should save components to files with custom suffix', async () => { @@ -144,7 +151,11 @@ describe('pull components actions', () => { interntal_tags_ids: [1], }] - await saveComponentsToFiles('12345', components, { path: '/path/to/components3', suffix: 'custom' }) + await saveComponentsToFiles('12345', { components, groups: [], presets: [] }, { + path: '/path/to/components3', + suffix: 'custom', + verbose: false, + }) const files = vol.readdirSync('/path/to/components3') expect(files).toEqual(['components.custom.json']) @@ -177,10 +188,14 @@ describe('pull components actions', () => { interntal_tags_ids: [1], }] - await saveComponentsToFiles('12345', components, { path: '/path/to/components4', separateFiles: true }) + await saveComponentsToFiles('12345', { components, groups: [], presets: [] }, { + path: '/path/to/components4', + separateFiles: true, + verbose: false, + }) const files = vol.readdirSync('/path/to/components4') - expect(files).toEqual(['component-name-2.12345.json', 'component-name.12345.json']) + expect(files).toEqual(['component-name-2.json', 'component-name.json']) }) }) }) diff --git a/src/commands/components/actions.ts b/src/commands/components/actions.ts index 26fedb3..56e0902 100644 --- a/src/commands/components/actions.ts +++ b/src/commands/components/actions.ts @@ -1,82 +1,17 @@ -import { ofetch } from 'ofetch' -import { handleAPIError, handleFileSystemError, slugify } from '../../utils' -import { regionsDomain } from '../../constants' +import { handleAPIError, handleFileSystemError } from '../../utils' +import type { RegionCode } from '../../constants' import { join } from 'node:path' import { resolvePath, saveToFile } from '../../utils/filesystem' -import type { PullComponentsOptions } from './constants' +import type { SaveComponentsOptions, SpaceComponent, SpaceComponentGroup, SpaceComponentPreset, SpaceData } from './constants' +import { getStoryblokUrl } from '../../utils/api-routes' +import { customFetch } from '../../utils/fetch' -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[] -} -/** - * Resolves the nested folder structure based on component group hierarchy. - * @param groupUuid - The UUID of the component group. - * @param groups - The list of all component groups. - * @returns The resolved path for the component group. - */ -const resolveGroupPath = (groupUuid: string, groups: SpaceComponentGroup[]): string => { - const group = groups.find(g => g.uuid === groupUuid) - if (!group) { return '' } - const parentPath = group.parent_uuid ? resolveGroupPath(group.parent_uuid, groups) : '' - return join(parentPath, slugify(group.name)) -} - -export const fetchComponents = async (space: string, token: string, region: string): Promise => { +export const fetchComponents = async (space: string, token: string, region: RegionCode): Promise => { try { - const response = await ofetch(`https://${regionsDomain[region]}/v1/spaces/${space}/components`, { + const url = getStoryblokUrl(region) + const response = await customFetch<{ + components: SpaceComponent[] + }>(`${url}/spaces/${space}/components`, { headers: { Authorization: token, }, @@ -88,9 +23,12 @@ export const fetchComponents = async (space: string, token: string, region: stri } } -export const fetchComponentGroups = async (space: string, token: string, region: string): Promise => { +export const fetchComponentGroups = async (space: string, token: string, region: RegionCode): Promise => { try { - const response = await ofetch(`https://${regionsDomain[region]}/v1/spaces/${space}/component_groups`, { + const url = getStoryblokUrl(region) + const response = await customFetch<{ + component_groups: SpaceComponentGroup[] + }>(`${url}/spaces/${space}/component_groups`, { headers: { Authorization: token, }, @@ -102,9 +40,12 @@ export const fetchComponentGroups = async (space: string, token: string, region: } } -export const fetchComponentPresets = async (space: string, token: string, region: string): Promise => { +export const fetchComponentPresets = async (space: string, token: string, region: RegionCode): Promise => { try { - const response = await ofetch(`https://${regionsDomain[region]}/v1/spaces/${space}/presets`, { + const url = getStoryblokUrl(region) + const response = await customFetch<{ + presets: SpaceComponentPreset[] + }>(`${url}/spaces/${space}/presets`, { headers: { Authorization: token, }, @@ -119,7 +60,7 @@ export const fetchComponentPresets = async (space: string, token: string, region export const saveComponentsToFiles = async ( space: string, spaceData: SpaceData, - options: PullComponentsOptions, + options: SaveComponentsOptions, ) => { const { components, groups, presets } = spaceData const { filename = 'components', suffix, path, separateFiles } = options diff --git a/src/commands/components/constants.ts b/src/commands/components/constants.ts index 010e09c..ccf0f7e 100644 --- a/src/commands/components/constants.ts +++ b/src/commands/components/constants.ts @@ -1,20 +1,66 @@ 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. */ export interface PullComponentsOptions extends CommandOptions { - /** - * The path to save the components file to. - * Defaults to `.storyblok/components`. - * @default `.storyblok/components` - */ - path?: string - /** - * The space ID. - * @required true - */ - space: string + /** * The filename to save the file as. * Defaults to `components`. The file will be saved as `..json`. @@ -33,3 +79,12 @@ export interface PullComponentsOptions extends CommandOptions { */ separateFiles?: boolean } + +export interface SaveComponentsOptions extends PullComponentsOptions { + /** + * The path to save the components file to. + * Defaults to `.storyblok/components`. + * @default `.storyblok/components` + */ + path?: string +} diff --git a/src/commands/components/index.test.ts b/src/commands/components/index.test.ts index 3598055..62b6f91 100644 --- a/src/commands/components/index.test.ts +++ b/src/commands/components/index.test.ts @@ -1,20 +1,23 @@ import { session } from '../../session' import { CommandError, konsola } from '../../utils' -import { pullComponents, saveComponentsToFiles } from './actions' +import { fetchComponents, saveComponentsToFiles } from './actions' import { componentsCommand } from '.' import chalk from 'chalk' import { colorPalette } from '../../constants' +import { group } from 'node:console' vi.mock('./actions', () => ({ - pullComponents: vi.fn(), + fetchComponents: vi.fn(), + fetchComponentGroups: vi.fn(), + fetchComponentPresets: vi.fn(), saveComponentsToFiles: vi.fn(), })) vi.mock('../../creds', () => ({ - addNetrcEntry: vi.fn(), - isAuthorized: vi.fn(), - getNetrcCredentials: vi.fn(), - getCredentialsForMachine: vi.fn(), + getCredentials: vi.fn(), + addCredentials: vi.fn(), + removeCredentials: vi.fn(), + removeAllCredentials: vi.fn(), })) // Mocking the session module @@ -97,13 +100,17 @@ describe('pull', () => { region: 'eu', } - vi.mocked(pullComponents).mockResolvedValue(mockResponse) + vi.mocked(fetchComponents).mockResolvedValue(mockResponse) await componentsCommand.parseAsync(['node', 'test', 'pull', '--space', '12345']) - expect(pullComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') - expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', mockResponse, { + expect(fetchComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') + expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', { + components: mockResponse, + groups: [], + presets: [], + }, { path: undefined, }) - expect(konsola.ok).toHaveBeenCalledWith(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(`./storyblok/components/components.12345.json`)}`) + expect(konsola.ok).toHaveBeenCalledWith(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/components/12345/components.json`)}`) }) it('should throw an error if the user is not logged in', async () => { @@ -149,12 +156,16 @@ describe('pull', () => { region: 'eu', } - vi.mocked(pullComponents).mockResolvedValue(mockResponse) + vi.mocked(fetchComponents).mockResolvedValue(mockResponse) await componentsCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--path', '/path/to/components']) - expect(pullComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') - expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', mockResponse, { path: '/path/to/components' }) - expect(konsola.ok).toHaveBeenCalledWith(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(`/path/to/components/components.12345.json`)}`) + expect(fetchComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') + expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', { + components: mockResponse, + groups: [], + presets: [], + }, { path: '/path/to/components' }) + expect(konsola.ok).toHaveBeenCalledWith(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(`/path/to/components/components.json`)}`) }) }) @@ -178,12 +189,16 @@ describe('pull', () => { region: 'eu', } - vi.mocked(pullComponents).mockResolvedValue(mockResponse) + vi.mocked(fetchComponents).mockResolvedValue(mockResponse) await componentsCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--filename', 'custom']) - expect(pullComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') - expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', mockResponse, { filename: 'custom' }) - expect(konsola.ok).toHaveBeenCalledWith(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(`./storyblok/components/custom.12345.json`)}`) + expect(fetchComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') + expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', { + components: mockResponse, + groups: [], + presets: [], + }, { filename: 'custom' }) + expect(konsola.ok).toHaveBeenCalledWith(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/components/12345/custom.json`)}`) }) }) @@ -217,12 +232,16 @@ describe('pull', () => { region: 'eu', } - vi.mocked(pullComponents).mockResolvedValue(mockResponse) + vi.mocked(fetchComponents).mockResolvedValue(mockResponse) await componentsCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--separate-files']) - expect(pullComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') - expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', mockResponse, { separateFiles: true, path: undefined }) - expect(konsola.ok).toHaveBeenCalledWith(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(`./storyblok/components`)}`) + expect(fetchComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') + expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', { + components: mockResponse, + groups: [], + presets: [], + }, { separateFiles: true, path: undefined }) + expect(konsola.ok).toHaveBeenCalledWith(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/components/12345/`)}`) }) it('should warn the user if the --filename is used along', async () => { @@ -244,11 +263,15 @@ describe('pull', () => { region: 'eu', } - vi.mocked(pullComponents).mockResolvedValue(mockResponse) + vi.mocked(fetchComponents).mockResolvedValue(mockResponse) await componentsCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--separate-files', '--filename', 'custom']) - expect(pullComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') - expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', mockResponse, { separateFiles: true, filename: 'custom' }) + expect(fetchComponents).toHaveBeenCalledWith('12345', 'valid-token', 'eu') + expect(saveComponentsToFiles).toHaveBeenCalledWith('12345', { + components: mockResponse, + groups: [], + presets: [], + }, { separateFiles: true, filename: 'custom' }) expect(konsola.warn).toHaveBeenCalledWith(`The --filename option is ignored when using --separate-files`) }) }) diff --git a/src/commands/components/index.ts b/src/commands/components/index.ts index f072489..7c41591 100644 --- a/src/commands/components/index.ts +++ b/src/commands/components/index.ts @@ -3,7 +3,7 @@ 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 { fetchComponentGroups, fetchComponentPresets, fetchComponents, saveComponentsToFiles } from './actions' import type { PullComponentsOptions } from './constants' const program = getProgram() // Get the shared singleton instance @@ -26,7 +26,7 @@ componentsCommand const verbose = program.opts().verbose // Command options const { space, path } = componentsCommand.opts() - const { separateFiles, suffix = space, filename = 'components' } = options + const { separateFiles, suffix, filename = 'components' } = options const { state, initializeSession } = session() await initializeSession() @@ -62,11 +62,14 @@ componentsCommand if (filename !== 'components') { konsola.warn(`The --filename option is ignored when using --separate-files`) } - konsola.ok(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}` : './storyblok/components')}`) + const filePath = path ? `${path}/` : `.storyblok/components/${space}/` + konsola.ok(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`) } else { - const msgFilename = `${filename}.${suffix}.json` - konsola.ok(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}/${msgFilename}` : `./storyblok/components/${msgFilename}`)}`) + const fileName = suffix ? `${filename}.${suffix}.json` : `${filename}.json` + const filePath = path ? `${path}/${fileName}` : `.storyblok/components/${space}/${fileName}` + + konsola.ok(`Components downloaded successfully in ${chalk.hex(colorPalette.PRIMARY)(filePath)}`) } } catch (error) {