From c3b09d96c8b0129ffc24dcac35e2ddb2fadebc69 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Wed, 13 Nov 2024 12:24:03 +0100 Subject: [PATCH] feat!: `.storyblok` directory encapsulation as default path BREAKING CHANGE: Generated files will no longer be saved on the root of the project by default, they will be encapsulated inside of a `.storyblok` folder. --- README.md | 17 ++++++++ src/commands/pull-languages/actions.ts | 27 ++---------- src/commands/pull-languages/index.test.ts | 2 +- src/commands/pull-languages/index.ts | 4 +- src/utils/filesystem.test.ts | 51 +++++++++++++++++++++++ src/utils/filesystem.ts | 29 +++++++++++++ 6 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 src/utils/filesystem.test.ts create mode 100644 src/utils/filesystem.ts diff --git a/README.md b/README.md index 96c40cda..4e78da03 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,23 @@ If you prefer not to install the package globally you can use `npx`: npx storyblok ``` +## Breaking Changes ⚠️ + +### `.storyblok` directory as default + +All the commands that generate files will now use the `.storyblok` directory as the default directory to interact with those files. This aims to encapsulate all Storyblok CLI operations instead of filling them on the root. Users would be able to customize the directory by using the `--path` flag. + +Example: + +```bash +storyblok pull-languages --space=12345 +``` + +Will generate the languages in the `.storyblok/languages` directory. + +> [!TIP] +> If you prefer to avoid pushing the `.storyblok` directory to your repository you can add it to your `.gitignore` file. + ## Setup First clone the repository and install the dependencies: diff --git a/src/commands/pull-languages/actions.ts b/src/commands/pull-languages/actions.ts index b1d963f2..2ce4fa73 100644 --- a/src/commands/pull-languages/actions.ts +++ b/src/commands/pull-languages/actions.ts @@ -1,9 +1,9 @@ -import { access, constants, mkdir, writeFile } from 'node:fs/promises' -import { join, resolve } from 'node:path' +import { join } from 'node:path' import { handleAPIError, handleFileSystemError } from '../../utils' import { ofetch } from 'ofetch' import { regionsDomain } from '../../constants' +import { resolvePath, saveToFile } from '../../utils/filesystem' export interface SpaceInternationalizationOptions { languages: SpaceLanguage[] @@ -35,29 +35,10 @@ export const saveLanguagesToFile = async (space: string, internationalizationOpt try { const data = JSON.stringify(internationalizationOptions, null, 2) const filename = `languages.${space}.json` - const resolvedPath = path ? resolve(process.cwd(), path) : process.cwd() + const resolvedPath = resolvePath(path, 'languages') const filePath = join(resolvedPath, filename) - // Check if the path exists, and create it if it doesn't - try { - await access(resolvedPath, constants.F_OK) - } - catch { - try { - await mkdir(resolvedPath, { recursive: true }) - } - catch (mkdirError) { - handleFileSystemError('mkdir', mkdirError as Error) - return // Exit early if the directory creation fails - } - } - - try { - await writeFile(filePath, data, { mode: 0o600 }) - } - catch (writeError) { - handleFileSystemError('write', writeError as Error) - } + await saveToFile(filePath, data) } catch (error) { handleFileSystemError('write', error as Error) diff --git a/src/commands/pull-languages/index.test.ts b/src/commands/pull-languages/index.test.ts index 1f938fb9..1a2da18d 100644 --- a/src/commands/pull-languages/index.test.ts +++ b/src/commands/pull-languages/index.test.ts @@ -89,7 +89,7 @@ describe('pullLanguages', () => { await pullLanguagesCommand.parseAsync(['node', 'test', '--space', '12345']) expect(pullLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu') expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, undefined) - expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`languages.12345.json`)}`) + expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/languages.12345.json`)}`) }) it('should throw an error if the user is not logged in', async () => { diff --git a/src/commands/pull-languages/index.ts b/src/commands/pull-languages/index.ts index e567e5c4..727be6b9 100644 --- a/src/commands/pull-languages/index.ts +++ b/src/commands/pull-languages/index.ts @@ -11,7 +11,7 @@ export const pullLanguagesCommand = program .command('pull-languages') .description(`Download your space's languages schema as json`) .option('-s, --space ', 'space ID') - .option('-p, --path ', 'path to save the file') + .option('-p, --path ', 'path to save the file. Default is .storyblok/languages') .action(async (options) => { konsola.title(` ${commands.PULL_LANGUAGES} `, colorPalette.PULL_LANGUAGES, 'Pulling languages...') // Global options @@ -39,7 +39,7 @@ export const pullLanguagesCommand = program return } await saveLanguagesToFile(space, internationalization, path) - konsola.ok(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}/languages.${space}.json` : `languages.${space}.json`)}`) + konsola.ok(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}/languages.${space}.json` : `.storyblok/languages/languages.${space}.json`)}`) } catch (error) { handleError(error as Error, verbose) diff --git a/src/utils/filesystem.test.ts b/src/utils/filesystem.test.ts new file mode 100644 index 00000000..5b074e9a --- /dev/null +++ b/src/utils/filesystem.test.ts @@ -0,0 +1,51 @@ +import { vol } from 'memfs' +import { resolvePath, saveToFile } from './filesystem' +import { resolve } from 'node:path' + +// tell vitest to use fs mock from __mocks__ folder +// this can be done in a setup file if fs should always be mocked +vi.mock('node:fs') +vi.mock('node:fs/promises') + +beforeEach(() => { + vi.clearAllMocks() + // reset the state of in-memory fs + vol.reset() +}) + +describe('filesystem utils', async () => { + describe('saveToFile', async () => { + it('should save the data to the file', async () => { + const filePath = '/path/to/file.txt' + const data = 'Hello, World!' + + await saveToFile(filePath, data) + + const content = vol.readFileSync(filePath, 'utf8') + expect(content).toBe(data) + }) + + it('should create the directory if it does not exist', async () => { + const filePath = '/path/to/new/file.txt' + const data = 'Hello, World!' + + await saveToFile(filePath, data) + + const content = vol.readFileSync(filePath, 'utf8') + expect(content).toBe(data) + }) + }) + + describe('resolvePath', async () => { + it('should resolve the path correctly', async () => { + const path = '/path/to/file' + const folder = 'folder' + + const resolvedPath = resolvePath(path, folder) + expect(resolvedPath).toBe(resolve(process.cwd(), path)) + + const resolvedPathWithoutPath = resolvePath(undefined, folder) + expect(resolvedPathWithoutPath).toBe(resolve(process.cwd(), '.storyblok/folder')) + }) + }) +}) diff --git a/src/utils/filesystem.ts b/src/utils/filesystem.ts new file mode 100644 index 00000000..c59244ae --- /dev/null +++ b/src/utils/filesystem.ts @@ -0,0 +1,29 @@ +import { parse, resolve } from 'node:path' +import { access, constants, mkdir, writeFile } from 'node:fs/promises' +import { handleFileSystemError } from './error/filesystem-error' + +export const saveToFile = async (filePath: string, data: string) => { + // Check if the path exists, and create it if it doesn't + const resolvedPath = parse(filePath).dir + try { + await access(resolvedPath, constants.F_OK) + } + catch { + try { + await mkdir(resolvedPath, { recursive: true }) + } + catch (mkdirError) { + handleFileSystemError('mkdir', mkdirError as Error) + return // Exit early if the directory creation fails + } + } + + try { + await writeFile(filePath, data, { mode: 0o600 }) + } + catch (writeError) { + handleFileSystemError('write', writeError as Error) + } +} + +export const resolvePath = (path: string | undefined, folder: string) => path ? resolve(process.cwd(), path) : resolve(resolve(process.cwd(), '.storyblok'), folder)