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

Enhance Frontend CI with Testing #81

Merged
merged 5 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,20 @@ jobs:
with:
working-directory: ${{ env.working-directory }}
- run: pnpm lint

frontend-ci:
needs: [setup-job]
if: ${{ needs.setup-job.outputs.has-changes == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15

defaults:
run:
working-directory: ${{ env.working-directory }}

steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/pnpm-setup
with:
working-directory: ${{ env.working-directory }}
- run: pnpm test:turbo
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"lint:turbo": "turbo lint",
"lint:packageVersion": "syncpack list-mismatches",
"fmt:turbo": "turbo fmt",
"gen:turbo": "turbo gen"
"gen:turbo": "turbo gen",
"test:turbo": "turbo test"
},
"packageManager": "[email protected]+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b",
"devDependencies": {
Expand Down
6 changes: 6 additions & 0 deletions frontend/packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ $ liam erd preview
# Launches the web application for preview
```

## Test

```bash
pnpm run test
```

## Building and Installing the Standalone CLI for Development

To build the CLI for development purposes, run:
Expand Down
106 changes: 1 addition & 105 deletions frontend/packages/cli/bin/cli.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,3 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { Command } from 'commander'
import { build, createServer, preview } from 'vite'
import { program } from '../src/index.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const root = path.resolve(__dirname, '../..')
const publicDir = path.join(process.cwd(), 'public')
const outDir = path.join(process.cwd(), 'dist')

function runPreprocess(inputPath: string | null) {
if (inputPath && !fs.existsSync(inputPath)) {
throw new Error('Invalid input path. Please provide a valid .sql file.')
}

const sqlContent = inputPath ? fs.readFileSync(inputPath, 'utf8') : '{}'
const filePath = path.join(publicDir, 'schema.json')

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

try {
const jsonContent = JSON.stringify({ sql: sqlContent }, null, 2)
fs.writeFileSync(filePath, jsonContent, 'utf8')
return filePath
} catch (error) {
console.error(
`Error during preprocessing: ${error instanceof Error ? error.message : 'Unknown error'}`,
)
return null
}
}

const program = new Command()

program.name('liam').description('CLI tool for Liam').version('0.0.0')

const erdCommand = new Command('erd').description('ERD commands')

program.addCommand(erdCommand)

erdCommand
.command('build')
.description('Run Vite build')
.option('--input <path>', 'Path to the .sql file')
.action(async (options) => {
try {
const inputPath = options.input
runPreprocess(inputPath)
await build({
publicDir,
root,
build: {
outDir,
emptyOutDir: false,
},
})
} catch (error) {
console.error('Build failed:', error)
process.exit(1)
}
})

erdCommand
.command('dev')
.description('Run Vite dev server')
.option('--input <path>', 'Path to the .sql file')
.action(async (options) => {
try {
const inputPath = options.input
runPreprocess(inputPath)
const server = await createServer({ publicDir, root })
const address = server.httpServer?.address()
const port = typeof address === 'object' && address ? address.port : 5173
console.info(`Dev server is running at http://localhost:${port}`)
await server.listen()
} catch (error) {
console.error('Failed to start dev server:', error)
process.exit(1)
}
})

erdCommand
.command('preview')
.description('Preview the production build')
.action(async () => {
try {
const previewServer = await preview({
publicDir,
root,
build: {
outDir,
emptyOutDir: false,
},
})
const address = previewServer.httpServer?.address()
const port = typeof address === 'object' && address ? address.port : 4173
console.info(`Preview server is running at http://localhost:${port}`)
} catch (error) {
console.error('Failed to start preview server:', error)
process.exit(1)
}
})
program.parse(process.argv)
6 changes: 4 additions & 2 deletions frontend/packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"lint:biome": "biome check .",
"lint:tsc": "tsc --noEmit",
"fmt": "conc -c auto pnpm:fmt:*",
"fmt:biome": "biome check --write --unsafe ."
"fmt:biome": "biome check --write --unsafe .",
"test": "pnpm vitest"
},
"dependencies": {
"@liam/erd-core": "workspace:*",
Expand All @@ -36,6 +37,7 @@
"@types/react-dom": "^18",
"@vitejs/plugin-react": "^4.3.3",
"typed-css-modules": "^0.9.1",
"typescript": "^5"
"typescript": "^5",
"vitest": "^2.1.4"
}
}
19 changes: 19 additions & 0 deletions frontend/packages/cli/src/cli/commands/buildCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { build } from 'vite'
import { runPreprocess } from '../runPreprocess.js'

export const buildCommand = async (
inputPath: string,
publicDir: string,
root: string,
outDir: string,
) => {
runPreprocess(inputPath, publicDir)
await build({
publicDir,
root,
build: {
outDir,
emptyOutDir: false,
},
})
}
15 changes: 15 additions & 0 deletions frontend/packages/cli/src/cli/commands/devCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createServer } from 'vite'
import { runPreprocess } from '../runPreprocess.js'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nits]
Could we write it like this to make it more concise?

Suggested change
import { runPreprocess } from '../runPreprocess.js'
import { runPreprocess } from '../runPreprocess'


export const devCommand = async (
inputPath: string,
publicDir: string,
root: string,
) => {
runPreprocess(inputPath, publicDir)
const server = await createServer({ publicDir, root })
const address = server.httpServer?.address()
const port = typeof address === 'object' && address ? address.port : 5173
console.info(`Dev server is running at http://localhost:${port}`)
await server.listen()
}
4 changes: 4 additions & 0 deletions frontend/packages/cli/src/cli/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { buildCommand } from './buildCommand.js'
import { devCommand } from './devCommand.js'
import { previewCommand } from './previewCommand.js'
export { buildCommand, devCommand, previewCommand }
Comment on lines +1 to +4
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nits]
Could we write it like this to make it more concise?

Suggested change
import { buildCommand } from './buildCommand.js'
import { devCommand } from './devCommand.js'
import { previewCommand } from './previewCommand.js'
export { buildCommand, devCommand, previewCommand }
export { buildCommand } from './buildCommand'
export { devCommand } from './devCommand'
export { previewCommand } from './previewCommand'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's actually been a bit of a headache for me since yesterday! At the moment, removing the .js extension causes the CLI to not work correctly. 😭 😭

error
[cli]$ rm -rf dist dist-cli/ node_modules/.tmp

[cli]$ pnpm run build:cli

> @liam/[email protected] build:cli /Users/hoshino/.local/share/go/src/github.com/liam-hq/liam/frontend/packages/cli
> tsc -p tsconfig.node.json

[cli]$
[cli]$ pnpm link --global

[cli]$ which liam
/Users/hoshino/.local/share/pnpm/liam

[cli]$ liam erd help
node:internal/modules/esm/resolve:265
    throw new ERR_MODULE_NOT_FOUND(
          ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Users/hoshino/.local/share/go/src/github.com/liam-hq/liam/frontend/packages/cli/dist-cli/src/cli/commands/buildCommand' imported from /Users/hoshino/.local/share/go/src/github.com/liam-hq/liam/frontend/packages/cli/dist-cli/src/cli/commands/index.js
    at finalizeResolution (node:internal/modules/esm/resolve:265:11)
    at moduleResolve (node:internal/modules/esm/resolve:933:10)
    at defaultResolve (node:internal/modules/esm/resolve:1157:11)
    at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:390:12)
    at ModuleLoader.resolve (node:internal/modules/esm/loader:359:25)
    at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:234:38)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:87:39)
    at link (node:internal/modules/esm/module_job:86:36) {
  code: 'ERR_MODULE_NOT_FOUND',
  url: 'file:///Users/hoshino/.local/share/go/src/github.com/liam-hq/liam/frontend/packages/cli/dist-cli/src/cli/commands/buildCommand'
}

Node.js v20.12.0

So, I can only confirm that it functions properly in its current state, with the .js extension included. It’s not ideal, but it's a workaround that keeps things running for now.

To explain a bit further, if I change tsconfig.node.json from "module": "ESNext" to "CommonJS" and remove "type": "module" from package.json, the CLI would indeed work without the .js extension. However, doing so triggers a warning: The CJS build of Vite's Node API is deprecated. See https://vite.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.

I’m hoping to get some help with this issue from someone else in the future. Also, since this is a rather fragile part of the setup, I’m considering adding a smoke test to ensure the CLI continues to work properly.

Hope that gives some clarity, and thank you for the suggestion!

19 changes: 19 additions & 0 deletions frontend/packages/cli/src/cli/commands/previewCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { preview } from 'vite'

export const previewCommand = async (
publicDir: string,
root: string,
outDir: string,
) => {
const previewServer = await preview({
publicDir,
root,
build: {
outDir,
emptyOutDir: false,
},
})
const address = previewServer.httpServer?.address()
const port = typeof address === 'object' && address ? address.port : 4173
console.info(`Preview server is running at http://localhost:${port}`)
}
86 changes: 86 additions & 0 deletions frontend/packages/cli/src/cli/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { Command } from 'commander'
import { describe, expect, it, vi } from 'vitest'
import { program } from '.'
import { buildCommand, devCommand, previewCommand } from './commands'

// Function to set up mocks
function setupMocks() {
vi.mock('./commands/buildCommand', () => ({
buildCommand: vi.fn(),
}))
vi.mock('./commands/devCommand', () => ({
devCommand: vi.fn(),
}))
vi.mock('./commands/previewCommand', () => ({
previewCommand: vi.fn(),
}))
}

// Utility function to find a subcommand
function findSubCommand(erdCommand: Command | undefined, name: string) {
return erdCommand?.commands.find((cmd: Command) => cmd.name() === name)
}

setupMocks()

describe('program', () => {
it('should have the correct name and description', () => {
expect(program.name()).toBe('liam')
expect(program.description()).toBe('CLI tool for Liam')
})

it('should have an "erd" command with subcommands', () => {
const erdCommand = program.commands.find((cmd) => cmd.name() === 'erd')
expect(erdCommand).toBeDefined()
expect(erdCommand?.description()).toBe('ERD commands')

// Verify subcommands
const buildSubCommand = findSubCommand(erdCommand, 'build')
expect(buildSubCommand).toBeDefined()
expect(buildSubCommand?.description()).toBe('Run Vite build')

const devSubCommand = findSubCommand(erdCommand, 'dev')
expect(devSubCommand).toBeDefined()
expect(devSubCommand?.description()).toBe('Run Vite dev server')

const previewSubCommand = findSubCommand(erdCommand, 'preview')
expect(previewSubCommand).toBeDefined()
expect(previewSubCommand?.description()).toBe(
'Preview the production build',
)
})

describe('commands', () => {
it('should call buildCommand when "build" command is executed', () => {
program.parse(['erd', 'build', '--input', 'path/to/file.sql'], {
from: 'user',
})
expect(buildCommand).toHaveBeenCalledWith(
'path/to/file.sql',
expect.stringMatching(/\/public$/),
expect.any(String),
expect.stringMatching(/\/dist$/),
)
})

it('should call devCommand when "dev" command is executed', () => {
program.parse(['erd', 'dev', '--input', 'path/to/file.sql'], {
from: 'user',
})
expect(devCommand).toHaveBeenCalledWith(
'path/to/file.sql',
expect.stringMatching(/\/public$/),
expect.any(String),
)
})

it('should call previewCommand when "preview" command is executed', () => {
program.parse(['erd', 'preview'], { from: 'user' })
expect(previewCommand).toHaveBeenCalledWith(
expect.stringMatching(/\/public$/),
expect.any(String),
expect.stringMatching(/\/dist$/),
)
})
})
})
36 changes: 36 additions & 0 deletions frontend/packages/cli/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { Command } from 'commander'
import { buildCommand, devCommand, previewCommand } from './commands/index.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const root = path.resolve(__dirname, '../../..')
const publicDir = path.join(process.cwd(), 'public')
const outDir = path.join(process.cwd(), 'dist')

const program = new Command()

program.name('liam').description('CLI tool for Liam').version('0.0.0')

const erdCommand = new Command('erd').description('ERD commands')
program.addCommand(erdCommand)

erdCommand
.command('build')
.description('Run Vite build')
.option('--input <path>', 'Path to the .sql file')
.action((options) => buildCommand(options.input, publicDir, root, outDir))

erdCommand
.command('dev')
.description('Run Vite dev server')
.option('--input <path>', 'Path to the .sql file')
.action((options) => devCommand(options.input, publicDir, root))

erdCommand
.command('preview')
.description('Preview the production build')
.action(() => previewCommand(publicDir, root, outDir))

export { program }
Loading
Loading