Skip to content
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
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