Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion src/winapp-npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,62 @@ npx winapp --help
**Node.js/Electron Specific:**

- [`node create-addon`](https://github.com/microsoft/WinAppCli/blob/main/docs/usage.md#node-create-addon) - Generate native C# or C++ addons
- [`node generate-bindings`](#js-bindings-generation) - Generate JS/TS bindings from WinRT metadata
- [`node add-electron-debug-identity`](https://github.com/microsoft/WinAppCli/blob/main/docs/usage.md#node-add-electron-debug-identity) - Add identity to Electron processes

The full CLI usage can be found here: [Documentation](https://github.com/microsoft/WinAppCli/blob/main/docs/usage.md)

### JS Bindings Generation

The CLI can automatically generate typed JavaScript bindings for Windows Runtime APIs using [winrt-meta](https://www.npmjs.com/package/winrt-meta).

**How it works:**
- After `winapp restore` or `winapp init`, the CLI automatically discovers the Windows App SDK AI metadata package from the NuGet dependency chain and generates JS bindings
- Runs automatically if `winrt-meta` is installed in the project — no manual scripts needed
- On first run, adds a default `jsBindings` section to `winapp.yaml`

**Setup:**

`winrt-meta` is included as a dependency — no extra install needed.

```bash
npx winapp restore # auto-generates bindings after restore
```

**Manual re-generation:**

```bash
npx winapp node generate-bindings [--verbose]
```

**Optional configuration** in `winapp.yaml` for additional Windows SDK types needed by your app:

```yaml
jsBindings:
lang: js # js (default) | cjs | ts
output: generated-js # default: generated-js
systemTypes:
- namespace: Windows.Storage
classes: StorageFile
- namespace: Windows.Graphics.Imaging
classes: BitmapDecoder
```

The AI metadata package and version are auto-discovered — only `systemTypes` needs manual configuration for app-level dependencies not present in the AI API signatures.

### Programmatic API

The package also exports typed async functions for all CLI commands and utility helpers, so you can use them directly from TypeScript/JavaScript without spawning a CLI process:

```typescript
import { init, packageApp, certGenerate } from '@microsoft/winappcli';
import { init, packageApp, certGenerate, generateJsBindings } from '@microsoft/winappcli';

await init({ useDefaults: true });
await certGenerate({ install: true });
await packageApp({ inputFolder: './dist', cert: './devcert.pfx' });

// Generate JS bindings from WinRT metadata
await generateJsBindings({ verbose: true });
```

Full programmatic API reference: [NPM API Documentation](https://github.com/microsoft/WinAppCli/blob/main/docs/npm-usage.md)
Expand Down
3 changes: 3 additions & 0 deletions src/winapp-npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
"os": [
"win32"
],
"dependencies": {
"winrt-meta": "^0.1.6"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^25.5.0",
Expand Down
176 changes: 176 additions & 0 deletions src/winapp-npm/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { generateCppAddonFiles } from './cpp-addon-utils';
import { generateCsAddonFiles } from './cs-addon-utils';
import { addElectronDebugIdentity, clearElectronDebugIdentity } from './msix-utils';
import { getWinappCliPath, callWinappCli, callWinappCliCapture, WINAPP_CLI_CALLER_VALUE } from './winapp-cli-utils';
import { autoGenerateJsBindings, generateJsBindings, readJsBindingsConfig } from './js-bindings-utils';
import { getProjectRootDir } from './utils';
import { spawn } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline';

// CLI name - change this to rebrand the tool
const CLI_NAME = 'winapp';
Expand Down Expand Up @@ -67,6 +71,16 @@ export async function main(): Promise<void> {

// Route everything else to winapp-cli
await callWinappCli(args, { exitOnError: true });

// 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') });
}
Comment on lines +75 to +83
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.
} catch (error) {
logErrorAndExit(error);
}
Expand Down Expand Up @@ -204,6 +218,7 @@ async function showCombinedHelp(): Promise<void> {
console.log('');
console.log('Node.js Subcommands:');
console.log(' node create-addon Generate native addon files for Electron');
console.log(' node generate-bindings Generate JS/TS bindings from WinRT metadata');
console.log(' node add-electron-debug-identity Add package identity to Electron debug process');
console.log(' node clear-electron-debug-identity Remove package identity from Electron debug process');
console.log('');
Expand Down Expand Up @@ -268,13 +283,15 @@ async function handleNode(args: string[]): Promise<void> {
console.log('');
console.log('Subcommands:');
console.log(' create-addon Generate native addon files for Electron');
console.log(' generate-bindings Generate JS/TS bindings from WinRT metadata');
console.log(' add-electron-debug-identity Add package identity to Electron debug process');
console.log(' clear-electron-debug-identity Remove package identity from Electron debug process');
console.log('');
console.log('Examples:');
console.log(` ${CLI_NAME} node create-addon --help`);
console.log(` ${CLI_NAME} node create-addon --name myAddon`);
console.log(` ${CLI_NAME} node create-addon --name myCsAddon --template cs`);
console.log(` ${CLI_NAME} node generate-bindings`);
console.log(` ${CLI_NAME} node add-electron-debug-identity`);
console.log(` ${CLI_NAME} node clear-electron-debug-identity`);
console.log('');
Expand All @@ -298,6 +315,10 @@ async function handleNode(args: string[]): Promise<void> {
await handleClearElectronDebugIdentity(subcommandArgs);
break;

case 'generate-bindings':
await handleGenerateBindings(subcommandArgs);
break;

Comment on lines +318 to +321
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.
default:
console.error(`❌ Unknown node subcommand: ${subcommand}`);
console.error(`Run "${CLI_NAME} node" for available subcommands.`);
Expand Down Expand Up @@ -504,6 +525,161 @@ async function handleClearElectronDebugIdentity(args: string[]): Promise<void> {
}
}

async function handleGenerateBindings(args: string[]): Promise<void> {
const options = parseArgs(args, { verbose: false });

if (options.help) {
console.log(`Usage: ${CLI_NAME} node generate-bindings [options]`);
console.log('');
console.log('Generate JS/TS bindings from WinRT metadata');
console.log('');
console.log('Reads the jsBindings section from winapp.yaml and generates typed');
console.log('bindings using winrt-meta. This runs automatically after restore/init');
console.log('if configured, but can also be called manually.');
console.log('');
console.log('Options:');
console.log(' --verbose Enable verbose output (default: false)');
console.log(' --help Show this help');
console.log('');
console.log('winapp.yaml configuration:');
console.log(' jsBindings:');
console.log(' lang: js # js | cjs | ts (default: js)');
console.log(' output: generated-js # output directory');
console.log(' packages: # NuGet packages with .winmd metadata');
console.log(' - Microsoft.WindowsAppSDK.AI');
console.log(' systemTypes: # additional Windows SDK classes');
console.log(' - namespace: Windows.Storage');
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.
console.log(' - Packages must be restored first (winapp restore)');
return;
}

try {
const result = await generateJsBindings({
verbose: options.verbose as boolean,
});

if (!result.generated) {
console.error(`❌ ${result.skipReason}`);
process.exit(1);
}
} catch (error) {
logErrorAndExit(error);
}
}

// ======================================================================
// Post-init: interactive JS/TS bindings prompt
// ======================================================================

function askQuestion(rl: readline.Interface, question: string): Promise<string> {
return new Promise((resolve) => {
rl.question(question, (answer) => resolve(answer.trim()));
});
}

async function promptJsBindingsAfterInit(options: { verbose?: boolean }): Promise<void> {
const projectRoot = getProjectRootDir();

// Only prompt for Node.js projects
if (!fs.existsSync(path.join(projectRoot, 'package.json'))) {
return;
}

// Skip if jsBindings already configured
const yamlPath = findWinappYamlPath(projectRoot);
if (yamlPath && readJsBindingsConfig(yamlPath)) {
await autoGenerateJsBindings(options);
return;
}

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

try {
console.log('');
const generate = await askQuestion(
rl,
'Would you like to generate JavaScript/TypeScript bindings for WinRT APIs? [Y/n] '
);

if (generate.toLowerCase() === 'n' || generate.toLowerCase() === 'no') {
return;
}

console.log('');
console.log('Select binding language:');
console.log(' 1. JavaScript (js)');
console.log(' 2. TypeScript (ts)');
const langChoice = await askQuestion(rl, 'Enter choice [1/2]: ');

let lang: string;
let output: string;
if (langChoice === '2' || langChoice.toLowerCase() === 'ts') {
lang = 'ts';
output = 'generated';
} else {
lang = 'js';
output = 'generated-js';
}

// Write jsBindings config to winapp.yaml
if (yamlPath) {
let content = fs.readFileSync(yamlPath, 'utf8');
if (!content.endsWith('\n')) {
content += '\n';
}
content += `\njsBindings:\n lang: ${lang}\n output: ${output}\n`;
fs.writeFileSync(yamlPath, content, 'utf8');
console.log(`\nAdded jsBindings config to ${path.basename(yamlPath)} (lang: ${lang}, output: ${output})`);
}

// Install dynwinrt-js runtime dependency
console.log('\nInstalling dynwinrt-js runtime dependency...');
try {
const { execSync } = require('child_process');
execSync('npm install dynwinrt-js', { cwd: projectRoot, stdio: 'inherit' });
} catch {
console.warn('Warning: failed to install dynwinrt-js. Install it manually: npm install dynwinrt-js');
}

// Generate bindings
const result = await generateJsBindings({ verbose: options.verbose });

if (result.generated) {
console.log('');
console.log('To use the generated bindings in your code:');
console.log(` const { ClassName } = require('./${output}/NamespaceName');`);
console.log('');
console.log('To regenerate bindings manually:');
console.log(` npx ${CLI_NAME} node generate-bindings`);
console.log('');
console.log(`Bindings will also regenerate automatically on '${CLI_NAME} restore'.`);
} else {
console.log(`\nNo bindings generated: ${result.skipReason}`);
console.log(`You can configure packages in winapp.yaml and run '${CLI_NAME} node generate-bindings' later.`);
}
} finally {
rl.close();
}
}

function findWinappYamlPath(projectRoot: string): string | null {
const candidates = [
path.join(projectRoot, 'winapp.yaml'),
path.join(projectRoot, '.winapp', 'winapp.yaml'),
];
for (const p of candidates) {
if (fs.existsSync(p)) return p;
}
return null;
}

function logErrorAndExit(error: unknown): never {
if (error instanceof Error && error.message.includes('winapp-cli exited with code')) {
process.exit(1);
Expand Down
11 changes: 11 additions & 0 deletions src/winapp-npm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { execSyncWithBuildTools } from './buildtools-utils';
import { addMsixIdentityToExe, addElectronDebugIdentity, clearElectronDebugIdentity } from './msix-utils';
import { getGlobalWinappPath, getLocalWinappPath } from './winapp-path-utils';
import { generateJsBindings } from './js-bindings-utils';
import * as winappCommands from './winapp-commands';

// Re-export types from child_process for convenience
Expand All @@ -22,6 +23,12 @@ export {
} from './winapp-cli-utils';
export { GenerateCppAddonOptions, GenerateCppAddonResult } from './cpp-addon-utils';
export { GenerateCsAddonOptions, GenerateCsAddonResult } from './cs-addon-utils';
export {
GenerateJsBindingsOptions,
GenerateJsBindingsResult,
JsBindingsConfig,
JsBindingsPackageEntry,
} from './js-bindings-utils';

// Re-export all command types and functions automatically
export * from './winapp-commands';
Expand All @@ -39,6 +46,9 @@ export {
// winapp directory utilities
getGlobalWinappPath,
getLocalWinappPath,

// JS/TS bindings generation
generateJsBindings,
};

// Default export for CommonJS compatibility
Expand All @@ -49,5 +59,6 @@ export default {
clearElectronDebugIdentity,
getGlobalWinappPath,
getLocalWinappPath,
generateJsBindings,
...winappCommands,
};
Loading
Loading