Package a dynwinrt Node.js application as MSIX

September 3, 2026 ยท View on GitHub

This guide shows how to package a Node.js application that uses @microsoft/dynwinrt into an MSIX with WinApp CLI 0.6.2.

The process first creates an application-specific Node Single Executable Application (SEA). The executable, application JavaScript, generated WinRT bindings, and dynwinrt native runtime are then staged together and packaged as a signed MSIX.

The installed application appears in the Start menu and launches with package identity.

1. Initialize the Node.js project

Create a Node.js project:

mkdir my-dynwinrt-app
cd my-dynwinrt-app
npm init -y
npm install --save-dev @microsoft/winappcli@0.6.2
npx winapp init . --use-defaults --add-js-bindings

winapp init creates the manifest and assets, installs matching dynwinrt runtime/codegen packages, and generates bindings under .winapp\bindings.

For setup and restore details, see the WinApp CLI Electron setup guide. Its initialization steps also apply to a plain Node.js project; the Electron-specific debug-identity steps are not needed here.

2. Build the application and Node SEA executable

Build the application JavaScript first. The examples below assume the entry module is emitted as dist\main.js.

Install postject, the tool used by Node to inject the SEA blob:

npm install --save-dev postject@1.0.0-alpha.6

Create sea-bootstrap.cjs. The bootstrap is embedded in the executable, then loads the external application files from the installed package:

const { createRequire } = require('node:module')
const path = require('node:path')

const packageRoot = path.dirname(process.execPath)
process.chdir(packageRoot)

const externalRequire = createRequire(path.join(packageRoot, 'main.js'))
externalRequire('./main.js')

Create sea-config.json:

{
  "main": "sea-bootstrap.cjs",
  "output": "build/sea-prep.blob",
  "disableExperimentalSEAWarning": true,
  "useSnapshot": false,
  "useCodeCache": false
}

Download a pinned node.exe directly from the official Node.js distribution and save it as the application executable. This example uses Node.js 24.19.0 x64:

Node.js does not publish a separate SEA executable. The regular official node.exe already contains SEA support; postject turns a copy of it into the application executable by injecting the blob and enabling its SEA fuse.

New-Item .\build -ItemType Directory -Force

$nodeVersion = "24.19.0"
$nodeUrl = "https://nodejs.org/dist/v$nodeVersion/win-x64/node.exe"
$nodeSha256 = "3602f2bb1a10f2cbab4c36886218a33c1ab3db87290e73b033c46c77147d0237"
$nodeExe = ".\build\MyApp.exe"

Invoke-WebRequest $nodeUrl -OutFile $nodeExe

$actualSha256 = (
  Get-FileHash $nodeExe -Algorithm SHA256
).Hash.ToLowerInvariant()
if ($actualSha256 -ne $nodeSha256) {
  throw "Node.js executable SHA256 mismatch"
}

Before modifying it, use the downloaded executable to generate the SEA preparation blob:

& $nodeExe --experimental-sea-config .\sea-config.json

The official Node executable is Authenticode-signed. Injection changes the file, so remove that signature before running postject:

npx winapp tool signtool remove `
  /s .\build\MyApp.exe

Inject the blob and enable Node's SEA fuse:

npx postject `
  .\build\MyApp.exe `
  NODE_SEA_BLOB `
  .\build\sea-prep.blob `
  --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2

Postject updates build\MyApp.exe in place. The resulting executable starts the packaged main.js.

Before continuing, make sure these files exist:

build\MyApp.exe
dist\main.js

For x64 and ARM64, build separate executables and use the matching dynwinrt.node.

3. Configure the manifest and assets

winapp init already generated Package.appxmanifest and the image assets. Update the app name, publisher, version, and any required capabilities.

To replace the generated artwork:

npx winapp manifest update-assets `
  .\packaging\logo.svg `
  --manifest .\Package.appxmanifest

4. Stage the runtime files

Create the package layout and copy the files needed at runtime:

New-Item @(
  ".\layout\.winapp",
  ".\layout\node_modules\@microsoft",
  ".\artifacts"
) -ItemType Directory -Force

Copy-Item .\build\MyApp.exe .\layout
Copy-Item .\dist\* .\layout -Recurse
Copy-Item .\Package.appxmanifest .\layout
Copy-Item .\Assets .\layout\Assets -Recurse
Copy-Item .\package.json .\layout
Copy-Item .\.winapp\bindings .\layout\.winapp\bindings -Recurse
Copy-Item `
  .\node_modules\@microsoft\dynwinrt `
  .\layout\node_modules\@microsoft\dynwinrt `
  -Recurse

Adjust build and dist if the project uses different output directories.

5. Create a development certificate

For local testing:

npx winapp cert generate `
  --manifest .\layout\Package.appxmanifest `
  --output .\artifacts\devcert.pfx `
  --export-cer `
  --password password

Trust it once from an elevated terminal:

npx winapp cert install .\artifacts\devcert.pfx `
  --password password

The generated certificate is for development only. Never commit the PFX or its password.

Sign the final SEA executable after injection:

npx winapp sign `
  .\layout\MyApp.exe `
  .\artifacts\devcert.pfx `
  --password password

6. Build the MSIX

npx winapp pack `
  .\layout `
  --manifest .\layout\Package.appxmanifest `
  --executable MyApp.exe `
  --output .\artifacts\Contoso.DynWinRTApp_1.0.0.0_x64.msix `
  --cert .\artifacts\devcert.pfx `
  --cert-password password

WinApp CLI detects the executable architecture, resolves the manifest token, generates PRI resources, creates the MSIX, and signs it.

Inspect the result when diagnosing packaging issues:

npx winapp tool makeappx unpack `
  /p .\artifacts\Contoso.DynWinRTApp_1.0.0.0_x64.msix `
  /d .\artifacts\unpacked `
  /o

Get-AuthenticodeSignature `
  .\artifacts\Contoso.DynWinRTApp_1.0.0.0_x64.msix

Here /p is the input package, /d is the unpack destination, and /o allows overwriting an existing destination.

7. Install and launch

Add-AppxPackage `
  .\artifacts\Contoso.DynWinRTApp_1.0.0.0_x64.msix

Launch through the registered application identity:

$package = Get-AppxPackage -Name Contoso.DynWinRTApp
$app = Get-StartApps |
  Where-Object AppID -Like "$($package.PackageFamilyName)!*" |
  Select-Object -First 1

Start-Process explorer.exe `
  -ArgumentList "shell:AppsFolder\$($app.AppID)"

Verify the identity from application code:

const { hasPackageIdentity } = require('@microsoft/dynwinrt')

if (!hasPackageIdentity()) {
  throw new Error('The process was not launched with package identity')
}

Uninstall the development package:

Get-AppxPackage -Name Contoso.DynWinRTApp | Remove-AppxPackage

Windows App Runtime applications

A Node.js application that only calls stock Windows.* APIs does not need a Windows App Runtime dependency.

A Node.js application that calls Microsoft.Windows.* or WinUI APIs can either:

  • declare the matching Windows App Runtime framework package; or
  • use npx winapp pack --self-contained.

A framework-dependent packaged process must not call the unpackaged Windows App SDK bootstrap path. Package activation resolves the declared framework. Keep metadata, runtime files, generated projections, and native extensions on the same Windows App SDK version.

Optional: test the layout before packaging

Use winapp run when the application needs package identity during development. See Node.js development mode for the registration, launch, and cleanup workflows.

Writable application data

The installed package directory is immutable. The Node.js application must store logs, databases, caches, and user state elsewhere.

Packaged desktop applications can virtualize %LOCALAPPDATA% paths under:

%LOCALAPPDATA%\Packages\<package-family-name>\LocalCache\Local\

Use Windows.Storage.ApplicationData for package-scoped state. Use an explicit external location if data must survive uninstall. Test upgrade, uninstall, and reinstall behavior for the selected location.

x64 and ARM64

Build and test one layout per native architecture:

publish\
  x64\
    MyApp.exe
    node_modules\@microsoft\dynwinrt\...
  arm64\
    MyApp.exe
    node_modules\@microsoft\dynwinrt\...

For ARM64, repeat the SEA build with the same Node version's win-arm64/node.exe and the ARM64 dynwinrt runtime.

WinApp CLI 0.6.2 can create a multi-architecture bundle directly:

npx winapp pack `
  .\publish\x64 `
  .\publish\arm64 `
  --manifest .\layout\Package.appxmanifest `
  --executable MyApp.exe `
  --output .\artifacts\Contoso.DynWinRTApp_1.0.0.0_x64_arm64.msixbundle `
  --cert .\artifacts\devcert.pfx `
  --cert-password password

This produces one .msixbundle at --output. The bundle contains separate x64 and ARM64 MSIX packages, and Windows installs the matching architecture.

WinApp CLI validates that package identity, capabilities, and dependencies are consistent across slices and stamps each package with the detected PE architecture.

Production release

Use Microsoft Store signing or an organization signing service when possible. If CI uses a protected PFX, keep building and signing in separate jobs and timestamp the signatures:

$password = $env:MSIX_CERT_PASSWORD

npx winapp sign .\layout\MyApp.exe .\secure\release.pfx `
  --password $password `
  --timestamp https://timestamp.digicert.com

npx winapp pack .\layout `
  --manifest .\layout\Package.appxmanifest `
  --executable MyApp.exe `
  --output .\artifacts\Contoso.DynWinRTApp_1.0.0.0_x64.msix

npx winapp sign .\artifacts\Contoso.DynWinRTApp_1.0.0.0_x64.msix `
  .\secure\release.pfx `
  --password $password `
  --timestamp https://timestamp.digicert.com

Before publishing, test the exact signed artifact on a clean x64 and ARM64 machine:

  • install and uninstall;
  • Start menu/AUMID activation;
  • package identity and capabilities;
  • framework and VC runtime dependencies;
  • upgrade with preserved state; and
  • absence of source files, build tools, and certificates in the package.