From c3b09d96c8b0129ffc24dcac35e2ddb2fadebc69 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Wed, 13 Nov 2024 12:24:03 +0100 Subject: [PATCH 1/6] 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 96c40cd..4e78da0 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 b1d963f..2ce4fa7 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 1f938fb..1a2da18 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 e567e5c..727be6b 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 0000000..5b074e9 --- /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 0000000..c59244a --- /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) From 19df3ffc2a8e78b1068c1d27a0ffdbabd5e2324f Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Wed, 13 Nov 2024 15:24:20 +0100 Subject: [PATCH 2/6] feat!: generated files naming syntax convention BREAKING CHANGE: generated file naming syntax is now standard between all the commands, following the structure `..json`. Both filename and suffix are customizable and by default would be the command name and the spaceId respectively. Ex: `components.12345.json` --- README.md | 31 +++++++++ src/commands/pull-languages/actions.test.ts | 7 +- src/commands/pull-languages/actions.ts | 8 ++- src/commands/pull-languages/constants.ts | 30 +++++++++ src/commands/pull-languages/index.test.ts | 75 +++++++++++++++++++-- src/commands/pull-languages/index.ts | 11 +-- src/types/index.ts | 9 +++ 7 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 src/commands/pull-languages/constants.ts create mode 100644 src/types/index.ts diff --git a/README.md b/README.md index 4e78da0..b614027 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,37 @@ 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. +### Generated filename syntax conventions + +The generated files will now follow a more consistent naming convention. The files will be named using the following syntax: + +``` +.. +``` + +Where: + +- `` is the name of the file. Customizable by the user with the `--filename` flag +- `` is an optional suffix to differentiate the files. By default is going to be the `spaceId` and is customizable by the user with the `--suffix` flag +- `` is the file extension. By default is `json` (Not configurable) + +Example: + +```bash +storyblok pull-languages --space=12345 --filename=my-languages --suffix=dev +``` + +Will generate the languages in the following path `.storyblok/languages/my-languages.dev.json` + +If you would like to use a timestamp as the suffix you can use: + +```bash +storyblok pull-languages --space=12345 --filename=my-languages --suffix="$(date +%s)" +``` + +> [!WARNING] +> The `--filename` will be ignored in the case that `--separate-files` is used on the commands that supports it. + ## Setup First clone the repository and install the dependencies: diff --git a/src/commands/pull-languages/actions.test.ts b/src/commands/pull-languages/actions.test.ts index dbec82b..49d533d 100644 --- a/src/commands/pull-languages/actions.test.ts +++ b/src/commands/pull-languages/actions.test.ts @@ -84,7 +84,12 @@ describe('pull languages actions', () => { }, ], } - await saveLanguagesToFile('12345', mockResponse, '/temp') + await saveLanguagesToFile('12345', mockResponse, { + filename: 'languages', + path: '/temp', + verbose: false, + space: '12345', + }) const content = vol.readFileSync('/temp/languages.12345.json', 'utf8') expect(content).toBe(JSON.stringify(mockResponse, null, 2)) }) diff --git a/src/commands/pull-languages/actions.ts b/src/commands/pull-languages/actions.ts index 2ce4fa7..03b57d0 100644 --- a/src/commands/pull-languages/actions.ts +++ b/src/commands/pull-languages/actions.ts @@ -4,6 +4,7 @@ import { handleAPIError, handleFileSystemError } from '../../utils' import { ofetch } from 'ofetch' import { regionsDomain } from '../../constants' import { resolvePath, saveToFile } from '../../utils/filesystem' +import type { PullLanguagesOptions } from './constants' export interface SpaceInternationalizationOptions { languages: SpaceLanguage[] @@ -31,12 +32,13 @@ export const pullLanguages = async (space: string, token: string, region: string } } -export const saveLanguagesToFile = async (space: string, internationalizationOptions: SpaceInternationalizationOptions, path?: string) => { +export const saveLanguagesToFile = async (space: string, internationalizationOptions: SpaceInternationalizationOptions, options: PullLanguagesOptions) => { try { + const { filename = 'languages', suffix = space, path } = options const data = JSON.stringify(internationalizationOptions, null, 2) - const filename = `languages.${space}.json` + const name = `${filename}.${suffix}.json` const resolvedPath = resolvePath(path, 'languages') - const filePath = join(resolvedPath, filename) + const filePath = join(resolvedPath, name) await saveToFile(filePath, data) } diff --git a/src/commands/pull-languages/constants.ts b/src/commands/pull-languages/constants.ts new file mode 100644 index 0000000..c25a4c5 --- /dev/null +++ b/src/commands/pull-languages/constants.ts @@ -0,0 +1,30 @@ +import type { CommandOptions } from '../../types' + +/** + * Interface representing the options for the `pull-languages` command. + */ +export interface PullLanguagesOptions extends CommandOptions { + /** + * The path to save the languages file to. + * Defaults to `.storyblok/languages`. + * @default `.storyblok/languages` + */ + path?: string + /** + * The space ID. + * @required true + */ + space: string + /** + * The filename to save the file as. + * Defaults to `languages`. The file will be saved as `..json`. + * @default `languages + */ + filename?: string + /** + * The suffix to add to the filename. + * Defaults to the space ID. + * @default space + */ + suffix?: string +} diff --git a/src/commands/pull-languages/index.test.ts b/src/commands/pull-languages/index.test.ts index 1a2da18..7c2732f 100644 --- a/src/commands/pull-languages/index.test.ts +++ b/src/commands/pull-languages/index.test.ts @@ -88,7 +88,9 @@ describe('pullLanguages', () => { vi.mocked(pullLanguages).mockResolvedValue(mockResponse) await pullLanguagesCommand.parseAsync(['node', 'test', '--space', '12345']) expect(pullLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu') - expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, undefined) + expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, { + space: '12345', + }) expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/languages.12345.json`)}`) }) @@ -109,8 +111,6 @@ describe('pullLanguages', () => { } const mockError = new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`) - - console.log(pullLanguagesCommand) await pullLanguagesCommand.parseAsync(['node', 'test']) expect(konsola.error).toHaveBeenCalledWith(mockError, false) }) @@ -157,8 +157,75 @@ describe('pullLanguages', () => { vi.mocked(pullLanguages).mockResolvedValue(mockResponse) await pullLanguagesCommand.parseAsync(['node', 'test', '--space', '12345', '--path', '/tmp']) expect(pullLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu') - expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, '/tmp') + expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, { + path: '/tmp', + space: '12345', + }) expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`/tmp/languages.12345.json`)}`) }) }) + + describe('--filename option', () => { + it('should save the file with the provided filename', async () => { + const mockResponse = { + default_lang_name: 'en', + languages: [ + { + code: 'ca', + name: 'Catalan', + }, + { + code: 'fr', + name: 'French', + }, + ], + } + session().state = { + isLoggedIn: true, + password: 'valid-token', + region: 'eu', + } + + vi.mocked(pullLanguages).mockResolvedValue(mockResponse) + await pullLanguagesCommand.parseAsync(['node', 'test', '--space', '12345', '--filename', 'custom-languages']) + expect(pullLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu') + expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, { + filename: 'custom-languages', + space: '12345', + }) + expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/custom-languages.12345.json`)}`) + }) + }) + + describe('--suffix option', () => { + it('should save the file with the provided suffix', async () => { + const mockResponse = { + default_lang_name: 'en', + languages: [ + { + code: 'ca', + name: 'Catalan', + }, + { + code: 'fr', + name: 'French', + }, + ], + } + session().state = { + isLoggedIn: true, + password: 'valid-token', + region: 'eu', + } + + vi.mocked(pullLanguages).mockResolvedValue(mockResponse) + await pullLanguagesCommand.parseAsync(['node', 'test', '--space', '12345', '--suffix', 'custom-suffix']) + expect(pullLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu') + expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, { + suffix: 'custom-suffix', + space: '12345', + }) + expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/languages.custom-suffix.json`)}`) + }) + }) }) diff --git a/src/commands/pull-languages/index.ts b/src/commands/pull-languages/index.ts index 727be6b..20ccefc 100644 --- a/src/commands/pull-languages/index.ts +++ b/src/commands/pull-languages/index.ts @@ -4,6 +4,7 @@ import { getProgram } from '../../program' import { session } from '../../session' import { pullLanguages, saveLanguagesToFile } from './actions' import chalk from 'chalk' +import type { PullLanguagesOptions } from './constants' const program = getProgram() // Get the shared singleton instance @@ -12,12 +13,14 @@ export const pullLanguagesCommand = program .description(`Download your space's languages schema as json`) .option('-s, --space ', 'space ID') .option('-p, --path ', 'path to save the file. Default is .storyblok/languages') - .action(async (options) => { + .option('-f, --filename ', 'filename to save the file as ..json') + .option('-su, --suffix ', 'suffix to add to the file name (e.g. languages..json). By default, the space ID is used.') + .action(async (options: PullLanguagesOptions) => { konsola.title(` ${commands.PULL_LANGUAGES} `, colorPalette.PULL_LANGUAGES, 'Pulling languages...') // Global options const verbose = program.opts().verbose // Command options - const { space, path } = options + const { space, path, filename = 'languages', suffix = options.space } = options const { state, initializeSession } = session() await initializeSession() @@ -38,8 +41,8 @@ export const pullLanguagesCommand = program konsola.warn(`No languages found in the space ${space}`) return } - await saveLanguagesToFile(space, internationalization, path) - konsola.ok(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}/languages.${space}.json` : `.storyblok/languages/languages.${space}.json`)}`) + await saveLanguagesToFile(space, internationalization, options) + konsola.ok(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(path ? `${path}/${filename}.${suffix}.json` : `.storyblok/languages/${filename}.${suffix}.json`)}`) } catch (error) { handleError(error as Error, verbose) diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..05ce6d8 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,9 @@ +/** + * Interface representing the default options for a CLI command. + */ +export interface CommandOptions { + /** + * Indicates whether verbose output is enabled. + */ + verbose: boolean +} From 42b09f2fcea84ad2f8eb819a5f5890d9952a6f7d Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Wed, 27 Nov 2024 18:03:52 +0100 Subject: [PATCH 3/6] feat: remove strict write permission for generated files --- src/utils/filesystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/filesystem.ts b/src/utils/filesystem.ts index c59244a..aed37a4 100644 --- a/src/utils/filesystem.ts +++ b/src/utils/filesystem.ts @@ -19,7 +19,7 @@ export const saveToFile = async (filePath: string, data: string) => { } try { - await writeFile(filePath, data, { mode: 0o600 }) + await writeFile(filePath, data) } catch (writeError) { handleFileSystemError('write', writeError as Error) From 65dc86c9de49fb2fecbb9bbbe29dec4fbaec5111 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Wed, 27 Nov 2024 18:11:40 +0100 Subject: [PATCH 4/6] feat: remove unnecesary check on save files since recursive mkdir is used. --- src/utils/filesystem.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/utils/filesystem.ts b/src/utils/filesystem.ts index aed37a4..b45e60f 100644 --- a/src/utils/filesystem.ts +++ b/src/utils/filesystem.ts @@ -1,25 +1,23 @@ import { parse, resolve } from 'node:path' -import { access, constants, mkdir, writeFile } from 'node:fs/promises' +import { 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 + // Get the directory path const resolvedPath = parse(filePath).dir + + // Ensure the directory exists try { - await access(resolvedPath, constants.F_OK) + await mkdir(resolvedPath, { recursive: true }) } - catch { - try { - await mkdir(resolvedPath, { recursive: true }) - } - catch (mkdirError) { - handleFileSystemError('mkdir', mkdirError as Error) - return // Exit early if the directory creation fails - } + catch (mkdirError) { + handleFileSystemError('mkdir', mkdirError as Error) + return // Exit early if the directory creation fails } + // Write the file try { - await writeFile(filePath, data) + await writeFile(filePath, data, { mode: 0o600 }) } catch (writeError) { handleFileSystemError('write', writeError as Error) From 36557ebe7e2bdc381a2c3f2ad2278e61d192182c Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Wed, 27 Nov 2024 18:29:33 +0100 Subject: [PATCH 5/6] feat: change argument syntax for suffix --- src/commands/pull-languages/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/pull-languages/index.ts b/src/commands/pull-languages/index.ts index 20ccefc..59fc3e8 100644 --- a/src/commands/pull-languages/index.ts +++ b/src/commands/pull-languages/index.ts @@ -14,7 +14,7 @@ export const pullLanguagesCommand = program .option('-s, --space ', 'space ID') .option('-p, --path ', 'path to save the file. Default is .storyblok/languages') .option('-f, --filename ', 'filename to save the file as ..json') - .option('-su, --suffix ', 'suffix to add to the file name (e.g. languages..json). By default, the space ID is used.') + .option('--su, --suffix ', 'suffix to add to the file name (e.g. languages..json). By default, the space ID is used.') .action(async (options: PullLanguagesOptions) => { konsola.title(` ${commands.PULL_LANGUAGES} `, colorPalette.PULL_LANGUAGES, 'Pulling languages...') // Global options From c66270c4c77ce2ea590b623132784ecad548d941 Mon Sep 17 00:00:00 2001 From: alvarosabu Date: Fri, 29 Nov 2024 17:23:20 +0100 Subject: [PATCH 6/6] feat: remove again strict permissions for filesystem --- src/utils/filesystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/filesystem.ts b/src/utils/filesystem.ts index b45e60f..5e9eaa0 100644 --- a/src/utils/filesystem.ts +++ b/src/utils/filesystem.ts @@ -17,7 +17,7 @@ export const saveToFile = async (filePath: string, data: string) => { // Write the file try { - await writeFile(filePath, data, { mode: 0o600 }) + await writeFile(filePath, data) } catch (writeError) { handleFileSystemError('write', writeError as Error)