mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
The CommonJS pass rewrites a dynamic import into a require, which cannot take the file URL the ESM path hands it, so the load goes through here.ts and its .cjs sibling. create-gui reads its manifest from the package root at run time, names its relative imports with their extension, and imports opener instead of requiring it. Co-authored-by: Hanzo Dev <[email protected]>
261 lines
7.8 KiB
TypeScript
261 lines
7.8 KiB
TypeScript
import { type ChildProcess, execFileSync, spawn } from 'node:child_process'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { temporaryDirectory } from 'tempy'
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
import { templates } from '../src/templates'
|
|
|
|
// Both suites below scaffold from a template that lives in another repository,
|
|
// so they only mean anything when that repository answers. cloneStarter tries
|
|
// https and then ssh, so ask both the same way it does: if neither responds
|
|
// there is nothing to scaffold from, and failing here would report someone
|
|
// else's unreachable repo as a defect in this CLI.
|
|
const starterFree = templates.find((template) => template.value === 'starter-free')!
|
|
|
|
const answers = (url: string) => {
|
|
try {
|
|
execFileSync('git', ['ls-remote', '--heads', url, starterFree.repo.branch], {
|
|
stdio: 'ignore',
|
|
timeout: 20_000,
|
|
env: {
|
|
...process.env,
|
|
// a missing repo asks for credentials; never sit on that prompt
|
|
GIT_TERMINAL_PROMPT: '0',
|
|
GIT_SSH_COMMAND: 'ssh -oBatchMode=yes -oStrictHostKeyChecking=accept-new',
|
|
},
|
|
})
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const unreachable =
|
|
!answers(starterFree.repo.url) && !answers(starterFree.repo.sshFallback)
|
|
|
|
if (unreachable) {
|
|
console.warn(
|
|
`[create-gui] skipping the scaffold suites: ${starterFree.repo.url} answers over neither https nor ssh. ` +
|
|
`Set STARTER_FREE_REPO_SOURCE to a reachable clone to run them.`
|
|
)
|
|
}
|
|
|
|
describe.skipIf(unreachable)('create-gui CLI', () => {
|
|
let tempDir: string
|
|
let cli: ChildProcess
|
|
let projectName: string
|
|
let projectPath: string
|
|
let output: string
|
|
|
|
beforeAll(
|
|
async () => {
|
|
tempDir = temporaryDirectory()
|
|
projectName = 'test-project'
|
|
const cliPath = path.join(__dirname, '../dist/esm/index.js')
|
|
projectPath = path.join(tempDir, projectName)
|
|
|
|
console.info(`Running: node ${cliPath}`)
|
|
console.info(` in dir: ${tempDir}`)
|
|
console.info(` then entering: ${projectName}`)
|
|
|
|
cli = spawn('node', [cliPath], {
|
|
cwd: tempDir,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
})
|
|
|
|
output = ''
|
|
|
|
cli.stdout?.on('data', (data) => {
|
|
output += data.toString()
|
|
console.info(data.toString()) // Log output for debugging
|
|
})
|
|
|
|
cli.stderr?.on('data', (data) => {
|
|
output += data.toString()
|
|
console.error(`ERROR`, data.toString()) // Log errors for debugging
|
|
})
|
|
|
|
// Helper function to write input after a delay
|
|
const writeWithDelay = (input: string, delay: number) => {
|
|
return new Promise<void>((resolve) => {
|
|
setTimeout(() => {
|
|
cli.stdin?.write(input)
|
|
resolve()
|
|
}, delay)
|
|
})
|
|
}
|
|
|
|
// Simulate user input
|
|
await writeWithDelay(`${projectName}`, 300)
|
|
await writeWithDelay('\r', 300) // Enter
|
|
// Select starter-free template (6th option) - arrow down 5 times then enter
|
|
await writeWithDelay('\x1B[B', 300) // Arrow down
|
|
await writeWithDelay('\x1B[B', 300) // Arrow down
|
|
await writeWithDelay('\x1B[B', 300) // Arrow down
|
|
await writeWithDelay('\x1B[B', 300) // Arrow down
|
|
await writeWithDelay('\x1B[B', 300) // Arrow down
|
|
await writeWithDelay('\r', 500) // Enter
|
|
|
|
// Wait for the process to finish
|
|
await new Promise<void>((resolve, reject) => {
|
|
cli.on('exit', (code) => {
|
|
if (code === 0) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error(`CLI process exited with code ${code}`))
|
|
}
|
|
})
|
|
})
|
|
},
|
|
// timeout
|
|
120_000
|
|
)
|
|
|
|
afterAll(() => {
|
|
// Clean up the temporary directory after each test
|
|
if (tempDir && fs.existsSync(tempDir)) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('should create the project directory', () => {
|
|
expect(fs.existsSync(projectPath)).toBe(true)
|
|
})
|
|
|
|
it('should create essential files', () => {
|
|
const essentialFiles = [
|
|
'package.json',
|
|
'tsconfig.json',
|
|
'apps/expo/app.json',
|
|
// NOT hanzogui.config.ts. These are paths inside hanzogui/starter-free,
|
|
// a different repository, and the brand transform that renamed
|
|
// tamagui -> hanzogui in THIS repo rewrote this string as if it named a
|
|
// file here. It never reached the template, so the scaffold still lays
|
|
// down tamagui.config.ts and the assertion asked for a file that has
|
|
// never existed. Assert what the template actually ships.
|
|
'packages/config/src/tamagui.config.ts',
|
|
]
|
|
|
|
essentialFiles.forEach((file) => {
|
|
expect(fs.existsSync(path.join(projectPath, file))).toBe(true)
|
|
})
|
|
})
|
|
|
|
it('should prompt for project name', () => {
|
|
expect(output).toContain('Project name:')
|
|
})
|
|
|
|
it('should display the selected template', () => {
|
|
expect(output).toContain('Next + Expo')
|
|
})
|
|
|
|
it('should provide instructions to visit the project', () => {
|
|
expect(output).toContain('visit your project')
|
|
})
|
|
|
|
it('should not contain any errors', () => {
|
|
expect(output).not.toContain('Error:')
|
|
})
|
|
|
|
it('should indicate successful project creation', () => {
|
|
expect(output).toContain(`Done! created a new project`)
|
|
})
|
|
|
|
it('should display the project name', () => {
|
|
expect(output).toContain(`cd ${projectName}`)
|
|
})
|
|
|
|
it('should not contain any git errors', () => {
|
|
expect(output).not.toContain('fatal: not a git repository')
|
|
})
|
|
})
|
|
|
|
describe.skipIf(unreachable)('create-gui CLI with --template flag', () => {
|
|
let tempDir: string
|
|
let cli: ChildProcess
|
|
let projectName: string
|
|
let projectPath: string
|
|
let output: string
|
|
|
|
beforeAll(async () => {
|
|
tempDir = temporaryDirectory()
|
|
projectName = 'starter-free-project'
|
|
const cliPath = path.join(__dirname, '../dist/esm/index.js')
|
|
projectPath = path.join(tempDir, projectName)
|
|
|
|
cli = spawn('node', [cliPath, '--template', 'starter-free'], {
|
|
cwd: tempDir,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
})
|
|
|
|
output = ''
|
|
|
|
cli.stdout?.on('data', (data) => {
|
|
output += data.toString()
|
|
if (process.env.DEBUG === 'test') {
|
|
console.info(data.toString())
|
|
}
|
|
})
|
|
|
|
cli.stderr?.on('data', (data) => {
|
|
output += data.toString()
|
|
if (process.env.DEBUG === 'test') {
|
|
console.error(data.toString())
|
|
}
|
|
})
|
|
|
|
// Simulate user input for project name only
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 500))
|
|
cli.stdin?.write(`${projectName}`)
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 500))
|
|
cli.stdin?.write('\r')
|
|
|
|
// Wait for the process to finish
|
|
await new Promise<void>((resolve, reject) => {
|
|
cli.on('exit', (code) => {
|
|
if (code === 0) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error(`CLI process exited with code ${code}`))
|
|
}
|
|
})
|
|
})
|
|
}, 120_000) // 120 second timeout for the setup
|
|
|
|
afterAll(() => {
|
|
if (tempDir && fs.existsSync(tempDir)) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('should create the project directory', () => {
|
|
expect(fs.existsSync(projectPath)).toBe(true)
|
|
})
|
|
|
|
it('should skip the template picker step', () => {
|
|
expect(output).not.toContain('Pick a template:')
|
|
})
|
|
|
|
it('should create essential files for starter-free project', () => {
|
|
const essentialFiles = [
|
|
'package.json',
|
|
'tsconfig.json',
|
|
// the template's own filename — see the note on the suite above
|
|
'packages/config/src/tamagui.config.ts',
|
|
]
|
|
|
|
essentialFiles.forEach((file) => {
|
|
expect(fs.existsSync(path.join(projectPath, file))).toBe(true)
|
|
})
|
|
})
|
|
|
|
it('should indicate successful project creation', () => {
|
|
expect(output).toContain(`Done! created a new project`)
|
|
expect(output).toContain(`cd ${projectName}`)
|
|
})
|
|
|
|
it('should not contain any errors', () => {
|
|
expect(output).not.toContain('Error:')
|
|
})
|
|
})
|