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
18 changes: 18 additions & 0 deletions .changeset/fix-cli-bundling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@kidd-cli/cli': patch
'@kidd-cli/core': patch
'@kidd-cli/config': patch
'@kidd-cli/utils': patch
'@kidd-cli/bundler': patch
---

fix(cli): bundle @kidd-cli/* deps so published CLI is self-contained

The published CLI had bare imports to workspace packages whose npm exports maps
were stale (renamed subpaths like `./loader` → `./utils`, `./fs` → `./node`).
Commands silently disappeared because the autoloader swallowed import errors.

- Bundle all `@kidd-cli/*` packages into CLI dist via `deps.alwaysBundle`
- Add `KIDD_DEBUG` env var support to surface autoload import failures
- Add integration test asserting all commands appear in `--help` output
- Republish all packages to sync npm exports maps with source
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@
"liquidjs": "catalog:",
"picocolors": "^1.1.1",
"react": "^19.2.4",
"tsdown": "catalog:",
"yaml": "^2.8.3",
"zod": "catalog:"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/react": "^19.2.14",
"tsdown": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
},
Expand Down
36 changes: 36 additions & 0 deletions packages/cli/src/cli.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { execFile } from 'node:child_process'
import { join } from 'node:path'

import { describe, expect, it } from 'vitest'

const CLI_BIN = join(import.meta.dirname, '..', 'bin', 'kidd.js')

const EXPECTED_COMMANDS = ['add', 'build', 'commands', 'dev', 'doctor', 'init', 'run', 'stories']

/**
* Run the CLI binary with the given args and return stdout.
*
* @param args - CLI arguments.
* @returns A promise resolving to the stdout string.
*/
function runCli(args: readonly string[]): Promise<string> {
return new Promise((resolve, reject) => {
execFile('node', [CLI_BIN, ...args], (error, stdout, stderr) => {
if (error) {
reject(new Error(`CLI exited with code ${String(error.code)}: ${stderr}`))
return
}
resolve(stdout)
})
})
}

describe('kidd CLI integration', () => {
it('should display all commands in --help output', async () => {
const output = await runCli(['--help'])

EXPECTED_COMMANDS.map((cmd) =>
expect(output, `missing command: ${cmd}`).toContain(`kidd ${cmd}`)
)
})
})
7 changes: 7 additions & 0 deletions packages/cli/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { defineConfig } from 'tsdown'

export default defineConfig({
clean: true,
deps: {
// Bundle @kidd-cli/* packages so the published CLI is self-contained and
// does not depend on workspace packages being published with correct exports.
alwaysBundle: [/^@kidd-cli\//],
// Keep packages with native bindings external — they cannot be inlined.
neverBundle: ['tsdown', /^@rolldown\//, /^rolldown/],
},
dts: true,
fixedExtension: false,
outDir: 'dist',
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/autoload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,28 @@ describe('autoload()', () => {
expect(result).toEqual({})
})

it('should log import errors when KIDD_DEBUG is enabled', async () => {
process.env.KIDD_DEBUG = 'true'
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined)

mockedReaddir.mockResolvedValue([makeDirent('broken.ts', true)] as unknown as Dirent[])

vi.doMock('/tmp/commands/broken.ts', () =>
Promise.reject(new Error('Module has no valid export'))
)

const result = await autoload({ dir: '/tmp/commands' })

expect(result).toEqual({})
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('[kidd] failed to import command from'),
expect.any(Error)
)

delete process.env.KIDD_DEBUG
warnSpy.mockRestore()
})

it('should ignore non-ts/js files', async () => {
mockedReaddir.mockResolvedValue([
makeDirent('readme.md', true),
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/autoload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isPlainObject, isString } from '@kidd-cli/utils/fp'
import { hasTag, withTag } from '@kidd-cli/utils/tag'
import { match } from 'ts-pattern'

import { isDebug } from './lib/debug.js'
import type { AutoloadOptions, Command, CommandMap } from './types/index.js'

const VALID_EXTENSIONS = new Set(['.ts', '.js', '.mjs', '.tsx', '.jsx'])
Expand Down Expand Up @@ -141,7 +142,10 @@ async function importCommand(filePath: string): Promise<Command | undefined> {
return mod.default
}
return undefined
} catch {
} catch (error: unknown) {
if (isDebug()) {
console.warn(`[kidd] failed to import command from ${filePath}:`, error)
}
return undefined
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { isCommandsConfig } from './command.js'
import { createRuntime, registerCommands } from './runtime/index.js'
import type { ErrorRef, ResolvedRef } from './runtime/index.js'


/**
* Bootstrap and run the CLI application.
*
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/lib/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Check whether debug mode is enabled via the `KIDD_DEBUG` environment variable.
*
* Truthy values: `"true"`, `"1"`
* Falsy values: `"false"`, `"0"`, `undefined`, `null`
*
* @returns `true` when `KIDD_DEBUG` is set to a truthy value.
*/
export function isDebug(): boolean {
const value = process.env.KIDD_DEBUG
return value === 'true' || value === '1'
}
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading