Skip to content

Add automatic JS binding generation from WinRT metadata#375

Open
lei9444 wants to merge 4 commits intomainfrom
leilzh/generate
Open

Add automatic JS binding generation from WinRT metadata#375
lei9444 wants to merge 4 commits intomainfrom
leilzh/generate

Conversation

@lei9444
Copy link
Copy Markdown
Contributor

@lei9444 lei9444 commented Mar 31, 2026

Description

Add winapp node generate-bindings command that generates typed JS/TS bindings from WinRT metadata using winrt-meta.

  • Interactive setup after winapp init: Detects Node.js projects (package.json), prompts the user to generate bindings and select language (JS or TS), then auto-configures
    winapp.yaml and generates bindings
  • Auto-generate on winapp restore: Silently regenerates bindings if jsBindings is already configured in winapp.yaml
  • Auto-discover all WinAppSDK packages: Scans the NuGet dependency chain to find all packages with .winmd metadata (Foundation, AI, ML, Search, etc.) — no manual package
    configuration needed
  • Smart UI exclusion: Excludes UI-only packages (WinUI, Widgets) and winmd files (Microsoft.UI., Microsoft.Web.WebView2.) from code generation, but uses them as --ref for type
    resolution — zero warnings
  • Works across SDK versions: Handles both the 1.6-style flat layout (lib/uap10.0/) and 1.8+ sub-package structure (metadata/), tested with 1.6, 1.8, and 2.0
  • Auto-installs dynwinrt-js runtime dependency into the user's project
  • Ships winrt-meta as a CLI dependency — users don't need to install it separately

Usage Example

After winapp init in a Node.js project, the CLI prompts the user to optionally
generate JS or TS bindings. If accepted, it:

  1. Adds a jsBindings section to winapp.yaml with the chosen language
  2. Installs dynwinrt-js as a runtime dependency
  3. Auto-discovers WinAppSDK packages and generates bindings

On subsequent winapp restore, bindings are regenerated automatically if configured.

To regenerate manually: npx winapp node generate-bindings

Type of Change

  • ✨ New feature

Checklist

  • Tested locally on Windows
  • docs/usage.md updated (if CLI commands changed)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 31, 2026

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 30.67 MB 30.67 MB ✅ 0.0 KB (0.00%)
CLI (x64) 31.04 MB 31.04 MB ✅ 0.0 KB (0.00%)
MSIX (ARM64) 12.94 MB N/A N/A
MSIX (x64) 13.75 MB N/A N/A
NPM Package 26.91 MB N/A N/A
NuGet Package 27.00 MB N/A N/A
VS Code Extension 19.73 MB N/A N/A

Test Results

820 passed, 5 failed, 1 skipped out of 826 tests in 484.5s (+16.1s vs. baseline)

Test Coverage

21.5% line coverage, 36.7% branch coverage · ✅ no change vs. baseline

CLI Startup Time

52ms median (x64, winapp --version) · ⚠️ +14ms vs. baseline


Updated 2026-05-01 17:00:59 UTC · commit 634c41b · workflow run

Copilot AI review requested due to automatic review settings May 1, 2026 16:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Node.js support to auto-generate typed JS/TS bindings from WinRT metadata (WinMD) using winrt-meta, integrating generation into winapp init, winapp restore, and a new winapp node generate-bindings subcommand.

Changes:

  • Introduces js-bindings-utils.ts to parse jsBindings config, discover WinMDs from NuGet cache, and invoke winrt-meta to generate bindings.
  • Extends the Node wrapper CLI to prompt after init, auto-generate after restore, and add node generate-bindings.
  • Adds winrt-meta as an NPM dependency and documents JS bindings generation usage.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/winapp-npm/src/js-bindings-utils.ts New implementation for config parsing, NuGet/WinMD discovery, and winrt-meta invocation.
src/winapp-npm/src/cli.ts Adds node generate-bindings, post-init prompt, and post-restore generation hook.
src/winapp-npm/src/index.ts Exposes generateJsBindings and related types via programmatic API exports.
src/winapp-npm/package.json Adds winrt-meta dependency for bundled generation support.
src/winapp-npm/README.md Documents the new JS/TS bindings generation feature and API usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +603 to +610
const depRegex = /<dependency\s+id="([^"]+)"\s+version="([^"]+)"\s*\/>/g;
let match;
while ((match = depRegex.exec(content)) !== null) {
deps.push({
name: match[1],
version: match[2].replace(/[\[\]()]/g, ''),
});
}
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

readNuspecDeps() strips only brackets/parentheses from dependency version ranges. NuGet commonly uses ranges like [1.2.3, ), which would become 1.2.3, and won’t match a folder name in the NuGet cache, breaking discovery. Please parse version ranges properly (e.g., split on comma and take the minimum/left side, trimming whitespace) similar to the existing C# NugetService.ParseMinimumVersion behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +669 to +673
const versions = fs.readdirSync(pkgBaseDir)
.filter(d => fs.statSync(path.join(pkgBaseDir, d)).isDirectory())
.sort()
.reverse(); // newest first

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

findWinmdMetadata() selects “latest” by doing a simple string .sort().reverse() on version directories. This is lexicographic, so it can pick the wrong version (e.g., 9.0.0 sorting after 10.0.0). Use a semantic/numeric version comparison when scanning the NuGet cache (or prefer the pinned version and otherwise pick the highest semver).

Copilot uses AI. Check for mistakes.
Comment thread src/winapp-npm/src/cli.ts
Comment on lines +75 to +83
// Post-init: prompt user to generate JS/TS bindings if this is a Node.js project
if (command === 'init') {
await promptJsBindingsAfterInit({ verbose: args.includes('--verbose') });
}

// Post-restore: auto-generate JS bindings if already configured
if (command === 'restore') {
await autoGenerateJsBindings({ verbose: args.includes('--verbose') });
}
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

autoGenerateJsBindings() is called after every winapp restore, but generateJsBindings() currently auto-creates a default jsBindings section when none exists. This means restore can unexpectedly modify winapp.yaml and attempt generation even when the user hasn’t opted in (and will warn if no consumer package.json is found due to getProjectRootDir() throwing). Consider guarding the restore hook to only run when jsBindings is already present (and only in Node projects), and/or adding an option to generateJsBindings() to disable auto-init behavior for the restore path.

Copilot uses AI. Check for mistakes.
Comment thread src/winapp-npm/src/cli.ts
Comment on lines +318 to +321
case 'generate-bindings':
await handleGenerateBindings(subcommandArgs);
break;

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

Shell completion support won’t suggest the new node generate-bindings subcommand because the completion list is driven by NODE_SUBCOMMANDS. Please add generate-bindings to the subcommand list used by handleComplete so completions match the help/switch cases.

Copilot uses AI. Check for mistakes.
Comment thread src/winapp-npm/src/cli.ts
console.log(' classes: StorageFile');
console.log('');
console.log('Prerequisites:');
console.log(' - winrt-meta must be installed (npm install -D winrt-meta)');
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

The help text says users must install winrt-meta manually (npm install -D winrt-meta), but this PR adds winrt-meta as a dependency of @microsoft/winappcli. Update this prerequisite text to avoid contradicting the actual packaging behavior.

Suggested change
console.log(' - winrt-meta must be installed (npm install -D winrt-meta)');
console.log(' - winrt-meta is included with @microsoft/winappcli');

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +80
let config = readJsBindingsConfig(configPath);
if (!config) {
// Auto-create jsBindings section with defaults
appendDefaultJsBindingsConfig(configPath);
config = readJsBindingsConfig(configPath);
if (!config) {
return { generated: false, skipReason: 'failed to initialize jsBindings config' };
}
console.log(`[js-bindings] added default jsBindings config to ${path.basename(configPath)}`);
}
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

generateJsBindings() appends a default jsBindings section whenever one is missing. That’s fine for an explicit node generate-bindings invocation, but it conflicts with the stated behavior of autoGenerateJsBindings() (“skip if not configured”) and can cause unexpected file modifications on restore. Consider splitting “read config” vs “initialize config” responsibilities (e.g., add a createIfMissing option defaulting to false, and only enable it for interactive/manual flows).

Copilot uses AI. Check for mistakes.
Comment on lines +401 to +406
if (trimmed === 'packages:') {
inPackages = true;
continue;
}
// Another top-level section ends packages
if (!line.startsWith(' ') && !line.startsWith('\t') && trimmed !== 'packages:') {
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

readPackageVersions() treats any line that trims to packages: as the start of the top-level packages section, even if it’s indented (e.g., jsBindings: ... packages:). This can make it parse the wrong section depending on ordering. Restrict the match to top-level (non-indented) packages: only, similar to how readJsBindingsConfig() detects top-level sections.

Suggested change
if (trimmed === 'packages:') {
inPackages = true;
continue;
}
// Another top-level section ends packages
if (!line.startsWith(' ') && !line.startsWith('\t') && trimmed !== 'packages:') {
const isTopLevel = !line.startsWith(' ') && !line.startsWith('\t');
if (isTopLevel && trimmed === 'packages:') {
inPackages = true;
continue;
}
// Another top-level section ends packages
if (isTopLevel && trimmed !== 'packages:') {

Copilot uses AI. Check for mistakes.
Comment on lines +460 to +474

for (const [name, version] of packageVersions) {
// Check the root package itself for .winmd files
checkAndAddPackage(name, version, nugetCache, discovered, seen, verbose);

// Check its NuGet dependencies for .winmd files
const deps = readNuspecDeps(nugetCache, name, version);
for (const dep of deps) {
checkAndAddPackage(dep.name, dep.version, nugetCache, discovered, seen, verbose);
}
}

return discovered;
}

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

Auto-discovery only inspects each root package’s direct .nuspec dependencies. The PR description claims scanning the dependency chain / transitive deps; as written, packages that are only reachable via a dependency-of-a-dependency will never be discovered. Consider making discoverPackages recursively traverse dependencies (with seen protection) to match the intended behavior.

Suggested change
for (const [name, version] of packageVersions) {
// Check the root package itself for .winmd files
checkAndAddPackage(name, version, nugetCache, discovered, seen, verbose);
// Check its NuGet dependencies for .winmd files
const deps = readNuspecDeps(nugetCache, name, version);
for (const dep of deps) {
checkAndAddPackage(dep.name, dep.version, nugetCache, discovered, seen, verbose);
}
}
return discovered;
}
const traversed = new Set<string>();
for (const [name, version] of packageVersions) {
discoverPackageRecursive(
name,
version,
nugetCache,
discovered,
seen,
traversed,
verbose
);
}
return discovered;
}
function discoverPackageRecursive(
name: string,
version: string | undefined,
nugetCache: string,
discovered: JsBindingsPackageEntry[],
seen: Set<string>,
traversed: Set<string>,
verbose: boolean
): void {
const lower = name.toLowerCase();
if (traversed.has(lower) || EXCLUDED_PACKAGES.has(lower)) return;
traversed.add(lower);
if (!version) return;
checkAndAddPackage(name, version, nugetCache, discovered, seen, verbose);
const deps = readNuspecDeps(nugetCache, name, version);
for (const dep of deps) {
discoverPackageRecursive(
dep.name,
dep.version,
nugetCache,
discovered,
seen,
traversed,
verbose
);
}
}

Copilot uses AI. Check for mistakes.
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.

3 participants