diff --git a/.changeset/fix-cli-bundling.md b/.changeset/fix-cli-bundling.md new file mode 100644 index 00000000..4b146b6f --- /dev/null +++ b/.changeset/fix-cli-bundling.md @@ -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 diff --git a/packages/cli/package.json b/packages/cli/package.json index 58a30d64..7c4ef122 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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:" }, diff --git a/packages/cli/src/cli.integration.test.ts b/packages/cli/src/cli.integration.test.ts new file mode 100644 index 00000000..24d3c0d7 --- /dev/null +++ b/packages/cli/src/cli.integration.test.ts @@ -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 { + 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}`) + ) + }) +}) diff --git a/packages/cli/tsdown.config.ts b/packages/cli/tsdown.config.ts index e1d0c30d..507dff8d 100644 --- a/packages/cli/tsdown.config.ts +++ b/packages/cli/tsdown.config.ts @@ -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', diff --git a/packages/core/src/autoload.test.ts b/packages/core/src/autoload.test.ts index abfc115a..15cadd66 100644 --- a/packages/core/src/autoload.test.ts +++ b/packages/core/src/autoload.test.ts @@ -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), diff --git a/packages/core/src/autoload.ts b/packages/core/src/autoload.ts index 725d2a33..4eebdcdd 100644 --- a/packages/core/src/autoload.ts +++ b/packages/core/src/autoload.ts @@ -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']) @@ -141,7 +142,10 @@ async function importCommand(filePath: string): Promise { return mod.default } return undefined - } catch { + } catch (error: unknown) { + if (isDebug()) { + console.warn(`[kidd] failed to import command from ${filePath}:`, error) + } return undefined } } diff --git a/packages/core/src/cli.ts b/packages/core/src/cli.ts index 38f5eed7..e25db6b7 100644 --- a/packages/core/src/cli.ts +++ b/packages/core/src/cli.ts @@ -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. * diff --git a/packages/core/src/lib/debug.ts b/packages/core/src/lib/debug.ts new file mode 100644 index 00000000..337a5bd5 --- /dev/null +++ b/packages/core/src/lib/debug.ts @@ -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' +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32089d15..227f435c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -319,6 +319,9 @@ importers: react: specifier: ^19.2.4 version: 19.2.4 + tsdown: + specifier: 'catalog:' + version: 0.21.7(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@typescript/native-preview@7.0.0-dev.20260401.1)(typescript@6.0.2) yaml: specifier: ^2.8.3 version: 2.8.3 @@ -332,9 +335,6 @@ importers: '@types/react': specifier: ^19.2.14 version: 19.2.14 - tsdown: - specifier: 'catalog:' - version: 0.21.7(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@typescript/native-preview@7.0.0-dev.20260401.1)(typescript@6.0.2) typescript: specifier: 'catalog:' version: 6.0.2