Skip to content

feat: recognize .msix / .msixbundle assets and 'msix' package format#26

Closed
jjeff wants to merge 3 commits into
dopry:nextfrom
jjeff:feat/msix-support
Closed

feat: recognize .msix / .msixbundle assets and 'msix' package format#26
jjeff wants to merge 3 commits into
dopry:nextfrom
jjeff:feat/msix-support

Conversation

@jjeff

@jjeff jjeff commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds MSIX support to Pecans so it can act as the update server for an Electron 41+ app packaged as MSIX. Electron 41's autoUpdater consumes the existing Squirrel.Mac-shaped JSON the /update/:platform/:version endpoint already emits — Pecans only needs to recognize .msix / .msixbundle as Windows assets and route to them when callers request the msix package format.

Changes

  • Extend SUPPORTED_FILE_EXTENSIONS with .msix and .msixbundle.
  • Extend PACKAGE_FORMATS with msix; filenameToPackageFormat detects both extensions.
  • filenameToOperatingSystem maps .msix / .msixbundlewindows.
  • filenameToArchitecture treats .msixbundle as universal (it's a multi-arch bundle by definition); single-arch .msix continues to derive arch from filename suffix (_x64, _arm64, _x86).
  • getDownloadExtensionsByOs('windows', 'msix') returns ['.msixbundle', '.msix'], preferring the bundle when both are published.

The existing /update/:platform/:version JSON endpoint already emits the {url, name, notes, pub_date} shape Electron's MSIX updater consumes. Callers (Electron's autoUpdater.setFeedURL) just pass ?filetype=msix or ?filetype=msixbundle and the resolved download URL routes through /download/version/.../?filetype=msix to the matching asset.

Test plan

  • Existing 637 tests still pass
  • +2 new unit tests verify handleUpdateOSX produces a Squirrel.Mac-shaped JSON response with a .msix / .msixbundle URL when called with the appropriate filetype
  • Extension / package format / OS / architecture detection covered by unit tests
  • tsc --noEmit clean

Notes

This is the server-side half of MSIX support for spaceagetv/missioncontrol (Visibox). Reference: Electron 41 release notes and electron/electron#49230.

🤖 Generated with Claude Code

@jjeff
jjeff marked this pull request as ready for review May 23, 2026 03:02
Copilot AI review requested due to automatic review settings May 23, 2026 03:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds MSIX/MSIXBUNDLE support across file extension, package format, OS/architecture detection, and update feed behavior to better support Windows MSIX auto-updaters (Squirrel.Mac-shaped JSON).

Changes:

  • Add .msix and .msixbundle to supported extensions and Windows download extension resolution when pkg=msix.
  • Add msix as a recognized PackageFormat and detect MSIX/MSIXBUNDLE filenames accordingly.
  • Extend OS/architecture detection to treat .msixbundle as universal and .msix/.msixbundle as Windows; add/update unit tests and Pecans update route tests.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/unit/pecans.spec.ts Adds tests asserting update JSON shape and URL behavior for MSIX/MSIXBUNDLE requests.
test/unit/SupportedFileExtension.spec.ts Extends extension support tests and Windows download extension selection for pkg=msix.
test/unit/PackageFormat.spec.ts Adds msix to formats and tests filename/query detection.
test/unit/OperatingSystem.spec.ts Adds Windows detection tests for .msix and .msixbundle.
test/unit/Architecture.spec.ts Adds architecture detection tests for .msix vs .msixbundle (universal).
src/utils/SupportedFileExtension.ts Adds MSIX extensions and updates Windows download extension resolution for pkg=msix.
src/utils/PackageFormat.ts Adds msix to PACKAGE_FORMATS and detects .msix/.msixbundle filenames.
src/utils/OperatingSystem.ts Recognizes .msix and .msixbundle as Windows.
src/utils/Architecture.ts Treats .msixbundle as universal architecture.

Comment thread src/utils/SupportedFileExtension.ts Outdated
Comment thread test/unit/pecans.spec.ts Outdated
jjeff added a commit to jjeff/pecans that referenced this pull request May 23, 2026
- SupportedFileExtension.ts: revert the misleading comment claim about
  special-casing .msixbundle. path.extname() already returns
  ".msixbundle" correctly for files with that extension; only .tar.gz
  needs the explicit endsWith() check because extname returns ".gz" for
  files ending in ".tar.gz".

- pecans.spec.ts: add channel:"stable" to the msixbundle test for
  parameter-shape consistency with the preceding msix test, so both
  cases exercise the same route contract.
jjeff added 2 commits June 26, 2026 15:04
Electron 41+'s autoUpdater has native MSIX support and consumes the
existing Squirrel.Mac-shaped JSON response format from /update/:platform/:version.
For Pecans to act as the update server for an MSIX-packaged Electron app,
it only needs to recognize the new asset extensions and surface them as
download candidates when callers request the 'msix' package format.

- Add .msix and .msixbundle to SUPPORTED_FILE_EXTENSIONS.
- Add 'msix' to PACKAGE_FORMATS; filenameToPackageFormat detects both
  .msix and .msixbundle.
- filenameToOperatingSystem maps .msix / .msixbundle to 'windows'.
- filenameToArchitecture treats .msixbundle as 'universal' (multi-arch
  bundle); single-arch .msix continues to derive arch from filename.
- getDownloadExtensionsByOs('windows', 'msix') returns
  ['.msixbundle', '.msix'], preferring the bundle when both are present.

The existing /update/:platform/:version JSON endpoint already emits the
{url, name, notes, pub_date} shape Electron's MSIX updater expects; with
the recognition above, callers pass ?filetype=msix (or msixbundle) and
the resolved download URL routes through /download/version/.../?filetype=msix
to the appropriate asset.

Tests cover extension/package/OS/arch detection and the update endpoint's
behavior for Windows MSIX clients.
- SupportedFileExtension.ts: revert the misleading comment claim about
  special-casing .msixbundle. path.extname() already returns
  ".msixbundle" correctly for files with that extension; only .tar.gz
  needs the explicit endsWith() check because extname returns ".gz" for
  files ending in ".tar.gz".

- pecans.spec.ts: add channel:"stable" to the msixbundle test for
  parameter-shape consistency with the preceding msix test, so both
  cases exercise the same route contract.
@dopry
dopry force-pushed the feat/msix-support branch from 615329f to 5dd07de Compare June 26, 2026 19:04
…gestion

filenameToPlatform built keys like WINDOWS_MSIX_UNIVERSAL that don't exist
in the platforms record, so the GitHub backend dropped every .msix and
.msixbundle asset and ?filetype=msix silently resolved to the .exe.

- add windows_universal to PLATFORMS; .msixbundle assets now type as
  windows_universal, single-arch .msix as windows_32/64
- exclude msix from platform id composition (windows ids don't encode pkg;
  msix assets resolve by filetype instead)
- resolveReleaseAssetForVersion matches windows_universal for arch-specific
  windows requests, but only when an msix filetype is explicitly wanted so
  default flows keep serving arch-specific .exe installers
- versions.filter counts windows_universal assets toward availability for
  arch-specific windows queries so bundle-only releases don't 404
- cover the ingestion seam (normalizeRelease/filenameToPlatform) and
  resolution behavior with tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018YjpR5uuatwzQvNbpDo8n4

@dopry dopry left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for this — the direction is right, the utility-layer changes are clean, and the added unit tests are well written. Unfortunately there's an integration gap that keeps the feature from working end-to-end, found by executing this branch rather than just reading it:

filenameToPlatform returns undefined for every MSIX asset, because the composed key (WINDOWS_MSIX_64 / WINDOWS_MSIX_UNIVERSAL) doesn't exist in the platforms record. The GitHub backend drops any asset whose platform resolves to null/undefined, so no .msix/.msixbundle ever becomes a release asset. Net effect: ?filetype=msix silently serves the .exe when one exists (an MSIX updater can't apply that), or 404s on an MSIX-only release. Details in the inline comments, along with a second issue: windows_universal assets aren't reachable from windows_64 requests in resolveReleaseAssetForVersion/versions.filter even after the platform map is fixed.

The existing 637 tests do pass (confirmed locally), but the new tests all stop short of the ingestion→resolution seam where this lives — suggestions for seam tests are inline.

A ready-made fix is available: branch claude/pecans-pr-26-review-249u6k on this repo contains your two commits plus a fix commit addressing all of the above, with tests at the seam (655 unit tests pass, tsc clean). It's a fast-forward on top of your head, so you can pull it straight into this PR:

git checkout feat/msix-support
git pull https://github.com/dopry/pecans claude/pecans-pr-26-review-249u6k
git push

On downstream compatibility (important for existing pecans consumers): with the fix applied, default windows downloads still resolve [".exe"] unless pkg=msix/filetype=msix is explicitly requested, the Squirrel.Windows RELEASES/.nupkg path is untouched, and there's a regression test pinning that the .exe still wins default resolution when MSIX assets are published alongside it. The PackageFormat/SupportedFileExtension unions widen, which only affects TypeScript consumers doing exhaustive switches — semver-minor.


Generated by Claude Code

const name = filename.toLowerCase();
if (name.endsWith(".deb")) return "deb";
if (name.endsWith(".rpm")) return "rpm";
if (name.endsWith(".msix") || name.endsWith(".msixbundle")) return "msix";

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Blocker: returning "msix" here breaks filenameToPlatform (src/utils/platforms.ts), which composes the platform key as os_pkg_arch — for MSIX assets that produces WINDOWS_MSIX_64 / WINDOWS_MSIX_UNIVERSAL, neither of which exists in the platforms record, so it returns undefined.

The GitHub backend filters release assets with filenameToPlatform(asset.name) != null (src/backends/github.ts:229-236), so every .msix/.msixbundle asset is silently dropped at ingestion and the feature doesn't work end-to-end. Verified by running the code on this branch:

filenameToPlatform("Visibox-5.0.13.msixbundle")  → undefined
filenameToPlatform("Visibox_5.0.13.0_x64.msix")  → undefined

Worse, because resolveReleaseAssetForVersion keeps all supported extensions as fallbacks, ?filetype=msix then silently serves the .exe when one exists — an MSIX updater receives an installer it can't apply — or 404s when the release only has MSIX assets.

Fix: keep msix out of the platform-id composition in filenameToPlatform (windows platform ids don't encode pkg; msix resolves by filetype) and add windows_universal to PLATFORMS so .msixbundle gets a valid type.


Generated by Claude Code

Comment thread src/utils/Architecture.ts
const name = filename.toLowerCase();
if (name == "releases") return "universal";
// .msixbundle is a multi-architecture bundle by definition
if (name.endsWith(".msixbundle")) return "universal";

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Mapping .msixbundleuniversal is right in principle, but nothing downstream can match it for windows clients. resolveReleaseAssetForVersion filters with asset.type.startsWith(platform) and only adds a universal fallback for OSX (src/utils/resolveForVersion.ts:26-28); versions.ts:53-56 has the same osx-only special case. So a client on windows_64 — the platform your own handleUpdateOSX tests use, and what the win32-x64 legacy mapping produces — would never resolve a bundle even once the platform-map issue is fixed.

Two additions are needed:

  • resolveReleaseAssetForVersion: also match windows_universal for arch-specific windows requests — but gate it on the wanted filetype being .msix/.msixbundle, otherwise a published bundle would start outranking the .exe for existing Squirrel clients (the type-length sort prefers longer types).
  • versions.filter: count windows_universal assets toward availability for windows_* queries, so a bundle-only release doesn't 404 the update endpoint.

Related edge worth a comment or test: arm64-only .msix files (App_arm64.msix) resolve to arch arm64, which isn't a supported windows arch (getSupportedArchByOs), so they're dropped — arm64 MSIX clients need the .msixbundle path.


Generated by Claude Code

Comment thread test/unit/pecans.spec.ts
);
});

it("should produce Squirrel.Mac-shaped JSON for Windows MSIX clients", async () => {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

These two tests mock pecans.versions.filter and assert only the shape of the JSON/URL string, so they pass even though MSIX assets never survive backend ingestion — the bug lives in the seam between normalizeRelease/filenameToPlatform and resolveReleaseAssetForVersion, which nothing here crosses.

Suggest adding:

  • a normalizeRelease test (in github.spec.ts) asserting a release with .exe + .msix + .msixbundle assets keeps all three, with the expected type/arch/pkg;
  • resolveReleaseAssetForVersion tests: filetype=msix picks the .msix/bundle over the .exe, and — the downstream-compat guarantee — a default request (no filetype) still picks the .exe when msix assets are published alongside it.

Generated by Claude Code

dopry commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Closing as superseded by #46.

Since this branch was opened, next went through the Phase 5–7 refactors (#41#45): versions.ts and resolveForVersion.ts — where half of this diff lives — were replaced by the unified ReleaseService pipeline on the discrete {os, arch, pkg} model, so a rebase couldn't be resolved mechanically and this fork branch isn't writable from the maintainer side to update the PR in place.

#46 reimplements the feature on the new architecture with the same external contract (?filetype=msix|msixbundle on the update/download routes, bundle preferred over single-arch .msix, default windows flows still resolving the .exe), ports all of this PR's tests, and adds end-to-end coverage through the HTTP surface. @jjeff's authorship is credited via Co-Authored-By on the commit — thank you for the contribution!


Generated by Claude Code

@dopry dopry closed this Jul 16, 2026
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.

4 participants