Skip to content

refactor(bundler): revert Bun.build, restore tsdown with factory API#144

Merged
zrosenbauer merged 12 commits intomainfrom
refactor/bundler-tsdown-migration
Apr 2, 2026
Merged

refactor(bundler): revert Bun.build, restore tsdown with factory API#144
zrosenbauer merged 12 commits intomainfrom
refactor/bundler-tsdown-migration

Conversation

@zrosenbauer
Copy link
Copy Markdown
Member

Summary

  • Revert Bun.build migration — restore tsdown as the bundler (tsdown → index.mjs → bun build --compile)
  • Factory APIcreateBundler({ config, cwd, ...lifecycle }) replaces standalone build/watch/compile exports
  • Lifecycle hooksonStart, onFinish, onStepStart, onStepFinish with phase/step/meta events
  • Thin public API — one function (createBundler) + 4 types
  • Shared node utils@kidd-cli/utils/node with fs.* and process.* async wrappers
  • Config restructure@kidd-cli/config/utils subpath, compileTargets single source of truth with { target, label, default }
  • Manifest read once — version + binary name resolved in factory, flow through config
  • ResultAsync replaces AsyncResult across all packages

Files changed

82 files, +1647 / -2190 (net -543 lines)

Deleted

  • bun-runner.ts, bun-config.ts, plugins.ts — Bun.build architecture
  • read-version.ts — manifest read once in factory instead
  • detect-build-entry.test.ts — merged into resolve-build-entry.test.ts

Created

  • bundler.tscreateBundler factory with lifecycle hooks
  • utils/resolve-build-entry.ts — async entry detection
  • utils/node/fs.tsexists, read, write, list, mkdir, remove
  • utils/node/process.tsexec, spawn, exists
  • config/utils/compile.tscompileTargets array, CompileTarget type
  • config/utils/index.ts — barrel for internal config exports

Test plan

  • pnpm check passes (typecheck + lint + format)
  • pnpm test --filter='./packages/*' — all package tests pass
  • kidd build produces correct output in examples
  • kidd build --compile produces standalone binaries
  • kidd dev watch mode works

zrosenbauer and others added 2 commits April 1, 2026 14:37
…red utils

- Revert Bun.build migration — restore tsdown as the bundler
- Factory pattern: `createBundler({ config, cwd, ...lifecycle })`
- Lifecycle hooks: onStart, onFinish, onStepStart, onStepFinish
- Thin public API: `createBundler` + 4 types
- Create `@kidd-cli/utils/node` with `fs.*` and `process.*` wrappers
- Create `@kidd-cli/config/utils` subpath for internal exports
- Single source of truth: `compileTargets` array with target/label/default
- Binary name derived from config → package.json name → 'cli' fallback
- Manifest read once in factory, version + binary name flow through config
- `ResultAsync` replaces `AsyncResult` across all packages
- Remove bun-runner.ts, bun-config.ts, plugins.ts, read-version.ts

Co-Authored-By: Claude <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 2, 2026

🦋 Changeset detected

Latest commit: 49b3a0b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@kidd-cli/bundler Minor
@kidd-cli/core Minor
@kidd-cli/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
oss-kidd Ignored Ignored Preview Apr 2, 2026 10:06pm

Request Review

- Move clean.ts from build/ to utils/
- Remove isCompiledBinary — was a bad heuristic matching any extensionless file
- Clean now computes exact binary filenames from resolved compile name + targets
- Remove isBuildArtifact export (private helper)
- Remove CleanResult export (private type)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces Bun-based bundler internals with tsdown/Rolldown: removes bun-config, bun-runner, Bun plugins, and Bun-specific build/watch/compile flows; adds tsdown build/watch config generators, Rolldown-style autoload plugin (transform/resolveId/load), a createBundler factory with lifecycle hooks, async clean/resolveBuildEntry utilities, and many test updates. Moves filesystem/process helpers to @kidd-cli/utils/node and renames AsyncResult → ResultAsync across packages. Bundler package public exports and several compile/watch APIs were narrowed/renamed.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor(bundler): revert Bun.build, restore tsdown with factory API' accurately summarizes the primary change: reverting from Bun.build back to tsdown while introducing a new factory-based API.
Description check ✅ Passed The description comprehensively covers the changeset: it explains the revert to tsdown, introduces the factory API pattern, documents lifecycle hooks, lists deleted/created files, and includes a concrete test plan.
Docstring Coverage ✅ Passed Docstring coverage is 92.06% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/bundler-tsdown-migration

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/cli/src/lib/detect.ts (1)

87-99: ⚠️ Potential issue | 🟠 Major

Validate the parsed package.json before casting.

jsonParse() only proves the file is syntactically valid JSON. Values like null or { "dependencies": "x" } still pass through Line 99, and detectProject() will later crash on pkg.dependencies / '@kidd-cli/core' in deps instead of returning a read_error. Reject non-object payloads here.

Suggested fix
+import { z } from 'zod'
+
 import { ok } from '@kidd-cli/utils/fp'
 import type { ResultAsync } from '@kidd-cli/utils/fp'
 import { fs } from '@kidd-cli/utils/node'
 import { jsonParse } from '@kidd-cli/utils/json'
@@
 interface PackageJson {
   readonly dependencies?: Record<string, string>
   readonly devDependencies?: Record<string, string>
 }
+
+const packageJsonSchema = z.object({
+  dependencies: z.record(z.string(), z.string()).optional(),
+  devDependencies: z.record(z.string(), z.string()).optional(),
+})
@@
   const [parseError, data] = jsonParse(content)
   if (parseError) {
     return [
       {
         message: `Failed to parse package.json: ${parseError.message}`,
@@
       null,
     ]
   }
 
-  return ok(data as PackageJson)
+  const validated = packageJsonSchema.safeParse(data)
+  if (!validated.success) {
+    return [
+      {
+        message: `Failed to parse package.json: ${validated.error.message}`,
+        path: filePath,
+        type: 'read_error' as const,
+      },
+      null,
+    ]
+  }
+
+  return ok(validated.data)
 }

As per coding guidelines, "Always use Zod for validation at all boundaries (config, CLI args, external data)" from contributing/standards/typescript/coding-style.md.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/lib/detect.ts` around lines 87 - 99, The parsed JSON from
jsonParse (variables parseError and data) must be validated with Zod before
casting to PackageJson; update the block in detect.ts to run a Zod schema (or
z.object({...}) for the PackageJson shape) against data and if validation fails
return the same read_error shape (include validation errors in the message and
filePath) instead of blindly returning ok(data as PackageJson); ensure you
reference jsonParse, parseError, data, ok(), and PackageJson when implementing
the validation and error-return path.
packages/bundler/src/compile/compile.ts (1)

109-124: ⚠️ Potential issue | 🟠 Major

Always emit onStepFinish for a started target.

A failed compileSingleTarget() currently skips onStepFinish. That breaks the new start/finish lifecycle contract and leaves status/task consumers with an open target step whenever a compile target fails.

Suggested fix
-    if (result[0] === null && params.lifecycle.onStepFinish) {
+    if (params.lifecycle.onStepFinish) {
       await params.lifecycle.onStepFinish({ phase: 'compile', step: 'target', meta })
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bundler/src/compile/compile.ts` around lines 109 - 124, The code
calls params.lifecycle.onStepFinish only when compileSingleTarget returns
success, leaving the lifecycle open on failures; fix by ensuring
params.lifecycle.onStepFinish is always invoked for a started target: wrap the
compileSingleTarget call in a try/finally (or ensure a finally block) and in the
finally call params.lifecycle.onStepFinish({ phase: 'compile', step: 'target',
meta }) if params.lifecycle.onStepFinish is defined (keeping existing
onStepStart handling), so compileSingleTarget, params.lifecycle.onStepStart, and
params.lifecycle.onStepFinish are used as the points to locate changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bundler/src/autoloader/autoload-plugin.ts`:
- Around line 41-51: The load() hook currently regenerates the virtual module
for RESOLVED_VIRTUAL_ID using scanCommandsDir(params.commandsDir) but doesn't
tell Rollup to watch the commands directory; call the plugin context's
addWatchFile(params.commandsDir) when id === RESOLVED_VIRTUAL_ID (inside
load()), before scanning/generating, so changes in params.commandsDir trigger
rebuilds; ensure you reference the same PluginContext used by the plugin (where
load is defined) so addWatchFile is available, then continue to call
scanCommandsDir(...) and generateStaticAutoloader(...) as before.

In `@packages/bundler/src/build/build.ts`:
- Around line 23-25: The cleanup step is performed outside the
AsyncBundlerResult flow and may fail silently — modify the build function so
that the call to clean({ compile: params.compile, outDir:
params.resolved.buildOutDir }) is executed and its failure is handled using the
project's Result/ResultAsync pattern (not throw), short-circuiting the function
and returning a failure AsyncBundlerResult if cleanup fails; only call
tsdownBuild() (and continue the normal success flow) when the clean Result
indicates success. Ensure you update the code paths around
params.resolved.build.clean, clean(), and tsdownBuild() to propagate the Result
type instead of throwing so callers receive a consistent AsyncBundlerResult on
cleanup errors.

In `@packages/bundler/src/build/clean.ts`:
- Around line 69-71: The current code awaits fs.remove inside the isArtifact
branch and unconditionally returns { type: 'removed', name } which can misreport
failures; wrap the await fs.remove(join(params.outDir, name)) in a try/catch,
only return { type: 'removed', name } on success, and on failure return or
propagate a failure payload (e.g., { type: 'error', name, error }) or rethrow
the caught error so callers can detect the failure; reference the isArtifact
branch, fs.remove call, join(params.outDir, name) and the existing return shape
to locate where to add the try/catch (or alternatively add a clear comment if
best-effort cleanup is intended).

In `@packages/bundler/src/build/config.test.ts`:
- Around line 27-108: Add a new test that calls toTsdownBuildConfig({ config,
version: undefined, compile: true }) and asserts the compile branch: check the
returned object’s deps.alwaysBundle contains the expected compile-time externals
(e.g., includes 'pg' and NODE_BUILTINS items) and that the returned plugins
array contains the compile stub plugin (assert presence in result.plugins or by
plugin.name). Use the existing test suite helpers and the toTsdownBuildConfig,
deps, alwaysBundle and plugins symbols to locate and add this assertion.

In `@packages/bundler/src/compile/compile.ts`:
- Around line 225-233: The match branch for 'linux-x64-musl' in the target
mapping inside compile.ts incorrectly returns 'bun-linux-x64' (glibc); update
the mapping in the function that builds compile targets (the match(...)
expression) so the 'linux-x64-musl' case returns 'bun-linux-x64-musl' instead,
leaving all other branches unchanged.

In `@packages/bundler/src/types.ts`:
- Around line 83-87: The lifecycle hooks on BundlerLifecycle (onStart, onFinish,
onStepStart, onStepFinish) must not be allowed to throw and bypass
AsyncBundlerResult; either change their signatures to return Result<void, E> |
ResultAsync<void, E> (or Promise<Result<void,E>>) or, if keeping current
signatures, update every call site (e.g., in createBundler, compile, and
build/watch invocation paths) to wrap calls to these hooks in try/catch and
convert any thrown/rejected error into the AsyncBundlerResult tuple ([error,
null]) so hook failures are folded into the Result path rather than rejecting
the promise.

In `@packages/bundler/src/utils/resolve-build-entry.ts`:
- Around line 27-32: Replace the two-branch if/return with a ts-pattern match:
import match from "ts-pattern" and use match(found).with({ found: true }, () =>
found.path).otherwise(() => undefined) (or equivalent pattern) to return the
path or undefined; update the code that currently uses results.find((r) =>
r.found) and the following if (found) return found.path to use this pattern
match instead (target the variable found and the function resolve-build-entry).

In `@packages/cli/src/commands/build.ts`:
- Around line 45-47: The ternary used to set mergedConfig should be rewritten
using an explicit if/else or ts-pattern match to satisfy the no-ternary rule:
locate the assignment to mergedConfig (currently using shouldCompile ?
mergeCompileTargets({ config, targets: ctx.args.targets }) : config) and replace
it with an if (shouldCompile) { mergedConfig = mergeCompileTargets({ config,
targets: ctx.args.targets }) } else { mergedConfig = config } or equivalently
use match(shouldCompile).with(true, () => mergeCompileTargets(...)).with(false,
() => config).run(); keep the same variables and call to mergeCompileTargets and
ensure mergedConfig is declared/initialized before the conditional.

In `@packages/config/package.json`:
- Around line 32-35: The package.json exports removed the public subpath
"./loader" which breaks consumers; restore a compatibility alias by adding an
exports entry for "./loader" that points to the compiled implementation (e.g.,
"./dist/loader.js") and its types (e.g., "./dist/loader.d.ts") so existing
imports of "@kidd-cli/config/loader" continue to work, or if you intend a
breaking change, clearly version-bump to a major release and document the
removal.

In `@packages/utils/src/index.ts`:
- Around line 2-3: Reintroduce a compatibility bridge in
packages/utils/src/index.ts by re-exporting the old identifiers clients may rely
on: add type alias export for the removed AsyncResult (e.g., export type {
ResultAsync as AsyncResult } from './fp/index.js') and re-export the filesystem
symbols under their previous root names (e.g., export { exists, list, read,
remove, write } from './node/fs.js' or provide legacy aliases if names changed),
and mark these exports with a short deprecation comment so consumers can migrate
before removal; ensure the bridge keeps the existing new exports (Result,
ResultAsync) intact and does not add new runtime dependencies.

In `@packages/utils/src/node/process.ts`:
- Around line 52-56: The spawn function currently conflates process launch
errors with a normal exit code of 1; update spawn to use the Result tuple
pattern required by the project (e.g., Promise<[number | null, Error | null]>),
wire the child process 'error' event to return [null, err] and the
'exit'/'close' handler to return [exitCode, null], and adjust the function body
around the spawn(...) call and handlers (the spawn function and its internal
'error'/'exit' handlers) so that actual launch failures propagate the Error in
the tuple instead of returning 1; also update any callers of spawn to handle the
[data, error] tuple.
- Line 26: The exported function exec currently uses two positional parameters
(cmd: string, args: readonly string[] = []) which violates the repo rule that
functions with 2+ params must accept an object; change exec to accept a single
object parameter (e.g., { cmd: string, args?: readonly string[] }) preserving
the default empty array behavior and return type ResultAsync<ExecOutput>, update
all internal callers to pass an object (e.g., exec({ cmd, args })) and adjust
any related type imports/usages to match the new signature, and run/adjust tests
or usages that referenced the old positional signature.

---

Outside diff comments:
In `@packages/bundler/src/compile/compile.ts`:
- Around line 109-124: The code calls params.lifecycle.onStepFinish only when
compileSingleTarget returns success, leaving the lifecycle open on failures; fix
by ensuring params.lifecycle.onStepFinish is always invoked for a started
target: wrap the compileSingleTarget call in a try/finally (or ensure a finally
block) and in the finally call params.lifecycle.onStepFinish({ phase: 'compile',
step: 'target', meta }) if params.lifecycle.onStepFinish is defined (keeping
existing onStepStart handling), so compileSingleTarget,
params.lifecycle.onStepStart, and params.lifecycle.onStepFinish are used as the
points to locate changes.

In `@packages/cli/src/lib/detect.ts`:
- Around line 87-99: The parsed JSON from jsonParse (variables parseError and
data) must be validated with Zod before casting to PackageJson; update the block
in detect.ts to run a Zod schema (or z.object({...}) for the PackageJson shape)
against data and if validation fails return the same read_error shape (include
validation errors in the message and filePath) instead of blindly returning
ok(data as PackageJson); ensure you reference jsonParse, parseError, data, ok(),
and PackageJson when implementing the validation and error-return path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 648f3654-2a65-425d-b13e-4907e0f697aa

📥 Commits

Reviewing files that changed from the base of the PR and between 5954388 and 6d3eeec.

⛔ Files ignored due to path filters (3)
  • .changeset/fix-bundler-transitive-deps.md is excluded by !.changeset/**
  • .changeset/upgrade-deps.md is excluded by !.changeset/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (79)
  • packages/bundler/package.json
  • packages/bundler/src/autoloader/autoload-plugin.test.ts
  • packages/bundler/src/autoloader/autoload-plugin.ts
  • packages/bundler/src/build/build.test.ts
  • packages/bundler/src/build/build.ts
  • packages/bundler/src/build/bun-config.test.ts
  • packages/bundler/src/build/bun-config.ts
  • packages/bundler/src/build/bun-runner.ts
  • packages/bundler/src/build/clean.test.ts
  • packages/bundler/src/build/clean.ts
  • packages/bundler/src/build/config.test.ts
  • packages/bundler/src/build/config.ts
  • packages/bundler/src/build/plugins.test.ts
  • packages/bundler/src/build/plugins.ts
  • packages/bundler/src/build/watch.test.ts
  • packages/bundler/src/build/watch.ts
  • packages/bundler/src/bundler.ts
  • packages/bundler/src/compile/compile.test.ts
  • packages/bundler/src/compile/compile.ts
  • packages/bundler/src/config/detect-build-entry.test.ts
  • packages/bundler/src/config/read-version.test.ts
  • packages/bundler/src/config/read-version.ts
  • packages/bundler/src/constants.ts
  • packages/bundler/src/index.ts
  • packages/bundler/src/types.ts
  • packages/bundler/src/utils/resolve-build-entry.test.ts
  • packages/bundler/src/utils/resolve-build-entry.ts
  • packages/bundler/src/utils/resolve-config.test.ts
  • packages/bundler/src/utils/resolve-config.ts
  • packages/bundler/tsdown.config.ts
  • packages/cli/src/commands/add/command.test.ts
  • packages/cli/src/commands/add/command.ts
  • packages/cli/src/commands/build.test.ts
  • packages/cli/src/commands/build.ts
  • packages/cli/src/commands/commands.test.ts
  • packages/cli/src/commands/commands.ts
  • packages/cli/src/commands/dev.test.ts
  • packages/cli/src/commands/dev.ts
  • packages/cli/src/commands/doctor.test.ts
  • packages/cli/src/commands/doctor.ts
  • packages/cli/src/commands/run.ts
  • packages/cli/src/lib/checks.test.ts
  • packages/cli/src/lib/checks.ts
  • packages/cli/src/lib/config-helpers.ts
  • packages/cli/src/lib/detect.ts
  • packages/cli/src/lib/render.ts
  • packages/cli/src/lib/write.ts
  • packages/cli/src/manifest.ts
  • packages/config/package.json
  • packages/config/src/index.ts
  • packages/config/src/types.ts
  • packages/config/src/utils/compile.ts
  • packages/config/src/utils/index.ts
  • packages/config/src/utils/loader.test.ts
  • packages/config/src/utils/loader.ts
  • packages/config/src/utils/schema.test.ts
  • packages/config/src/utils/schema.ts
  • packages/config/tsdown.config.ts
  • packages/core/src/cli.test.ts
  • packages/core/src/cli.ts
  • packages/core/src/middleware/auth/context.ts
  • packages/core/src/middleware/auth/types.ts
  • packages/core/src/middleware/icons/context.ts
  • packages/core/src/middleware/icons/install.ts
  • packages/core/src/middleware/icons/list-system-fonts.ts
  • packages/core/src/middleware/icons/types.ts
  • packages/core/src/runtime/runtime.ts
  • packages/core/src/runtime/types.ts
  • packages/utils/package.json
  • packages/utils/src/fp/result.test.ts
  • packages/utils/src/fp/result.ts
  • packages/utils/src/fs.ts
  • packages/utils/src/index.ts
  • packages/utils/src/manifest.ts
  • packages/utils/src/node/fs.test.ts
  • packages/utils/src/node/fs.ts
  • packages/utils/src/node/index.ts
  • packages/utils/src/node/process.ts
  • packages/utils/tsdown.config.ts
💤 Files with no reviewable changes (10)
  • packages/bundler/src/config/detect-build-entry.test.ts
  • packages/bundler/tsdown.config.ts
  • packages/bundler/src/config/read-version.ts
  • packages/utils/src/fs.ts
  • packages/bundler/src/config/read-version.test.ts
  • packages/bundler/src/build/bun-config.ts
  • packages/bundler/src/build/bun-runner.ts
  • packages/bundler/src/build/plugins.test.ts
  • packages/bundler/src/build/plugins.ts
  • packages/bundler/src/build/bun-config.test.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bundler/src/utils/clean.ts`:
- Around line 42-44: Replace the ternary that sets binaryNames with a ts-pattern
match: import match from 'ts-pattern' and use match(params.compile).with(true,
() => buildBinaryNames(params.resolved.compile.name,
params.resolved.compile.targets)).otherwise(() => new Set<string>()); do the
same refactor for the other two ternary occurrences flagged by the linter in
this file (the ones around lines 89-91 and the single-expression at line 97) —
convert each ternary to a match(...) with .with(...) and .otherwise(...) so
behavior remains identical while satisfying the no-ternary rule.
- Around line 46-55: The code currently calls fs.remove inside the Promise.all
map and unconditionally returns {type: 'removed', name} even if fs.remove
throws; wrap the removal in a try/catch inside the entries.map callback
(referencing isBuildArtifact, binaryNames, fs.remove and
params.resolved.buildOutDir) and only return {type: 'removed', name} when
fs.remove succeeds, otherwise return a non-removed result (e.g., {type: 'failed'
or 'foreign', name}) and surface/log the caught error so CleanResult.removed
only contains actually-removed items.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e900bba1-69e7-4b33-b05c-7bc87d950487

📥 Commits

Reviewing files that changed from the base of the PR and between 6d3eeec and 14815b9.

📒 Files selected for processing (5)
  • packages/bundler/src/build/build.ts
  • packages/bundler/src/build/clean.test.ts
  • packages/bundler/src/build/clean.ts
  • packages/bundler/src/utils/clean.test.ts
  • packages/bundler/src/utils/clean.ts
💤 Files with no reviewable changes (2)
  • packages/bundler/src/build/clean.test.ts
  • packages/bundler/src/build/clean.ts

- Replace 3 ternaries in clean.ts with match().exhaustive()
- Replace 1 ternary in cli build.ts with match().exhaustive()
- Fix array-callback-return: .map() → .reduce() in buildBinaryNames

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/src/commands/build.ts (1)

169-169: ⚠️ Potential issue | 🟠 Major

Zod schema should validate targets against actual CompileTarget values, not arbitrary strings.

The schema at line 17 accepts any string (z.array(z.string())), but CompileTarget is a finite literal union (e.g., 'darwin-arm64', 'linux-x64'). The assertion at line 169 bypasses type checking; invalid targets would pass validation at the CLI boundary and fail downstream in mapCompileTarget().

Refine the schema to enumerate valid targets: use .refine() to validate against compileTargets, or derive a literal union from the constant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/build.ts` at line 169, The CLI Zod schema currently
accepts any string for targets and then casts params.targets to CompileTarget
when building (targets: params.targets as CompileTarget[]), which lets invalid
values through and breaks mapCompileTarget(); update the Zod schema that
validates the CLI args (the schema that currently uses z.array(z.string())) to
validate against the real CompileTarget set by either: (a) deriving a Zod
enum/literal union from the compileTargets constant or CompileTarget union, or
(b) keeping z.array(z.string()) but adding .refine(values => values.every(v =>
compileTargets.includes(v)), { message: 'invalid compile target' }); ensure the
validated value is typed as CompileTarget[] so you can remove the unsafe
assertion in the build step where params.targets is passed to
mapCompileTarget().
♻️ Duplicate comments (1)
packages/bundler/src/utils/clean.ts (1)

48-57: ⚠️ Potential issue | 🟠 Major

fs.remove failure silently misreports item as removed.

The fs.remove call doesn't check its Result tuple. If removal fails (permissions, file locked, etc.), the entry is still categorized as removed even though it remains on disk. This breaks the contract of CleanResult.removed.

Line 38 correctly handles fs.list's Result—apply the same pattern here.

🔧 Proposed fix
   const results = await Promise.all(
     entries.map(async (name) => {
       const shouldRemove = isBuildArtifact(name) || binaryNames.has(name)
       if (shouldRemove) {
-        await fs.remove(join(params.resolved.buildOutDir, name))
-        return { type: 'removed' as const, name }
+        const [removeErr] = await fs.remove(join(params.resolved.buildOutDir, name))
+        if (removeErr) {
+          return { type: 'foreign' as const, name }
+        }
+        return { type: 'removed' as const, name }
       }
       return { type: 'foreign' as const, name }
     })
   )

As per coding guidelines: "represent expected failures with Result tuples... Always destructure and check the error element before using the value element" (contributing/standards/typescript/errors.md).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bundler/src/utils/clean.ts` around lines 48 - 57, The code currently
calls fs.remove inside entries.map and unconditionally returns {type: 'removed',
name} even if fs.remove failed; change the async removal to mirror the fs.list
handling by awaiting fs.remove and destructuring its Result tuple (e.g., const
[rmErr] = await fs.remove(join(params.resolved.buildOutDir, name))), check rmErr
before reporting removal, and only return {type: 'removed', name} when rmErr is
falsy; otherwise return a non-removed result (e.g., {type: 'foreign'|'error',
name, error: rmErr}) so CleanResult.removed correctly reflects actual deletions.
Ensure this logic is applied inside the entries.map async callback and reference
isBuildArtifact, binaryNames, and params.resolved.buildOutDir when locating the
target to remove.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/commands/build.ts`:
- Around line 54-57: The spinner handlers onStepStart and onStepFinish access
meta.label unsafely (StepEvent's meta is Readonly<Record<string, unknown>>), so
coerce or validate it before use: update the onStepStart and onStepFinish
callbacks to read meta.label with a type guard or fallback (e.g., const label =
typeof meta?.label === 'string' ? meta.label : 'step' or String(meta?.label ??
'step')) and pass that safe label into ctx.status.spinner.message to avoid
"undefined" or malformed messages; ensure both handlers use the same safeLabel
logic.

---

Outside diff comments:
In `@packages/cli/src/commands/build.ts`:
- Line 169: The CLI Zod schema currently accepts any string for targets and then
casts params.targets to CompileTarget when building (targets: params.targets as
CompileTarget[]), which lets invalid values through and breaks
mapCompileTarget(); update the Zod schema that validates the CLI args (the
schema that currently uses z.array(z.string())) to validate against the real
CompileTarget set by either: (a) deriving a Zod enum/literal union from the
compileTargets constant or CompileTarget union, or (b) keeping
z.array(z.string()) but adding .refine(values => values.every(v =>
compileTargets.includes(v)), { message: 'invalid compile target' }); ensure the
validated value is typed as CompileTarget[] so you can remove the unsafe
assertion in the build step where params.targets is passed to
mapCompileTarget().

---

Duplicate comments:
In `@packages/bundler/src/utils/clean.ts`:
- Around line 48-57: The code currently calls fs.remove inside entries.map and
unconditionally returns {type: 'removed', name} even if fs.remove failed; change
the async removal to mirror the fs.list handling by awaiting fs.remove and
destructuring its Result tuple (e.g., const [rmErr] = await
fs.remove(join(params.resolved.buildOutDir, name))), check rmErr before
reporting removal, and only return {type: 'removed', name} when rmErr is falsy;
otherwise return a non-removed result (e.g., {type: 'foreign'|'error', name,
error: rmErr}) so CleanResult.removed correctly reflects actual deletions.
Ensure this logic is applied inside the entries.map async callback and reference
isBuildArtifact, binaryNames, and params.resolved.buildOutDir when locating the
target to remove.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5fc3ad77-ee48-4c8f-a874-4c5b3e331e8b

📥 Commits

Reviewing files that changed from the base of the PR and between 14815b9 and e7d7aa5.

📒 Files selected for processing (2)
  • packages/bundler/src/utils/clean.ts
  • packages/cli/src/commands/build.ts

- Fix linux-x64-musl target mapping to bun-linux-x64-musl (was glibc)
- Handle fs.remove failure in clean.ts (report as foreign on error)
- Use ts-pattern match in resolve-build-entry.ts
- Convert exec() to object params per coding standards
- Add meta.label fallback for type safety in build.ts spinner
- Add compile=true test coverage for config.test.ts
- Collapse _require intermediate variable in config.ts

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/cli/src/commands/build.ts (1)

155-169: ⚠️ Potential issue | 🟠 Major

Validate CLI compile targets against the config package's schema.

params.targets originates from z.array(z.string()).optional() and is then cast unsafely to CompileTarget[] at line 166. This lets invalid target names pass type checking and fail later in bundler logic. The config package defines CompileTargetSchema (in packages/config/src/utils/schema.ts) but marks it private and does not export it. Either export the schema or the valid targets list from the config package and use it to validate the CLI value at the boundary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/build.ts` around lines 155 - 169,
mergeCompileTargets is unsafely casting params.targets (from
z.array(z.string()).optional()) to CompileTarget[] which allows invalid target
names to slip through; update mergeCompileTargets to validate params.targets
against the config package's canonical schema or exported valid-target list
before assigning to compile.targets. Either export CompileTargetSchema or a
validated string list from the config package (from
packages/config/src/utils/schema.ts), import that into
packages/cli/src/commands/build.ts, run validation on params.targets (reject or
map invalid entries) and only then set compile.targets (instead of the direct
cast to CompileTarget[]) so the CLI boundary enforces the config's allowed
target values.
packages/bundler/src/compile/compile.ts (1)

64-68: ⚠️ Potential issue | 🔴 Critical

Result tuple destructuring breaks type narrowing — fix by keeping tuples intact until after checked.

After destructuring [error, value] = resultTuple, both members remain independently nullable. Lines 155–165 use bunTarget without guards after the mapError check; lines 266–271 use entries after checking listError; lines 64–68 pass a nullable error to err(). Narrow the tuple itself before unpacking: if (result[0]) return err(result[0]); const [, value] = result.

Also applies to: 153–165, 263–271


Lifecycle hook is asymmetric: onStepStart always fires, but onStepFinish only on success (line 127). This leaves failed compilation steps unclosed, breaking step tracking in the UI. Call onStepFinish unconditionally or document why failures skip it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bundler/src/compile/compile.ts` around lines 64 - 68, The tuple
destructuring is breaking type narrowing: don’t destructure result tuples until
you’ve checked result[0] exists; instead test the tuple as a whole (e.g., if
(result[0]) return err(result[0]); then const [, value] = result) wherever you
currently do `[error, value] = result` (fix occurrences that reference bunTarget
after mapError, entries after listError, and the results.find(...) usage that
currently passes a nullable to err()). Also make the lifecycle symmetric: ensure
onStepFinish is invoked unconditionally (e.g., in a finally block) for each
onStepStart call so failed compilation steps still close (adjust the code paths
that call onStepStart/onStepFinish accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bundler/src/build/config.test.ts`:
- Line 1: The test is calling .map() directly on output.plugins (an
InlineConfig.plugins / RolldownPluginOption<any>) which may include falsy,
nested, or async entries; before mapping plugin names, normalize and narrow
output.plugins to a plain Plugin[]: convert to an array if undefined, flatten
nested arrays, filter out falsy values, and cast to the Rollup Plugin type (or
resolve async entries if your test expects promises) — then call .map(...) on
that narrowed plugins array instead of directly on output.plugins.

In `@packages/bundler/src/build/config.ts`:
- Around line 98-100: The returned ALWAYS_BUNDLE array is being returned by
reference from the match in alwaysBundle, causing shared-state leakage; change
the branch that currently returns ALWAYS_BUNDLE to return a shallow clone (e.g.,
spread into a new array) so each config gets its own instance — update the match
for compile (symbol: alwaysBundle, compile, match) to clone ALWAYS_BUNDLE before
returning instead of returning the module-level array directly.

In `@packages/bundler/src/compile/compile.ts`:
- Around line 127-129: The code only calls params.lifecycle.onStepFinish when
result[0] === null, leaving started targets without a matching finish; ensure
onStepFinish is always invoked for targets that had onStepStart called. Update
the logic around the target lifecycle so that when you call
params.lifecycle.onStepStart(...) you set a local flag (e.g., targetStarted) and
then, instead of checking result[0] === null, always await
params.lifecycle.onStepFinish({ phase: 'compile', step: 'target', meta }) when
targetStarted is true and params.lifecycle.onStepFinish exists; keep using the
same meta object so consumers can correlate start/finish.

In `@packages/utils/src/node/process.ts`:
- Around line 25-31: The exec function currently calls execFile without a cwd,
causing child processes to inherit the parent working directory; update exec to
accept an optional cwd (e.g., add cwd?: string to the params object for exec)
and pass it into the execFile options when invoking execFile, so execFile(cmd, {
cwd, args: [...] }, …) is used; then ensure callers (such as compileSingleTarget
in packages/bundler/src/compile/compile.ts) pass params.resolved.cwd into exec
so cleanBunBuildArtifacts can reliably clean the same directory.

---

Outside diff comments:
In `@packages/bundler/src/compile/compile.ts`:
- Around line 64-68: The tuple destructuring is breaking type narrowing: don’t
destructure result tuples until you’ve checked result[0] exists; instead test
the tuple as a whole (e.g., if (result[0]) return err(result[0]); then const [,
value] = result) wherever you currently do `[error, value] = result` (fix
occurrences that reference bunTarget after mapError, entries after listError,
and the results.find(...) usage that currently passes a nullable to err()). Also
make the lifecycle symmetric: ensure onStepFinish is invoked unconditionally
(e.g., in a finally block) for each onStepStart call so failed compilation steps
still close (adjust the code paths that call onStepStart/onStepFinish
accordingly).

In `@packages/cli/src/commands/build.ts`:
- Around line 155-169: mergeCompileTargets is unsafely casting params.targets
(from z.array(z.string()).optional()) to CompileTarget[] which allows invalid
target names to slip through; update mergeCompileTargets to validate
params.targets against the config package's canonical schema or exported
valid-target list before assigning to compile.targets. Either export
CompileTargetSchema or a validated string list from the config package (from
packages/config/src/utils/schema.ts), import that into
packages/cli/src/commands/build.ts, run validation on params.targets (reject or
map invalid entries) and only then set compile.targets (instead of the direct
cast to CompileTarget[]) so the CLI boundary enforces the config's allowed
target values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fafef126-e6e8-4caf-9ea7-94f2364eebf8

📥 Commits

Reviewing files that changed from the base of the PR and between e7d7aa5 and a196f3e.

📒 Files selected for processing (8)
  • packages/bundler/src/build/config.test.ts
  • packages/bundler/src/build/config.ts
  • packages/bundler/src/compile/compile.test.ts
  • packages/bundler/src/compile/compile.ts
  • packages/bundler/src/utils/clean.ts
  • packages/bundler/src/utils/resolve-build-entry.ts
  • packages/cli/src/commands/build.ts
  • packages/utils/src/node/process.ts

- Fix typecheck: cast plugins to typed array in config.test.ts
- Clone ALWAYS_BUNDLE to prevent shared state mutation
- Always fire onStepFinish for symmetry with onStepStart
- Add optional cwd to exec() and thread through compile

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bundler/src/build/config.ts`:
- Around line 138-144: Replace the two-branch imperative if/return in
buildPlugins (and similarly in resolveId and load) with ts-pattern's match()
expression: use match(compile).with(true, ...) and .otherwise(...) to return
[createStubPlugin(STUB_PACKAGES)] when compile is true and [] otherwise; follow
the same pattern used by buildDeps and buildDefine to keep conditional logic
functional and consistent with the repo style.

In `@packages/bundler/src/compile/compile.test.ts`:
- Around line 36-38: The test narrows compile.targets using an overly broad "as
never"; replace that with an explicit type cast to the expected CompileTarget
array type and import the type: add "import type { CompileTarget } from
'@kidd-cli/config'" at the top and change the cast on compile.targets to use "as
CompileTarget[]" (or the precise expected CompileTarget form) so the targets
field has the correct typed value in the compile object.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cc3c3a59-f7ce-42ed-8f14-63c8d2180d49

📥 Commits

Reviewing files that changed from the base of the PR and between a196f3e and ea78809.

📒 Files selected for processing (5)
  • packages/bundler/src/build/config.test.ts
  • packages/bundler/src/build/config.ts
  • packages/bundler/src/compile/compile.test.ts
  • packages/bundler/src/compile/compile.ts
  • packages/utils/src/node/process.ts

- Replace if/return with match() in buildPlugins, resolveId, load
- Tighten type cast from `as never` to `as readonly CompileTarget[]`

Co-Authored-By: Claude <noreply@anthropic.com>
zrosenbauer and others added 4 commits April 2, 2026 13:56
Add build.define to kidd.config.ts for explicit compile-time constants.
Auto-resolve KIDD_PUBLIC_* env vars from process.env at build time
(same convention as Next.js NEXT_PUBLIC_*). Show injected vars in
build output log.

Closes #145

Co-Authored-By: Claude <noreply@anthropic.com>
… migration

- Binary runner: use example name instead of hardcoded 'cli'
- Node runner: default to dist/index.mjs (tsdown ESM output)
- authenticated-service: fix custom binary name and entry path
- CI: update chmod to match new binary naming convention
- Benchmarks: update codspeed entry to index.mjs

Co-Authored-By: Claude <noreply@anthropic.com>
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 2, 2026

Merging this PR will improve performance by 16.66%

⚡ 2 improved benchmarks
⏩ 2 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
WallTime icons --help 336.5 ms 288.4 ms +16.66%
WallTime icons status 353.8 ms 306.6 ms +15.39%

Comparing refactor/bundler-tsdown-migration (49b3a0b) with main (5954388)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Co-Authored-By: Claude <noreply@anthropic.com>
@zrosenbauer zrosenbauer merged commit 501110e into main Apr 2, 2026
12 checks passed
@zrosenbauer zrosenbauer deleted the refactor/bundler-tsdown-migration branch April 2, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant