Add automatic JS binding generation from WinRT metadata#375
Add automatic JS binding generation from WinRT metadata#375
Conversation
Build Metrics ReportBinary Sizes
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 Time52ms median (x64, Updated 2026-05-01 17:00:59 UTC · commit |
There was a problem hiding this comment.
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.tsto parsejsBindingsconfig, discover WinMDs from NuGet cache, and invokewinrt-metato generate bindings. - Extends the Node wrapper CLI to prompt after
init, auto-generate afterrestore, and addnode generate-bindings. - Adds
winrt-metaas 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.
| 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, ''), | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
| const versions = fs.readdirSync(pkgBaseDir) | ||
| .filter(d => fs.statSync(path.join(pkgBaseDir, d)).isDirectory()) | ||
| .sort() | ||
| .reverse(); // newest first | ||
|
|
There was a problem hiding this comment.
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).
| // 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') }); | ||
| } |
There was a problem hiding this comment.
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.
| case 'generate-bindings': | ||
| await handleGenerateBindings(subcommandArgs); | ||
| break; | ||
|
|
There was a problem hiding this comment.
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.
| console.log(' classes: StorageFile'); | ||
| console.log(''); | ||
| console.log('Prerequisites:'); | ||
| console.log(' - winrt-meta must be installed (npm install -D winrt-meta)'); |
There was a problem hiding this comment.
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.
| console.log(' - winrt-meta must be installed (npm install -D winrt-meta)'); | |
| console.log(' - winrt-meta is included with @microsoft/winappcli'); |
| 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)}`); | ||
| } |
There was a problem hiding this comment.
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).
| if (trimmed === 'packages:') { | ||
| inPackages = true; | ||
| continue; | ||
| } | ||
| // Another top-level section ends packages | ||
| if (!line.startsWith(' ') && !line.startsWith('\t') && trimmed !== 'packages:') { |
There was a problem hiding this comment.
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.
| 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:') { |
|
|
||
| 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; | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| 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 | |
| ); | |
| } | |
| } |
Description
Add winapp node generate-bindings command that generates typed JS/TS bindings from WinRT metadata using winrt-meta.
winapp.yaml and generates bindings
configuration needed
resolution — zero warnings
Usage Example
After
winapp initin a Node.js project, the CLI prompts the user to optionallygenerate JS or TS bindings. If accepted, it:
jsBindingssection towinapp.yamlwith the chosen languagedynwinrt-jsas a runtime dependencyOn subsequent
winapp restore, bindings are regenerated automatically if configured.To regenerate manually:
npx winapp node generate-bindingsType of Change
Checklist