Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a CLI #50

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f81ac0c
feat!: setup oclif with a basic 'init' command
EmileRolley Sep 30, 2024
e398310
test: setup tests for the CLI
EmileRolley Oct 1, 2024
af5a39c
feat: install deps with the init command
EmileRolley Oct 1, 2024
66e5b6e
feat(commands/init): add the --pkg-manager flag
EmileRolley Oct 1, 2024
16081c3
ci: use bun to run tests files
EmileRolley Oct 1, 2024
8b035f0
fix(command/init): fix spinning in prod
EmileRolley Oct 1, 2024
b215bbe
feat(command/init): generate README and src/base.publicodes
EmileRolley Oct 1, 2024
9529f57
feat(command/init): ask to install deps
EmileRolley Oct 2, 2024
fb5e4c8
refactor(command): better error handling
EmileRolley Oct 2, 2024
5985901
refactor: typing nitpicks
EmileRolley Oct 2, 2024
32acb8e
refactor(command): better exitWithError API
EmileRolley Oct 2, 2024
bf5e641
feat(compilation): add resolveRuleTypes
EmileRolley Oct 2, 2024
9566a25
feat(command/compile): first version of the compile command
EmileRolley Oct 2, 2024
af207df
feat(command/init): add --yes flag
EmileRolley Oct 3, 2024
92baef5
refactor(command/init): try to have a simplier run function
EmileRolley Oct 3, 2024
bb364df
feat(command/init): setup test with vitest
EmileRolley Oct 3, 2024
ed49480
feat(command/compile): use publicodes object config from the package.…
EmileRolley Oct 24, 2024
853b44a
v1.3.0-0
EmileRolley Oct 24, 2024
5bce220
feat(command/compile): change default output dir and add comments for…
EmileRolley Oct 25, 2024
0bdc172
v1.3.0-1
EmileRolley Oct 25, 2024
3a3f986
nitpicks
EmileRolley Nov 15, 2024
7f15aa0
feat(command/compile): add RuleValue to the genereated d.ts file
EmileRolley Nov 18, 2024
af220eb
refactor(command/compile)!: serialized parsed rules instead of raw rules
EmileRolley Nov 18, 2024
e8ff6e9
v1.3.0-2
EmileRolley Nov 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bin/dev.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off

node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %*
6 changes: 6 additions & 0 deletions bin/dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bun

// eslint-disable-next-line n/shebang
import { execute } from '@oclif/core'

await execute({ development: true, dir: import.meta.url })
3 changes: 3 additions & 0 deletions bin/run.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off

node "%~dp0\run" %*
5 changes: 5 additions & 0 deletions bin/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import { execute } from '@oclif/core'

await execute({ dir: import.meta.url })
27 changes: 21 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "@publicodes/tools",
"version": "1.2.5",
"description": "A set of utility functions to build tools around Publicodes models",
"description": "A CLI tool for Publicodes",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsup",
"watch": "tsup --watch",
"clean": "rm -rf dist docs",
"test": "jest",
"test": "vitest --globals",
"docs": "typedoc",
"format": "prettier --write .",
"format:check": "prettier --check ."
Expand Down Expand Up @@ -39,7 +39,8 @@
}
},
"files": [
"dist"
"dist",
"bin"
],
"repository": {
"type": "git",
Expand All @@ -55,31 +56,39 @@
],
"author": "Emile Rolley <[email protected]>",
"license": "MIT",
"bin": {
"publicodes": "./bin/run.js"
},
"dependencies": {
"@clack/prompts": "^0.7.0",
"@oclif/core": "^4.0.23",
"@types/node": "^18.11.18",
"chalk": "^5.3.0",
"glob": "^10.4.1",
"path": "^0.12.7",
"publicodes": "^1.3.3",
"yaml": "^2.4.5"
},
"devDependencies": {
"@oclif/test": "^4.0.9",
"@types/jest": "^29.2.5",
"docdash": "^2.0.1",
"jest": "^29.4.1",
"prettier": "^3.0.0",
"ts-jest": "^29.0.4",
"ts-node": "^10.9.2",
"tsup": "^8.0.2",
"typedoc": "^0.24.8",
"typedoc-plugin-export-functions": "^1.0.0",
"typescript": "^4.9.4"
"typescript": "^4.9.4",
"vitest": "^2.1.1"
},
"tsup": {
"entry": [
"src/index.ts",
"src/optims/index.ts",
"src/compilation/index.ts",
"src/migration/index.ts"
"src/migration/index.ts",
"src/commands"
],
"format": [
"cjs",
Expand All @@ -90,6 +99,12 @@
"clean": true,
"cjsInterop": true
},
"oclif": {
"bin": "publicodes",
"commands": "./dist/commands",
"dirname": "publicodes",
"topicSeparator": ":"
},
"publishConfig": {
"access": "public"
}
Expand Down
83 changes: 83 additions & 0 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Args, Command, Flags, ux } from '@oclif/core'
import { exec } from 'node:child_process'
import * as p from '@clack/prompts'
import chalk from 'chalk'
import fs from 'fs'
import path from 'path'
import { basePackageJson, getPackageJson, PackageJson } from '../utils/pjson'

export default class Init extends Command {
static override args = {}

static override description = 'initialize a new project'

static override examples = ['<%= command.id %>']

static override flags = {}

public async run(): Promise<void> {
p.intro(chalk.bgHex('#2975d1')(' publicodes init '))

const pjson = getPackageJson()

if (pjson) {
p.log.info(`Updating existing ${chalk.bold('package.json')} file`)
updatePackageJson(pjson)
} else {
p.log.step(`Creating a new ${chalk.bold('package.json')} file`)
const pjson = await askPackageJsonInfo()
updatePackageJson(pjson)
}

p.outro('🚀 publicodes is ready to use!')
}
}

function updatePackageJson(pjson: PackageJson): void {
const packageJsonPath = path.join(process.cwd(), 'package.json')
const fullPjson = { ...basePackageJson, ...pjson }
try {
fs.writeFileSync(packageJsonPath, JSON.stringify(fullPjson, null, 2))
p.log.success(`${chalk.bold('package.json')} file written`)
} catch (error) {
p.cancel(
`An error occurred while writing the ${chalk.magenta('package.json')} file`,
)
process.exit(1)
}
}

function askPackageJsonInfo(): Promise<PackageJson> {
const currentDir = path.basename(process.cwd())

return p.group(
{
name: () =>
p.text({
message: 'Name',
defaultValue: currentDir,
placeholder: currentDir,
}),
description: () => p.text({ message: 'Description', defaultValue: '' }),
version: () =>
p.text({
message: 'Version',
defaultValue: '1.0.0',
placeholder: '1.0.0',
}),
author: () => p.text({ message: 'Author', defaultValue: '' }),
license: () =>
p.text({
message: 'License',
defaultValue: 'MIT',
placeholder: 'MIT',
}),
},
{
onCancel: () => {
p.cancel('init cancelled')
process.exit(1)
},
},
)
}
47 changes: 47 additions & 0 deletions src/utils/pjson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fs from 'fs'

export type PackageJson = {
name: string
version: string
description: string
main?: string
type?: string
types?: string
files?: string[]
repository?: {
url: string
type: string
}
author: string
license: string
scripts?: {
[key: string]: string
}
peerDependencies?: {
[key: string]: string
}
}

export const basePackageJson: PackageJson = {
name: '',
version: '1.0.0',
description: '',
author: '',
type: 'module',
main: 'dist/index.js',
types: 'dist/index.d.ts',
license: 'MIT',
files: ['dist'],
peerDependencies: {
// TODO: how to get the latest version of publicodes?
publicodes: '^1.5.1',
},
}

export function getPackageJson(): PackageJson | undefined {
try {
return JSON.parse(fs.readFileSync('package.json', 'utf8'))
} catch (error) {
return undefined
}
}
98 changes: 98 additions & 0 deletions test/cli-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
ExecException,
execSync,
ExecSyncOptionsWithBufferEncoding,
} from 'child_process'
import path, { join } from 'path'
import fs, { mkdtempSync } from 'fs'
import os from 'os'

export type ExecError = ExecException & { stderr: string; stdout: string }

export type ExecOptions = ExecSyncOptionsWithBufferEncoding & {
silent?: boolean
}

export type ExecResult = {
code: number
stdout?: string
stderr?: string
error?: ExecError
}

const TEST_DATA_DIR = path.join(process.cwd(), 'test', 'commands-data')

/**
* Utility test class to execute the CLI command
*/
export class CLIExecutor {
private binPath: string

constructor() {
const curentDir = process.cwd()
this.binPath =
process.platform === 'win32'
? path.join(curentDir, 'bin', 'dev.cmd')
: path.join(curentDir, 'bin', 'dev.js')
}

public execCommand(cmd: string, options?: ExecOptions): Promise<ExecResult> {
const silent = options?.silent ?? true
const command = `${this.binPath} ${cmd}`
const cwd = process.cwd()

return new Promise((resolve) => {
if (silent) {
try {
const r = execSync(command, { ...options, stdio: 'pipe', cwd })
Dismissed Show dismissed Hide dismissed
const stdout = r.toString()

resolve({ code: 0, stdout })
} catch (error) {
const err = error as ExecError
console.log(error)

resolve({
code: 1,
error: err,
stdout: err.stdout?.toString() ?? '',
stderr: err.stderr?.toString() ?? '',
})
}
} else {
execSync(command, { ...options, stdio: 'inherit', cwd })
Dismissed Show dismissed Hide dismissed
resolve({ code: 0 })
}
})
}
}

// On macOS, os.tmpdir() is not a real path:
// https://github.com/nodejs/node/issues/11422
const TMP_DIR = fs.realpathSync(os.tmpdir())

export async function runInDir(
dir: 'tmp' | string,
fn: () => Promise<void>,
): Promise<void> {
const baseCwd = process.cwd()
const ctd =
dir === 'tmp'
? mkdtempSync(path.join(TMP_DIR, 'publicodes-cli-test-'))
: path.join(TEST_DATA_DIR, dir)

if (!fs.existsSync(ctd)) {
fs.mkdirSync(ctd, { recursive: true })
}

process.chdir(ctd)

try {
await fn()
} finally {
process.chdir(baseCwd)
if (dir === 'tmp' && fs.existsSync(ctd)) {
fs.rmSync(ctd, { recursive: true })
}
}
}
16 changes: 16 additions & 0 deletions test/commands-data/yarn-init/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "yarn-init",
"version": "1.0.0",
"description": "",
"author": "",
"type": "module",
"main": "index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"files": [
"dist"
],
"peerDependencies": {
"publicodes": "^1.5.1"
}
}
13 changes: 13 additions & 0 deletions test/commands/base.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CLIExecutor, runInDir } from '../cli-utils'

describe('publicodes --help', () => {
it('should list all available commands', async () => {
const cli = new CLIExecutor()

runInDir('tmp', async () => {
const { stdout } = await cli.execCommand('--help')

expect(stdout).toContain('init initialize a new project')

Check failure on line 10 in test/commands/base.test.ts

View workflow job for this annotation

GitHub Actions / main

Unhandled error

AssertionError: expected '' to contain 'init initialize a new project' - Expected + Received - init initialize a new project ❯ test/commands/base.test.ts:10:22 ❯ processTicksAndRejections node:internal/process/task_queues:95:5 ❯ Module.runInDir test/cli-utils.ts:91:5 This error originated in "test/commands/base.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "test/commands/base.test.ts". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.
})
})
})
31 changes: 31 additions & 0 deletions test/commands/init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { execSync } from 'child_process'
import { CLIExecutor, runInDir } from '../cli-utils'
import fs from 'fs'

describe('publicodes init', () => {
it('should update existing package.json', async () => {
const cli = new CLIExecutor()

runInDir('tmp', async () => {
execSync('yarn init -y')
const { stdout } = await cli.execCommand('init')

expect(stdout).toContain('existing package.json file')

Check failure on line 13 in test/commands/init.test.ts

View workflow job for this annotation

GitHub Actions / main

Unhandled error

AssertionError: expected '' to contain 'existing package.json file' - Expected + Received - existing package.json file ❯ test/commands/init.test.ts:13:22 ❯ processTicksAndRejections node:internal/process/task_queues:95:5 ❯ Module.runInDir test/cli-utils.ts:91:5 This error originated in "test/commands/init.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "test/commands/init.test.ts". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.
expect(stdout).toContain('package.json file written')
expect(stdout).toContain('🚀 publicodes is ready to use!')
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
expect(packageJson).toMatchObject({
type: 'module',
main: 'dist/index.js',
types: 'dist/index.d.ts',
files: ['dist'],
peerDependencies: {
publicodes: '^1.5.1',
},
devDependencies: {
'@publicodes/tools': '^1.5.1',
},
})
})
})
})
Loading
Loading