Skip to content

fix: windows compatibility across packages#184

Merged
zrosenbauer merged 5 commits into
mainfrom
fix/windows-compatibility
May 8, 2026
Merged

fix: windows compatibility across packages#184
zrosenbauer merged 5 commits into
mainfrom
fix/windows-compatibility

Conversation

@zrosenbauer
Copy link
Copy Markdown
Member

Summary

Resolves multiple Windows compatibility issues across bundler, cli, utils, and core. Some are concrete bugs (PARSE_ERROR on kidd build, kidd run --engine=tsx ENOENT), others are paths-as-strings assumptions that worked on POSIX but failed silently on Windows. Also drops dead submodule-detection code that was already disabled on Windows via a process.platform === 'win32' guard.

Changes

@kidd-cli/bundler (patch)

  • Autoload imports: generated import statements now use file:// URL specifiers (pathToFileURL(p).href) instead of raw absolute paths. On Windows, paths like C:\joggr\update.ts were being parsed as JavaScript string literals where \j, \u, \w triggered PARSE_ERROR: Invalid escape sequence. The runtime autoloader was already fixed in fix(core): use file:// URLs for command autoload imports #180; this extends the same fix to the static autoloader plugin.
  • Compile output naming: resolveBinaryName() now appends .exe for windows-* targets so CompiledBinary.path matches the file bun actually produces on disk. Bun auto-appends .exe for Windows targets, so we were recording a path that didn't exist on disk. clean() previously enumerated both cli and cli.exe to compensate; now it produces a single canonical name.

@kidd-cli/cli (patch)

  • Template path normalization: renderTemplate now normalizes path.relative() output to forward slashes via toPosixPath. This fixes two latent bugs in kidd init on Windows:
    • the regex /(^|\/)gitignore($|\/)/g didn't match \gitignore so gitignore.liquid wasn't renamed to .gitignore
    • file.relativePath.includes('commands/hello.ts') didn't match commands\hello.ts, so --no-example and --no-config flags silently no-op'd

@kidd-cli/utils (patch)

  • proc.exec and proc.spawn now pass shell: true on Windows. Without this, Node's CreateProcess cannot launch .cmd shim files — which is how npm/pnpm install most CLI binaries (tsx, prettier, etc.). Affects kidd run --engine=tsx on Windows. node and bun are unaffected (they're real .exe files).

@kidd-cli/core (breaking, minor)

  • Removed unused git submodule detection. Drops isInSubmodule, getParentRepoRoot, and the ProjectRoot type from the public API. findProjectRoot() now returns string | null (the path) instead of ProjectRoot | null.
  • The submodule code was inherited from a project skeleton, had zero internal callers, and was silently disabled on Windows via if (process.platform === 'win32') return null. Same pattern of "we punted on Windows here" — opted to delete rather than maintain.

Tests

  • Added Windows-specific cases in packages/bundler/src/compile/compile.test.ts covering .exe naming for single-target, multi-target, and mixed-target builds.
  • Updated packages/bundler/src/autoloader/generate-autoloader.test.ts to assert file:// URL specifiers.
  • Rewrote packages/core/src/lib/project/root.test.ts for the simplified API.
  • Updated tests/helpers.ts (integration test runner) to append .exe for the Windows host so the path matches reality and stops relying on Windows' CreateProcessW extension auto-append.

Investigation notes

The compile output .exe change was the only finding from this round of audit that wasn't immediately obvious. I dispatched two adversarial reviewers — one argued NOT to fix (Windows masks the spawn case via auto-extension; recording "what user requested" is defensible), one argued to fix (clean.ts already had a workaround enumerating both variants, which is evidence the contract leak was already biting). The smoking gun was packages/bundler/src/utils/clean.ts:101-102 which had to special-case Windows because binary.path couldn't be trusted. Verdict: real bug.

Caveat

Even with these fixes, none of the Windows code paths have been executed on a Windows machine in this session. CI runs on windows-latest (.github/workflows/ci-integration.yml) but builds the example artifacts on macOS and runs the pre-built CLIs directly — kidd run, kidd init, and the bundler's autoload generation are not exercised end-to-end on Windows. Recommend monitoring the Windows job on this PR closely.

Test plan

  • pnpm check (typecheck + lint + format) — clean
  • pnpm test — 1075 core + 158 cli + 135 bundler + 101 utils tests pass
  • CI integration job on windows-latest passes
  • CI integration job on macos-latest, macos-15-intel, ubuntu-latest pass

- bundler: emit file:// URL specifiers for autoload imports so Windows
  paths don't trigger PARSE_ERROR when backslashes are interpreted as
  escape sequences in generated string literals
- bundler: append .exe to compiled binary names for Windows targets so
  CompiledBinary.path matches the file bun produces on disk; simplify
  clean() which previously had to enumerate both variants
- cli: normalize template paths to forward slashes in renderTemplate so
  the gitignore -> .gitignore rename and --no-example/--no-config filters
  in kidd init work on Windows (where path.relative returns native \)
- utils: pass shell:true on Windows in proc.exec/proc.spawn so .cmd
  shims (npm/pnpm-installed CLIs like tsx) can be launched via cmd.exe
- core (breaking): remove unused git submodule detection. Drops
  isInSubmodule, getParentRepoRoot, and the ProjectRoot type.
  findProjectRoot now returns string | null. The submodule code was
  inherited from a project skeleton, had no internal callers, and was
  silently disabled on Windows.

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

changeset-bot Bot commented May 8, 2026

🦋 Changeset detected

Latest commit: 6f15480

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

This PR includes changesets to release 5 packages
Name Type
@kidd-cli/bundler Patch
@kidd-cli/cli Patch
@kidd-cli/core Minor
@kidd-cli/utils Patch
@kidd-cli/config 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 May 8, 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 May 8, 2026 8:52pm

Request Review

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 8, 2026

Merging this PR will not alter performance

✅ 2 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing fix/windows-compatibility (6f15480) with main (e2235e8)

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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

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

This PR standardizes generated and runtime ESM import specifiers to file:// URLs via new path utilities, normalizes template-relative paths to POSIX form, adds Windows .exe handling for compiled binaries and cleanup, enables shell execution for child processes on Windows, and simplifies findProjectRoot to return a string path (removing submodule/parent-repo APIs and types).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • joggrdocs/kidd#180: Prior work addressing file:// import-specifier behavior for dynamic autoload imports.
  • joggrdocs/kidd#83: Related changes to the autoloader generator and its tests.
  • joggrdocs/kidd#182: Related build command changes touching bundler invocation and flags.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the primary change—Windows compatibility fixes across multiple packages—which aligns with the substantial cross-package work detailed in the changeset.
Description check ✅ Passed The description comprehensively covers all significant changes, includes investigation rationale, and explicitly acknowledges Windows execution gaps; it is clearly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 97.87% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/windows-compatibility

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

zrosenbauer and others added 3 commits May 8, 2026 16:31
…tils/node/path

Centralize the two cross-platform path helpers that previously lived as
private one-liners in the bundler and the CLI:

- `path.toImportUrl(p)` wraps `pathToFileURL(p).href` for converting
  filesystem paths into ESM `file://` import specifiers.
- `path.toPosixPath(p)` normalizes native paths to forward slashes.

Updated call sites:
- packages/bundler/src/autoloader/generate-autoloader.ts (3 usages)
- packages/core/src/autoload.ts (1 usage)
- packages/cli/src/lib/render.ts (1 usage)

Co-Authored-By: Claude <noreply@anthropic.com>
…at module load

Capturing process.platform at module load freezes the value before any
test stub or platform mock can apply. Convert NEEDS_SHELL constant into
a needsShell() function so each spawn/exec call re-reads the value.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace `match(targets.length > 0)` with `match(targets).with([], ...)`
and `match(isMultiTarget)` with `match(resolvedTargets.length).with(1, ...)`
so ts-pattern matches on the underlying value rather than a boolean
intermediary. Drops the now-redundant isMultiTarget local.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bundler/src/utils/clean.ts`:
- Around line 98-101: Replace the imperative if-check that appends ".exe" with a
ts-pattern match expression: use match(target).with('windows-*', () =>
`${base}.exe`).otherwise(() => base) (or the equivalent pattern used elsewhere),
ensuring you reference the same target and base variables and keep consistent
ts-pattern usage as in the surrounding code.
🪄 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: ea0588e4-d53c-4867-8d35-7525027c18ad

📥 Commits

Reviewing files that changed from the base of the PR and between 21ece3e and ef94443.

📒 Files selected for processing (1)
  • packages/bundler/src/utils/clean.ts

Comment thread packages/bundler/src/utils/clean.ts
Sweep the codebase for the match(boolean) antipattern where ts-pattern
was being fed a derived comparison or a bare boolean variable. Replaced
with one of:

- match on the underlying value (e.g. match(parts.length > 1) becomes
  inlined if/else; match(targets.length > 0) becomes match(targets).with([], ...))
- match on the parent object via property pattern
  (e.g. match(params.compile) becomes match(params).with({ compile: true }, ...))
- straight if/else for simple boolean variables

Files touched (14): bundler/build/config, bundler/utils/clean,
bundler/utils/format-error, cli/commands/build, cli/lib/template-versions,
core/autoload, core/lib/format/code-frame, core/middleware/config/config,
core/middleware/icons/context, core/middleware/icons/install,
core/runtime/register, core/stories/check, core/ui/keys.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/stories/check.ts`:
- Around line 133-143: Replace the two-branch if returning an error or empty
array with a ts-pattern match: import { match } from 'ts-pattern' and use
match(params.editableFieldCount).with(value => value > MAX_EDITABLE_FIELDS, ()
=> [Object.freeze({ storyName: params.name, severity: 'error' as const, message:
`Too many editable fields: ${String(params.editableFieldCount)} (max
${String(MAX_EDITABLE_FIELDS)}). Move fields to \\`defaults\\` to reduce.`,
})]).otherwise(() => []) so the logic uses match() instead of the if; ensure you
reference params.editableFieldCount and MAX_EDITABLE_FIELDS and return the same
frozen object shape.
🪄 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: ef41fa41-68c8-4cc8-8ac7-63d35f862f16

📥 Commits

Reviewing files that changed from the base of the PR and between ef94443 and 6f15480.

📒 Files selected for processing (13)
  • packages/bundler/src/build/config.ts
  • packages/bundler/src/utils/clean.ts
  • packages/bundler/src/utils/format-error.ts
  • packages/cli/src/commands/build.ts
  • packages/cli/src/lib/template-versions.ts
  • packages/core/src/autoload.ts
  • packages/core/src/lib/format/code-frame.ts
  • packages/core/src/middleware/config/config.ts
  • packages/core/src/middleware/icons/context.ts
  • packages/core/src/middleware/icons/install.ts
  • packages/core/src/runtime/register.ts
  • packages/core/src/stories/check.ts
  • packages/core/src/ui/keys.ts

Comment thread packages/core/src/stories/check.ts
@zrosenbauer zrosenbauer merged commit f8290a2 into main May 8, 2026
12 checks passed
@zrosenbauer zrosenbauer deleted the fix/windows-compatibility branch May 8, 2026 21:02
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