Skip to content

Commit

Permalink
Merge pull request #36 from mbc-net/feat/cli-version-flag
Browse files Browse the repository at this point in the history
[CLI]: specify framework version for project creation
  • Loading branch information
koichimurakami authored Nov 4, 2024
2 parents 163e71a + 9a37428 commit 0cb0043
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 52 deletions.
3 changes: 1 addition & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
![MBC CQRS serverless framework](https://mbc-net.github.io/mbc-cqrs-serverless-doc/img/mbc-cqrs-serverless.png)
# MBC CQRS serverless framework CLI package

## Description

The MBC CLI is a command-line interface tool that helps you to initialize your mbc-cqrs-serverless applications.

## Installation

To install `mbc`, run:

```bash
npm install -g @mbc-cqrs-serverless/cli
```

## Usage

### `mbc new|n [projectName@version]`

There are 3 usages for the new command:

- `mbc new`
- Creates a new project in the current folder using a default name with the latest framework version.
- `mbc new [projectName]`
- Creates a new project in the `projectName` folder using the latest framework version.
- `mbc new [projectName@version]`
- If the specified version exists, the CLI uses that exact version.
- If the provided version is a prefix, the CLI uses the latest version matching that prefix.
- If no matching version is found, the CLI logs an error and provides a list of available versions for the user.


## Documentation

Visit https://mbc-net.github.io/mbc-cqrs-serverless-doc/ to view the full documentation.
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
"rimraf": "^5.0.5"
},
"devDependencies": {
"@faker-js/faker": "^8.3.1",
"@mbc-cqrs-serverless/core": "^0.1.21-beta.0"
"@faker-js/faker": "^8.3.1"
}
}
42 changes: 42 additions & 0 deletions packages/cli/src/actions/new.action.get-package-version.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { execSync } from 'child_process'

import { exportsForTesting } from './new.action'

const { getPackageVersion } = exportsForTesting

jest.mock('child_process', () => ({
execSync: jest.fn(),
}))

describe('getPackageVersion', () => {
const mockExecSync = execSync as jest.MockedFunction<typeof execSync>
const packageName = '@mbc-cqrs-serverless/core'

afterEach(() => {
jest.clearAllMocks()
})

it('should return the latest version when isLatest is true', () => {
const mockLatestVersion = '1.2.3'
mockExecSync.mockReturnValue(Buffer.from(`${mockLatestVersion}\n`))

const result = getPackageVersion(packageName, true)

expect(mockExecSync).toHaveBeenCalledWith(
`npm view ${packageName} dist-tags.latest`,
)
expect(result).toEqual([mockLatestVersion])
})

it('should return all versions when isLatest is false', () => {
const mockVersions = ['1.0.0', '1.1.0', '1.2.0']
mockExecSync.mockReturnValue(Buffer.from(JSON.stringify(mockVersions)))

const result = getPackageVersion(packageName, false)

expect(mockExecSync).toHaveBeenCalledWith(
`npm view ${packageName} versions --json`,
)
expect(result).toEqual(mockVersions)
})
})
126 changes: 88 additions & 38 deletions packages/cli/src/actions/new.action.spec.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,111 @@
import { faker } from '@faker-js/faker'
import { copyFileSync, readFileSync, unlinkSync } from 'fs'
import { execSync } from 'child_process'
import { Command } from 'commander'
import { copyFileSync, cpSync, mkdirSync } from 'fs'
import path from 'path'

import { exportsForTesting } from './new.action'
import newAction, { exportsForTesting } from './new.action'

const { useLatestPackageVersion } = exportsForTesting
jest.mock('child_process', () => ({
execSync: jest.fn(),
}))

// create testcase for useLatestPackageVersion function in new.action.ts file
describe('useLatestPackageVersion', () => {
const fname = path.join(__dirname, 'package.json')
const packageJson = JSON.parse(
readFileSync(path.join(__dirname, '../../package.json')).toString(),
)
jest.mock('fs', () => ({
copyFileSync: jest.fn(),
cpSync: jest.fn(),
mkdirSync: jest.fn(),
unlinkSync: jest.fn(),
writeFileSync: jest.fn(),
readFileSync: jest.fn(() =>
JSON.stringify({ dependencies: {}, devDependencies: {} }),
),
}))

describe('newAction', () => {
const mockExecSync = execSync as jest.MockedFunction<typeof execSync>
const mockCommand = new Command().name('new') // Mock command with name 'new'

beforeEach(() => {
copyFileSync(path.join(__dirname, '../../templates/package.json'), fname)
jest.clearAllMocks()
})

afterEach(() => {
unlinkSync(fname)
})
it('should generate a project with the latest version when version is not specified', async () => {
const projectName = 'test-project'
const latestVersion = '1.2.3'
mockExecSync
.mockReturnValueOnce(Buffer.from(latestVersion))
.mockReturnValue(Buffer.from(''))

it('it should update deps', () => {
useLatestPackageVersion(__dirname)
const tplPackageJson = JSON.parse(readFileSync(fname).toString())
await newAction(`${projectName}`, {}, mockCommand)

expect(packageJson.devDependencies['@mbc-cqrs-serverless/core']).toBe(
tplPackageJson.dependencies['@mbc-cqrs-serverless/core'],
expect(execSync).toHaveBeenCalledWith(
'npm view @mbc-cqrs-serverless/core dist-tags.latest',
)
expect(mkdirSync).toHaveBeenCalledWith(
path.join(process.cwd(), projectName),
{ recursive: true },
)
expect(packageJson.version).toBe(
tplPackageJson.devDependencies['@mbc-cqrs-serverless/cli'],
expect(cpSync).toHaveBeenCalledWith(
path.join(__dirname, '../../templates'),
path.join(process.cwd(), projectName),
{ recursive: true },
)
expect(copyFileSync).toHaveBeenCalledTimes(2) // For .gitignore and .env.local
expect(mockExecSync).toHaveBeenCalledWith('git init', {
cwd: path.join(process.cwd(), projectName),
})
expect(mockExecSync).toHaveBeenCalledWith('npm i', {
cwd: path.join(process.cwd(), projectName),
})
})

it('it should not update name', () => {
const { name } = JSON.parse(readFileSync(fname).toString())
it('should use a specific version if specified', async () => {
const projectName = 'test-project'
const version = '1.0.0'
const mockVersions = ['1.0.0', '1.1.0', '1.2.0']
mockExecSync
.mockReturnValueOnce(Buffer.from(JSON.stringify(mockVersions)))
.mockReturnValue(Buffer.from(''))

useLatestPackageVersion(__dirname)
const tplPackageJson = JSON.parse(readFileSync(fname).toString())
await newAction(`${projectName}@${version}`, {}, mockCommand)

expect(name).toBe(tplPackageJson.name)
expect(execSync).toHaveBeenCalledWith(
'npm view @mbc-cqrs-serverless/core versions --json',
)
expect(mkdirSync).toHaveBeenCalledWith(
path.join(process.cwd(), projectName),
{ recursive: true },
)
expect(cpSync).toHaveBeenCalledWith(
path.join(__dirname, '../../templates'),
path.join(process.cwd(), projectName),
{ recursive: true },
)
expect(copyFileSync).toHaveBeenCalledTimes(2) // For .gitignore and .env.local
expect(mockExecSync).toHaveBeenCalledWith('git init', {
cwd: path.join(process.cwd(), projectName),
})
expect(mockExecSync).toHaveBeenCalledWith('npm i', {
cwd: path.join(process.cwd(), projectName),
})
})

it('it should not update name with empty name', () => {
const { name } = JSON.parse(readFileSync(fname).toString())

useLatestPackageVersion(__dirname, '')
const tplPackageJson = JSON.parse(readFileSync(fname).toString())
it('should throw an error for an invalid version', async () => {
const projectName = 'test-project'
const invalidVersion = '2.0.0'
const mockVersions = ['1.0.0', '1.1.0', '1.2.0']
mockExecSync.mockReturnValueOnce(Buffer.from(JSON.stringify(mockVersions)))

expect(name).toBe(tplPackageJson.name)
})
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()

it('it should update name', () => {
const name = faker.word.sample()
useLatestPackageVersion(__dirname, name)
const tplPackageJson = JSON.parse(readFileSync(fname).toString())
await newAction(`${projectName}@${invalidVersion}`, {}, mockCommand)

expect(name).toBe(tplPackageJson.name)
expect(execSync).toHaveBeenCalledWith(
'npm view @mbc-cqrs-serverless/core versions --json',
)
expect(consoleSpy).toHaveBeenCalledWith(
'The specified package version does not exist. Please chose a valid version!\n',
mockVersions,
)
consoleSpy.mockRestore()
})
})
62 changes: 52 additions & 10 deletions packages/cli/src/actions/new.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,43 @@ export default async function newAction(
options: object,
command: Command,
) {
const [projectName, version = 'latest'] = name.split('@')

console.log(
`Executing command '${command.name()}' for application '${name}' with options '${JSON.stringify(
`Executing command '${command.name()}' for application '${projectName}' with options '${JSON.stringify(
options,
)}'`,
)

const destDir = path.join(process.cwd(), name)
let packageVersion

if (version === 'latest') {
packageVersion = `^${
getPackageVersion('@mbc-cqrs-serverless/core', true)[0]
}` // use the latest patch and minor versions
} else {
const versions = getPackageVersion('@mbc-cqrs-serverless/core')
const regex = new RegExp(`^${version}(?![0-9]).*$`) // start with version and not directly follow by a digit
const matchVersions = versions.filter((v) => regex.test(v))
if (versions.includes(version)) {
packageVersion = version // specific version
} else if (matchVersions.length !== 0) {
packageVersion = `^${matchVersions.at(-1)}` // use the patch and minor versions
} else {
console.log(
'The specified package version does not exist. Please chose a valid version!\n',
versions,
)
return
}
}

const destDir = path.join(process.cwd(), projectName)
console.log('Generating MBC cqrs serverless application in', destDir)
mkdirSync(destDir, { recursive: true })
cpSync(path.join(__dirname, '../../templates'), destDir, { recursive: true })

// upgrade package
useLatestPackageVersion(destDir, name)
usePackageVersion(destDir, packageVersion, projectName)

// mv gitignore .gitignore
const gitignore = path.join(destDir, 'gitignore')
Expand All @@ -47,27 +71,45 @@ export default async function newAction(
console.log(logs.toString())
}

function useLatestPackageVersion(destDir: string, name?: string) {
function usePackageVersion(
destDir: string,
packageVersion: string,
projectName?: string,
) {
const packageJson = JSON.parse(
readFileSync(path.join(__dirname, '../../package.json')).toString(),
)
const fname = path.join(destDir, 'package.json')
const tplPackageJson = JSON.parse(readFileSync(fname).toString())

if (name) {
tplPackageJson.name = name
if (projectName) {
tplPackageJson.name = projectName
}

tplPackageJson.dependencies['@mbc-cqrs-serverless/core'] =
packageJson.devDependencies['@mbc-cqrs-serverless/core']
tplPackageJson.dependencies['@mbc-cqrs-serverless/core'] = packageVersion
tplPackageJson.devDependencies['@mbc-cqrs-serverless/cli'] =
packageJson.version

writeFileSync(fname, JSON.stringify(tplPackageJson, null, 2))
}

function getPackageVersion(packageName: string, isLatest = false): string[] {
if (isLatest) {
const latestVersion = execSync(`npm view ${packageName} dist-tags.latest`)
.toString()
.trim()
return [latestVersion]
}

const versions = JSON.parse(
execSync(`npm view ${packageName} versions --json`).toString(),
) as string[]
return versions
}

export let exportsForTesting = {
useLatestPackageVersion,
usePackageVersion,
getPackageVersion,
}
if (process.env.NODE_ENV !== 'test') {
exportsForTesting = undefined
Expand Down
Loading

0 comments on commit 0cb0043

Please sign in to comment.