Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions .changeset/fuzzy-bugs-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
127 changes: 127 additions & 0 deletions packages/bundler/src/build/bun-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { describe, expect, it, vi } from 'vitest'

vi.mock(import('node:module'), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
createRequire: vi.fn(() => ({
resolve: vi.fn((id: string) => {
if (id === '@kidd-cli/utils/tag') {
return '/resolved/tag/index.js'
}
if (id === '@kidd-cli/core') {
return '/resolved/core/dist/index.js'
}
return id
}),
})),
}
})

const { buildRunnerConfig } = await import('./bun-config.js')

function makeResolvedConfig(): Parameters<typeof buildRunnerConfig>[0]['config'] {
return {
entry: '/project/src/index.ts',
commands: '/project/src/commands',
buildOutDir: '/project/dist',
compileOutDir: '/project/dist/bin',
build: {
target: 'node18',
minify: true,
sourcemap: true,
external: ['pg'],
clean: true,
},
compile: {
targets: ['darwin-arm64' as const],
name: 'cli',
},
include: [],
cwd: '/project',
}
}

describe('buildRunnerConfig', () => {
it('should return a config with all required fields', () => {
const result = buildRunnerConfig({
config: makeResolvedConfig(),
version: '1.0.0',
compile: false,
})

expect(result.entry).toBe('/project/src/index.ts')
expect(result.outDir).toBe('/project/dist')
expect(result.commandsDir).toBe('/project/src/commands')
expect(result.minify).toBe(true)
expect(result.sourcemap).toBe(true)
expect(result.target).toBe('node18')
expect(result.compile).toBe(false)
expect(result.external).toEqual(['pg'])
})

it('should define __KIDD_VERSION__ when version is provided', () => {
const result = buildRunnerConfig({
config: makeResolvedConfig(),
version: '2.3.4',
compile: false,
})

expect(result.define).toEqual({ __KIDD_VERSION__: '"2.3.4"' })
})

it('should return empty define when version is undefined', () => {
const result = buildRunnerConfig({
config: makeResolvedConfig(),
version: undefined,
compile: false,
})

expect(result.define).toEqual({})
})

it('should resolve tag module path and core dist dir', () => {
const result = buildRunnerConfig({
config: makeResolvedConfig(),
version: '1.0.0',
compile: true,
})

expect(result.tagModulePath).toBe('/resolved/tag/index.js')
expect(result.coreDistDir).toBe('/resolved/core/dist')
})

it('should convert ALWAYS_BUNDLE regexes to source strings', () => {
const result = buildRunnerConfig({
config: makeResolvedConfig(),
version: '1.0.0',
compile: false,
})

expect(result.alwaysBundlePatterns.length).toBeGreaterThan(0)
expect(result.alwaysBundlePatterns.every((pattern) => typeof pattern === 'string')).toBe(true)
})

it('should include node builtins', () => {
const result = buildRunnerConfig({
config: makeResolvedConfig(),
version: '1.0.0',
compile: false,
})

expect(result.nodeBuiltins.length).toBeGreaterThan(0)
expect(result.nodeBuiltins).toContain('fs')
expect(result.nodeBuiltins).toContain('node:fs')
})

it('should include stub packages', () => {
const result = buildRunnerConfig({
config: makeResolvedConfig(),
version: '1.0.0',
compile: false,
})

expect(result.stubPackages.length).toBeGreaterThan(0)
expect(result.stubPackages).toContain('chokidar')
})
})
214 changes: 214 additions & 0 deletions packages/cli/src/commands/add/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import type { CommandContext } from '@kidd-cli/core'
import { beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock(import('../../lib/detect.js'), () => ({
detectProject: vi.fn(),
}))

vi.mock(import('../../lib/render.js'), () => ({
renderTemplate: vi.fn(),
}))

vi.mock(import('../../lib/write.js'), () => ({
writeFiles: vi.fn(),
}))

vi.mock(import('@kidd-cli/utils/manifest'), () => ({
readManifest: vi.fn(),
}))

const { detectProject } = await import('../../lib/detect.js')
const { renderTemplate } = await import('../../lib/render.js')
const { writeFiles } = await import('../../lib/write.js')
const { readManifest } = await import('@kidd-cli/utils/manifest')
const mockedDetectProject = vi.mocked(detectProject)
const mockedRenderTemplate = vi.mocked(renderTemplate)
const mockedWriteFiles = vi.mocked(writeFiles)
const mockedReadManifest = vi.mocked(readManifest)

const mod = await import('./config.js')

function makeContext(): CommandContext {
return {
args: {},
config: {},
fail: vi.fn((msg: string) => {
throw new Error(msg)
}) as never,
format: { json: vi.fn(() => ''), table: vi.fn(() => '') },
log: {
error: vi.fn(),
info: vi.fn(),
intro: vi.fn(),
message: vi.fn(),
newline: vi.fn(),
note: vi.fn(),
outro: vi.fn(),
raw: vi.fn(),
step: vi.fn(),
success: vi.fn(),
warn: vi.fn(),
},
prompts: {
confirm: vi.fn(),
multiselect: vi.fn(),
password: vi.fn(),
select: vi.fn(),
text: vi.fn(),
},
status: { spinner: { message: vi.fn(), start: vi.fn(), stop: vi.fn() } },
meta: { command: ['add', 'config'], name: 'kidd', version: '0.0.0' },
store: { clear: vi.fn(), delete: vi.fn(), get: vi.fn(), has: vi.fn(), set: vi.fn() },
} as unknown as CommandContext
}

describe('add config', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('should fail when detect returns an error', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([new Error('detect failed'), null])

await expect(mod.default.handler!(ctx)).rejects.toThrow('detect failed')
})

it('should fail when not in a kidd project', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([null, null])

await expect(mod.default.handler!(ctx)).rejects.toThrow('Not in a kidd project')
})

it('should resolve project name from package.json', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([
null,
{ commandsDir: null, hasKiddDep: true, rootDir: '/project' },
])
mockedReadManifest.mockResolvedValue([null, { name: 'my-cool-app', version: '1.0.0' }] as never)
mockedRenderTemplate.mockResolvedValue([null, [{ content: 'code', relativePath: 'config.ts' }]])
mockedWriteFiles.mockResolvedValue([null, { skipped: [], written: ['config.ts'] }])

await mod.default.handler!(ctx)

expect(mockedRenderTemplate).toHaveBeenCalledWith(
expect.objectContaining({
variables: { name: 'my-cool-app' },
})
)
})

it('should fall back to default name when readManifest fails', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([
null,
{ commandsDir: null, hasKiddDep: true, rootDir: '/project' },
])
mockedReadManifest.mockResolvedValue([new Error('no manifest'), null] as never)
mockedRenderTemplate.mockResolvedValue([null, [{ content: 'code', relativePath: 'config.ts' }]])
mockedWriteFiles.mockResolvedValue([null, { skipped: [], written: ['config.ts'] }])

await mod.default.handler!(ctx)

expect(mockedRenderTemplate).toHaveBeenCalledWith(
expect.objectContaining({
variables: { name: 'my-app' },
})
)
})

it('should fall back to default name when manifest has no name', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([
null,
{ commandsDir: null, hasKiddDep: true, rootDir: '/project' },
])
mockedReadManifest.mockResolvedValue([null, { version: '1.0.0' }] as never)
mockedRenderTemplate.mockResolvedValue([null, [{ content: 'code', relativePath: 'config.ts' }]])
mockedWriteFiles.mockResolvedValue([null, { skipped: [], written: ['config.ts'] }])

await mod.default.handler!(ctx)

expect(mockedRenderTemplate).toHaveBeenCalledWith(
expect.objectContaining({
variables: { name: 'my-app' },
})
)
})

it('should fail when render returns an error', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([
null,
{ commandsDir: null, hasKiddDep: true, rootDir: '/project' },
])
mockedReadManifest.mockResolvedValue([null, { name: 'app', version: '1.0.0' }] as never)
mockedRenderTemplate.mockResolvedValue([new Error('render failed'), null])

await expect(mod.default.handler!(ctx)).rejects.toThrow('render failed')
expect(ctx.status.spinner.stop).toHaveBeenCalledWith('Failed')
})

it('should fail when write returns an error', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([
null,
{ commandsDir: null, hasKiddDep: true, rootDir: '/project' },
])
mockedReadManifest.mockResolvedValue([null, { name: 'app', version: '1.0.0' }] as never)
mockedRenderTemplate.mockResolvedValue([null, [{ content: 'code', relativePath: 'config.ts' }]])
mockedWriteFiles.mockResolvedValue([new Error('write failed'), null])

await expect(mod.default.handler!(ctx)).rejects.toThrow('write failed')
expect(ctx.status.spinner.stop).toHaveBeenCalledWith('Failed')
})

it('should write to src directory', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([
null,
{ commandsDir: null, hasKiddDep: true, rootDir: '/project' },
])
mockedReadManifest.mockResolvedValue([null, { name: 'app', version: '1.0.0' }] as never)
mockedRenderTemplate.mockResolvedValue([null, [{ content: 'code', relativePath: 'config.ts' }]])
mockedWriteFiles.mockResolvedValue([null, { skipped: [], written: ['config.ts'] }])

await mod.default.handler!(ctx)

expect(mockedWriteFiles).toHaveBeenCalledWith(
expect.objectContaining({ outputDir: '/project/src' })
)
})

it('should display next steps after creation', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([
null,
{ commandsDir: null, hasKiddDep: true, rootDir: '/project' },
])
mockedReadManifest.mockResolvedValue([null, { name: 'app', version: '1.0.0' }] as never)
mockedRenderTemplate.mockResolvedValue([null, [{ content: 'code', relativePath: 'config.ts' }]])
mockedWriteFiles.mockResolvedValue([null, { skipped: [], written: ['config.ts'] }])

await mod.default.handler!(ctx)

expect(ctx.log.raw).toHaveBeenCalledWith('Next steps:')
})

it('should log skipped files', async () => {
const ctx = makeContext()
mockedDetectProject.mockResolvedValue([
null,
{ commandsDir: null, hasKiddDep: true, rootDir: '/project' },
])
mockedReadManifest.mockResolvedValue([null, { name: 'app', version: '1.0.0' }] as never)
mockedRenderTemplate.mockResolvedValue([null, [{ content: 'code', relativePath: 'config.ts' }]])
mockedWriteFiles.mockResolvedValue([null, { skipped: ['config.ts'], written: [] }])

await mod.default.handler!(ctx)

expect(ctx.log.raw).toHaveBeenCalledWith(' skipped config.ts (already exists)')
})
})
Loading
Loading