Skip to content

Commit

Permalink
Merge pull request #148 from storyblok/feature/refactor-language-comm…
Browse files Browse the repository at this point in the history
…and-updated-dx

feat: re-structured language command with sub commands
  • Loading branch information
alvarosabu authored Jan 16, 2025
2 parents ac810ae + 4862d0d commit e48e870
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 254 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"request": "launch",
"name": "Debug Pull languages",
"program": "${workspaceFolder}/dist/index.mjs",
"args": ["pull-languages", "--space", "2950182323", "--path", ".storyblok"],
"args": ["languages", "pull", "--space", "2950182323", "--path", ".storyblok"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { vol } from 'memfs'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { pullLanguages, saveLanguagesToFile } from './actions'
import { fetchLanguages, saveLanguagesToFile } from './actions'

const handlers = [
http.get('https://api.storyblok.com/v1/spaces/12345', async ({ request }) => {
Expand Down Expand Up @@ -44,7 +44,7 @@ describe('pull languages actions', () => {
vol.reset()
})

describe('pullLanguages', () => {
describe('fetchLanguages', () => {
it('should pull languages successfully with a valid token', async () => {
const mockResponse = {
default_lang_name: 'en',
Expand All @@ -59,12 +59,12 @@ describe('pull languages actions', () => {
},
],
}
const result = await pullLanguages('12345', 'valid-token', 'eu')
const result = await fetchLanguages('12345', 'valid-token', 'eu')
expect(result).toEqual(mockResponse)
})
})
it('should throw an masked error for invalid token', async () => {
await expect(pullLanguages('12345', 'invalid-token', 'eu')).rejects.toThrow(
await expect(fetchLanguages('12345', 'invalid-token', 'eu')).rejects.toThrow(
new Error(`The user is not authorized to access the API`),
)
})
Expand All @@ -90,7 +90,7 @@ describe('pull languages actions', () => {
verbose: false,
space: '12345',
})
const content = vol.readFileSync('/temp/languages.12345.json', 'utf8')
const content = vol.readFileSync('/temp/languages.json', 'utf8')
expect(content).toBe(JSON.stringify(mockResponse, null, 2))
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { RegionCode } from '../../constants'
import type { SpaceInternationalization } from '../../types'
import { getStoryblokUrl } from '../../utils/api-routes'

export const pullLanguages = async (space: string, token: string, region: RegionCode): Promise<SpaceInternationalization | undefined> => {
export const fetchLanguages = async (space: string, token: string, region: RegionCode): Promise<SpaceInternationalization | undefined> => {
try {
const url = getStoryblokUrl(region)
const response = await customFetch<{
Expand All @@ -31,10 +31,10 @@ export const pullLanguages = async (space: string, token: string, region: Region

export const saveLanguagesToFile = async (space: string, internationalizationOptions: SpaceInternationalization, options: PullLanguagesOptions) => {
try {
const { filename = 'languages', suffix = space, path } = options
const { filename = 'languages', suffix, path } = options
const data = JSON.stringify(internationalizationOptions, null, 2)
const name = `${filename}.${suffix}.json`
const resolvedPath = resolvePath(path, 'languages')
const name = suffix ? `${filename}.${suffix}.json` : `${filename}.json`
const resolvedPath = resolvePath(path, `languages/${space}/`)
const filePath = join(resolvedPath, name)

await saveToFile(filePath, data)
Expand Down
File renamed without changes.
234 changes: 234 additions & 0 deletions src/commands/languages/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import chalk from 'chalk'
import { languagesCommand } from '.'
import { session } from '../../session'
import { CommandError, konsola } from '../../utils'
import { fetchLanguages, saveLanguagesToFile } from './actions'
import { colorPalette } from '../../constants'

vi.mock('./actions', () => ({
fetchLanguages: vi.fn(),
saveLanguagesToFile: vi.fn(),
}))

vi.mock('../../creds', () => ({
getCredentials: vi.fn(),
addCredentials: vi.fn(),
removeCredentials: vi.fn(),
removeAllCredentials: vi.fn(),
}))

// Mocking the session module
vi.mock('../../session', () => {
let _cache: Record<string, any> | null = null
const session = () => {
if (!_cache) {
_cache = {
state: {
isLoggedIn: false,
},
updateSession: vi.fn(),
persistCredentials: vi.fn(),
initializeSession: vi.fn(),
}
}
return _cache
}

return {
session,
}
})

vi.mock('../../utils', async () => {
const actualUtils = await vi.importActual('../../utils')
return {
...actualUtils,
konsola: {
ok: vi.fn(),
title: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
handleError: (error: Error, header = false) => {
konsola.error(error, header)
// Optionally, prevent process.exit during tests
},
}
})

describe('languagesCommand', () => {
describe('pull', () => {
beforeEach(() => {
vi.resetAllMocks()
vi.clearAllMocks()
// Reset the option values
languagesCommand._optionValues = {}
for (const command of languagesCommand.commands) {
command._optionValues = {}
}
})
describe('default mode', () => {
it('should prompt the user if operation was sucessfull', 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(fetchLanguages).mockResolvedValue(mockResponse)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345'])
expect(fetchLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu')
expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, {
path: undefined,
})
expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/12345/languages.json`)}`)
})

it('should throw an error if the user is not logged in', async () => {
session().state = {
isLoggedIn: false,
}
const mockError = new CommandError(`You are currently not logged in. Please login first to get your user info.`)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345'])
expect(konsola.error).toHaveBeenCalledWith(mockError, false)
})

it('should throw an error if the space is not provided', async () => {
session().state = {
isLoggedIn: true,
password: 'valid-token',
region: 'eu',
}

const mockError = new CommandError(`Please provide the space as argument --space YOUR_SPACE_ID.`)
await languagesCommand.parseAsync(['node', 'test', 'pull'])
expect(konsola.error).toHaveBeenCalledWith(mockError, false)
})

it('should prompt a warning the user if no languages are found', async () => {
const mockResponse = {
default_lang_name: 'en',
languages: [],
}
session().state = {
isLoggedIn: true,
password: 'valid-token',
region: 'eu',
}

vi.mocked(fetchLanguages).mockResolvedValue(mockResponse)

await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '24568'])
expect(konsola.warn).toHaveBeenCalledWith(`No languages found in the space 24568`)
})
})

describe('--path option', () => {
it('should save the file at the provided path', 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(fetchLanguages).mockResolvedValue(mockResponse)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--path', '/tmp'])
expect(fetchLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu')
expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, {
path: '/tmp',
})
expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`/tmp/languages.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(fetchLanguages).mockResolvedValue(mockResponse)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--filename', 'custom-languages'])
expect(fetchLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu')
expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, {
filename: 'custom-languages',
path: undefined,
})
expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/12345/custom-languages.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(fetchLanguages).mockResolvedValue(mockResponse)
await languagesCommand.parseAsync(['node', 'test', 'pull', '--space', '12345', '--suffix', 'custom-suffix'])
expect(fetchLanguages).toHaveBeenCalledWith('12345', 'valid-token', 'eu')
expect(saveLanguagesToFile).toHaveBeenCalledWith('12345', mockResponse, {
suffix: 'custom-suffix',
path: undefined,
})
expect(konsola.ok).toHaveBeenCalledWith(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(`.storyblok/languages/12345/languages.custom-suffix.json`)}`)
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,31 @@ import { colorPalette, commands } from '../../constants'
import { CommandError, handleError, konsola } from '../../utils'
import { getProgram } from '../../program'
import { session } from '../../session'
import { pullLanguages, saveLanguagesToFile } from './actions'
import { fetchLanguages, saveLanguagesToFile } from './actions'
import chalk from 'chalk'
import type { PullLanguagesOptions } from './constants'

const program = getProgram() // Get the shared singleton instance

export const pullLanguagesCommand = program
.command('pull-languages')
.description(`Download your space's languages schema as json`)
export const languagesCommand = program
.command('languages')
.alias('lang')
.description(`Manage your space's languages`)
.option('-s, --space <space>', 'space ID')
.option('-p, --path <path>', 'path to save the file. Default is .storyblok/languages')

languagesCommand
.command('pull')
.description(`Download your space's languages schema as json`)
.option('-f, --filename <filename>', 'filename to save the file as <filename>.<suffix>.json')
.option('--su, --suffix <suffix>', 'suffix to add to the file name (e.g. languages.<suffix>.json). By default, the space ID is used.')
.action(async (options: PullLanguagesOptions) => {
konsola.title(` ${commands.PULL_LANGUAGES} `, colorPalette.PULL_LANGUAGES, 'Pulling languages...')
konsola.title(` ${commands.LANGUAGES} `, colorPalette.LANGUAGES, 'Pulling languages...')
// Global options
const verbose = program.opts().verbose
// Command options
const { space, path, filename = 'languages', suffix = options.space } = options
const { space, path } = languagesCommand.opts()
const { filename = 'languages', suffix = options.space } = options

const { state, initializeSession } = session()
await initializeSession()
Expand All @@ -35,14 +41,19 @@ export const pullLanguagesCommand = program
}

try {
const internationalization = await pullLanguages(space, state.password, state.region)
const internationalization = await fetchLanguages(space, state.password, state.region)

if (!internationalization || internationalization.languages?.length === 0) {
konsola.warn(`No languages found in the space ${space}`)
return
}
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`)}`)
await saveLanguagesToFile(space, internationalization, {
...options,
path,
})
const fileName = suffix ? `${filename}.${suffix}.json` : `${filename}.json`
const filePath = path ? `${path}/${fileName}` : `.storyblok/languages/${space}/${fileName}`
konsola.ok(`Languages schema downloaded successfully at ${chalk.hex(colorPalette.PRIMARY)(filePath)}`)
}
catch (error) {
handleError(error as Error, verbose)
Expand Down
Loading

0 comments on commit e48e870

Please sign in to comment.