@involvex/msix-packager-cli 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/package.js ADDED
@@ -0,0 +1,909 @@
1
+ const path = require("path");
2
+ const fs = require("fs-extra");
3
+ const {
4
+ executeCommand,
5
+ findWindowsSDKTools,
6
+ formatFileSize,
7
+ getLatestNodeVersion,
8
+ } = require("./utils");
9
+ const { determineSigningMethod } = require("./certificates");
10
+ const {
11
+ generateManifest,
12
+ createDefaultAssets,
13
+ generateResourceConfig,
14
+ } = require("./manifest");
15
+ const { CONSTANTS, MSIXError, SigningError } = require("./constants");
16
+
17
+ /**
18
+ * Creates an MSIX package
19
+ * @param {string} packageDir - Directory containing package contents
20
+ * @param {string} outputPath - Output MSIX file path
21
+ * @returns {string} Path to created MSIX file
22
+ */
23
+ async function createMsixPackage(packageDir, outputPath) {
24
+ try {
25
+ console.log("Creating MSIX package...");
26
+
27
+ const tools = findWindowsSDKTools();
28
+
29
+ // Ensure output directory exists
30
+ const outputDir = path.dirname(outputPath);
31
+ await fs.ensureDir(outputDir);
32
+
33
+ // Create the MSIX package using makeappx
34
+ const command = `"${tools.makeappx}" pack /d "${packageDir}" /p "${outputPath}" /overwrite`;
35
+
36
+ console.log("Running makeappx...");
37
+ executeCommand(command);
38
+
39
+ // Verify the package was created
40
+ if (!(await fs.pathExists(outputPath))) {
41
+ throw new MSIXError("MSIX package was not created successfully");
42
+ }
43
+
44
+ const stats = await fs.stat(outputPath);
45
+ console.log(
46
+ `MSIX package created: ${outputPath} (${formatFileSize(stats.size)})`,
47
+ );
48
+
49
+ return outputPath;
50
+ } catch (error) {
51
+ throw new MSIXError(`Failed to create MSIX package: ${error.message}`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Signs an MSIX package
57
+ * @param {string} packagePath - Path to MSIX package to sign
58
+ * @param {Object} signingConfig - Signing configuration
59
+ * @returns {boolean} True if signing was successful
60
+ */
61
+ async function signMsixPackage(packagePath, signingConfig) {
62
+ try {
63
+ const signingMethod = await determineSigningMethod(signingConfig);
64
+
65
+ if (!signingMethod) {
66
+ console.log("Signing disabled, skipping...");
67
+ return false;
68
+ }
69
+
70
+ console.log(`Signing package using ${signingMethod.method} method...`);
71
+
72
+ const tools = findWindowsSDKTools();
73
+ let signCommand;
74
+
75
+ if (signingMethod.method === "pfx") {
76
+ // Sign with PFX file
77
+ const { pfxPath, password, timestampUrl } = signingMethod.parameters;
78
+ signCommand = `"${tools.signtool}" sign /f "${pfxPath}" /p "${password}" /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${packagePath}"`;
79
+ } else if (signingMethod.method === "store") {
80
+ // Sign with certificate from store
81
+ const { thumbprint, store, timestampUrl } = signingMethod.parameters;
82
+ const storeLocation = store.toLowerCase() === "localmachine" ? "/sm" : "";
83
+ console.log(
84
+ `Using certificate from ${store} store with thumbprint: ${thumbprint}`,
85
+ );
86
+
87
+ // For test certificates, try the most basic signing approach first
88
+ if (timestampUrl && timestampUrl.length > 0) {
89
+ try {
90
+ signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${packagePath}"`;
91
+ console.log("Attempting basic signing without timestamp first...");
92
+ executeCommand(signCommand);
93
+ console.log(
94
+ "Basic signing successful, package signed without timestamp",
95
+ );
96
+ return true;
97
+ } catch (basicError) {
98
+ console.log("Basic signing failed, trying with timestamp...");
99
+ signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${packagePath}"`;
100
+ }
101
+ } else {
102
+ console.log("Signing without timestamp server...");
103
+ signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${packagePath}"`;
104
+ }
105
+ } else {
106
+ throw new SigningError(`Unknown signing method: ${signingMethod.method}`);
107
+ }
108
+
109
+ console.log("Running signtool...");
110
+ executeCommand(signCommand);
111
+
112
+ console.log("Package signed successfully");
113
+ return true;
114
+ } catch (error) {
115
+ throw new SigningError(`Failed to sign MSIX package: ${error.message}`);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Prepares the package directory structure with enhanced workflow
121
+ * This implements the proper Node.js app packaging workflow:
122
+ * 1. Copy source code to temp directory
123
+ * 2. Build the application (install deps, run build scripts)
124
+ * 3. Create executable structure
125
+ * 4. Create proper MSIX directory structure
126
+ * @param {Object} config - Configuration object
127
+ * @param {string} tempDir - Temporary directory for package preparation
128
+ * @param {Object} packageJson - package.json content
129
+ * @returns {string} Path to prepared package directory
130
+ */
131
+ async function preparePackageDirectory(config, tempDir, packageJson) {
132
+ try {
133
+ console.log("🏗️ Preparing package directory structure...");
134
+
135
+ // Step 1: Create temporary directories for the enhanced workflow
136
+ const sourceDir = path.join(tempDir, "source");
137
+ const buildDir = path.join(tempDir, "build");
138
+ const packageDir = path.join(tempDir, "package");
139
+ const assetsDir = path.join(packageDir, "Assets");
140
+
141
+ await fs.ensureDir(sourceDir);
142
+ await fs.ensureDir(buildDir);
143
+ await fs.ensureDir(packageDir);
144
+ await fs.ensureDir(assetsDir);
145
+
146
+ // Step 2: Copy source code to temporary directory
147
+ console.log("📁 Copying source code to temporary directory...");
148
+ const { copyFiles } = require("./utils");
149
+ await copyFiles(config.inputPath, sourceDir, true);
150
+
151
+ // Step 3: Build the application (install dependencies and run build scripts)
152
+ console.log("🔧 Building the application...");
153
+ await buildApplication(sourceDir, buildDir, packageJson, config);
154
+
155
+ // Step 4: Create executable structure
156
+ console.log("⚙️ Creating executable structure...");
157
+ await createExecutableStructure(buildDir, packageDir, config, packageJson);
158
+
159
+ // Step 5: Generate MSIX directory structure
160
+ console.log("📦 Setting up MSIX directory structure...");
161
+ await setupMsixStructure(packageDir, assetsDir, config, packageJson);
162
+
163
+ console.log("✅ Package directory prepared successfully");
164
+ return packageDir;
165
+ } catch (error) {
166
+ throw new MSIXError(
167
+ `Failed to prepare package directory: ${error.message}`,
168
+ );
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Builds the Node.js application in the build directory
174
+ * @param {string} sourceDir - Source directory path
175
+ * @param {string} buildDir - Build directory path
176
+ * @param {Object} packageJson - package.json content
177
+ * @param {Object} config - Configuration object
178
+ */
179
+ async function buildApplication(sourceDir, buildDir, packageJson, config) {
180
+ try {
181
+ // Copy source files to build directory with filtering
182
+ console.log("📋 Copying source files for build...");
183
+ await fs.copy(sourceDir, buildDir, {
184
+ overwrite: true,
185
+ errorOnExist: false,
186
+ filter: (src) => {
187
+ // Skip node_modules, build artifacts, and temporary files
188
+ const relativePath = path.relative(sourceDir, src);
189
+ const excludePatterns = [
190
+ "node_modules",
191
+ ".git",
192
+ "dist",
193
+ "build",
194
+ ".nyc_output",
195
+ "coverage",
196
+ ".vscode",
197
+ ".idea",
198
+ "*.log",
199
+ ".env",
200
+ ];
201
+
202
+ return !excludePatterns.some(
203
+ (pattern) =>
204
+ relativePath.includes(pattern) ||
205
+ relativePath.endsWith(pattern.replace("*", "")),
206
+ );
207
+ },
208
+ });
209
+
210
+ // Install dependencies (include dev dependencies if build script exists or configured)
211
+ const packageJsonPath = path.join(buildDir, "package.json");
212
+ if (await fs.pathExists(packageJsonPath)) {
213
+ const needsDevDeps =
214
+ config.installDevDeps &&
215
+ ((packageJson.scripts && packageJson.scripts.build) ||
216
+ config.installDevDeps === true);
217
+ if (needsDevDeps) {
218
+ console.log(
219
+ "📦 Installing all dependencies (including dev dependencies for build)...",
220
+ );
221
+ await installNodeDependencies(buildDir, false); // false = don't production-only
222
+ } else {
223
+ console.log("📦 Installing production dependencies...");
224
+ await installNodeDependencies(buildDir, true); // true = production-only
225
+ }
226
+ }
227
+
228
+ // Run build script if defined in package.json and not skipped
229
+ if (!config.skipBuild && packageJson.scripts && packageJson.scripts.build) {
230
+ console.log("🔨 Running build script...");
231
+ const originalCwd = process.cwd();
232
+ try {
233
+ process.chdir(buildDir);
234
+ executeCommand("bun run build");
235
+ } catch (buildError) {
236
+ console.warn(`⚠️ Build script failed: ${buildError.message}`);
237
+
238
+ // Check if this is a TypeScript build issue
239
+ if (
240
+ buildError.message.includes("tsc") ||
241
+ buildError.message.includes("TypeScript")
242
+ ) {
243
+ console.log(
244
+ "🔧 Detected TypeScript build failure. Attempting to install TypeScript...",
245
+ );
246
+ try {
247
+ // Try to install TypeScript locally
248
+ executeCommand("bun install typescript --save-dev --silent");
249
+ console.log("📦 TypeScript installed. Retrying build...");
250
+ executeCommand("bun run build");
251
+ console.log("✅ Build succeeded after installing TypeScript");
252
+ } catch (retryError) {
253
+ console.warn(
254
+ `⚠️ Build still failed after installing TypeScript: ${retryError.message}`,
255
+ );
256
+ console.log(
257
+ "📝 Continuing without build step. You may need to manually build your application before packaging.",
258
+ );
259
+ }
260
+ } else {
261
+ console.warn(
262
+ "📝 Continuing without build step. Ensure your application is pre-built if needed.",
263
+ );
264
+ }
265
+ } finally {
266
+ process.chdir(originalCwd);
267
+ }
268
+ } else if (config.skipBuild) {
269
+ console.log("⏭️ Skipping build step as requested in configuration");
270
+ }
271
+
272
+ console.log("✅ Application built successfully");
273
+ } catch (error) {
274
+ throw new MSIXError(`Failed to build application: ${error.message}`);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Creates the executable structure for the MSIX package
280
+ * @param {string} buildDir - Build directory path
281
+ * @param {string} packageDir - Package directory path
282
+ * @param {Object} config - Configuration object
283
+ * @param {Object} packageJson - package.json content
284
+ */
285
+ async function createExecutableStructure(
286
+ buildDir,
287
+ packageDir,
288
+ config,
289
+ packageJson,
290
+ ) {
291
+ try {
292
+ // Check for pre-built executable (e.g., bun-compiled binary)
293
+ if (config.prebuiltExecutablePath) {
294
+ const prebuiltPath = path.resolve(
295
+ config.inputPath,
296
+ config.prebuiltExecutablePath,
297
+ );
298
+ if (await fs.pathExists(prebuiltPath)) {
299
+ const exeName = path.basename(prebuiltPath);
300
+ console.log(`✅ Using pre-built executable: ${exeName}`);
301
+ await fs.copyFile(prebuiltPath, path.join(packageDir, exeName));
302
+ return;
303
+ }
304
+ throw new Error(
305
+ `Pre-built executable not found: ${prebuiltPath}. Run the compile step first.`,
306
+ );
307
+ }
308
+
309
+ // Copy built application files to package directory
310
+ console.log("📁 Copying built application files...");
311
+ await fs.copy(buildDir, packageDir, {
312
+ overwrite: true,
313
+ errorOnExist: false,
314
+ filter: (src) => {
315
+ // Skip development files
316
+ const basename = path.basename(src);
317
+ const excludeFiles = [
318
+ ".gitignore",
319
+ ".env.example",
320
+ "README.md",
321
+ "CHANGELOG.md",
322
+ "LICENSE",
323
+ "tsconfig.json",
324
+ "jest.config.js",
325
+ "webpack.config.js",
326
+ ];
327
+ return !excludeFiles.includes(basename);
328
+ },
329
+ });
330
+
331
+ // Ensure Node.js runtime is available
332
+ console.log("🟢 Ensuring Node.js runtime...");
333
+ await ensureNodeRuntime(packageDir, config);
334
+
335
+ // Create startup script if the main executable is node.exe
336
+ if (
337
+ config.executable === "node.exe" ||
338
+ config.executable.endsWith("node.exe")
339
+ ) {
340
+ await createNodeStartupScript(packageDir, packageJson, config);
341
+ }
342
+
343
+ // Try to create SEA (Single Executable Application) first
344
+ const {
345
+ createSingleExecutableApp,
346
+ createFallbackLauncher,
347
+ } = require("./sea-handler");
348
+ const seaSuccess = await createSingleExecutableApp(
349
+ packageDir,
350
+ config,
351
+ packageJson,
352
+ );
353
+
354
+ // Create fallback launcher if SEA creation failed
355
+ if (!seaSuccess) {
356
+ console.log("📄 Creating fallback Node.js launcher...");
357
+ await createFallbackLauncher(packageDir, config, packageJson);
358
+ }
359
+
360
+ console.log("✅ Executable structure created successfully");
361
+ } catch (error) {
362
+ throw new MSIXError(
363
+ `Failed to create executable structure: ${error.message}`,
364
+ );
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Sets up the MSIX directory structure with manifests and assets
370
+ * @param {string} packageDir - Package directory path
371
+ * @param {string} assetsDir - Assets directory path
372
+ * @param {Object} config - Configuration object
373
+ * @param {Object} packageJson - package.json content
374
+ */
375
+ async function setupMsixStructure(packageDir, assetsDir, config, packageJson) {
376
+ try {
377
+ // Generate AppxManifest.xml
378
+ console.log("📄 Generating AppxManifest.xml...");
379
+ const manifestContent = generateManifest(config, packageJson);
380
+ await fs.writeFile(
381
+ path.join(packageDir, "AppxManifest.xml"),
382
+ manifestContent,
383
+ );
384
+
385
+ // Create package assets
386
+ console.log("🎨 Creating package assets...");
387
+ await createDefaultAssets(assetsDir, config.icon);
388
+
389
+ // Create MCP Server configuration
390
+ console.log("🔧 Creating MCP Server configuration...");
391
+ await createMcpServerConfig(assetsDir, config);
392
+
393
+ // Generate resource configuration
394
+ console.log("⚙️ Generating resource configuration...");
395
+ const resourceConfig = generateResourceConfig(config);
396
+ await fs.writeFile(
397
+ path.join(packageDir, "msix-resource-config.json"),
398
+ resourceConfig,
399
+ );
400
+
401
+ // Create registry entries if needed
402
+ await createRegistryEntries(packageDir, config, packageJson);
403
+
404
+ console.log("✅ MSIX structure setup completed");
405
+ } catch (error) {
406
+ throw new MSIXError(`Failed to setup MSIX structure: ${error.message}`);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Creates a Node.js startup script
412
+ * @param {string} packageDir - Package directory path
413
+ * @param {Object} packageJson - package.json content
414
+ * @param {Object} config - Configuration object
415
+ */
416
+ async function createNodeStartupScript(packageDir, packageJson, config) {
417
+ try {
418
+ const mainScript = packageJson.main || "index.js";
419
+ const startScript = packageJson.scripts && packageJson.scripts.start;
420
+
421
+ // Create batch file to start the Node.js application
422
+ let batchContent;
423
+
424
+ if (startScript && !startScript.includes("node ")) {
425
+ // Use the start script if it doesn't already include 'node'
426
+ batchContent = `@echo off
427
+ title ${config.displayName || config.appName}
428
+ cd /d "%~dp0"
429
+ echo Starting ${config.displayName || config.appName}...
430
+ bun run start
431
+ if errorlevel 1 (
432
+ echo.
433
+ echo Failed to start the application.
434
+ echo Make sure Node.js is installed on this system.
435
+ echo.
436
+ pause
437
+ exit /b 1
438
+ )
439
+ `;
440
+ } else {
441
+ // Create direct node execution with configurable args
442
+ const nodeArgs = config.executableArgs || mainScript;
443
+ batchContent = `@echo off
444
+ title ${config.displayName || config.appName}
445
+ cd /d "%~dp0"
446
+ echo Starting ${config.displayName || config.appName}...
447
+ node ${nodeArgs} %*
448
+ if errorlevel 1 (
449
+ echo.
450
+ echo Failed to start the application.
451
+ echo Make sure Node.js is installed on this system.
452
+ echo.
453
+ pause
454
+ exit /b 1
455
+ )
456
+ `;
457
+ }
458
+
459
+ const batchPath = path.join(packageDir, "start.bat");
460
+ await fs.writeFile(batchPath, batchContent);
461
+
462
+ // Also create a silent launcher for background services
463
+ const silentBatchContent = `@echo off
464
+ cd /d "%~dp0"
465
+ start "" /min cmd /c "node "${mainScript}" %*"
466
+ `;
467
+
468
+ const silentBatchPath = path.join(packageDir, "start-silent.bat");
469
+ await fs.writeFile(silentBatchPath, silentBatchContent);
470
+
471
+ console.log("📄 Created Node.js startup scripts (interactive and silent)");
472
+ } catch (error) {
473
+ console.warn(`Warning: Could not create startup script: ${error.message}`);
474
+ }
475
+ }
476
+
477
+ /**
478
+ * Creates registry entries for the application if needed
479
+ * @param {string} packageDir - Package directory path
480
+ * @param {Object} config - Configuration object
481
+ * @param {Object} packageJson - package.json content
482
+ */
483
+ async function createRegistryEntries(packageDir, config, packageJson) {
484
+ try {
485
+ // Create a registry file for application registration
486
+ const regContent = `Windows Registry Editor Version 5.00
487
+
488
+ [HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}]
489
+ "FriendlyAppName"="${config.displayName || config.appName}"
490
+
491
+ [HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}\\shell]
492
+
493
+ [HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}\\shell\\open]
494
+
495
+ [HKEY_CURRENT_USER\\Software\\Classes\\Applications\\${config.executable}\\shell\\open\\command]
496
+ @="\\"${config.executable}\\" \\"%1\\""
497
+ `;
498
+
499
+ const regPath = path.join(packageDir, "app-registration.reg");
500
+ await fs.writeFile(regPath, regContent);
501
+
502
+ console.log("📋 Created registry entries");
503
+ } catch (error) {
504
+ console.warn(
505
+ `Warning: Could not create registry entries: ${error.message}`,
506
+ );
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Creates MCP Server configuration file in the Assets directory
512
+ * @param {string} assetsDir - Assets directory path
513
+ * @param {Object} config - Configuration object
514
+ */
515
+ async function createMcpServerConfig(assetsDir, config) {
516
+ try {
517
+ // Generate the executable name from config
518
+ let executableName;
519
+ if (config.executable && config.executable.endsWith(".exe")) {
520
+ // Use the configured executable name
521
+ executableName = config.executable;
522
+ } else {
523
+ // Generate executable name from app name
524
+ const appName = config.appName || config.displayName || "app";
525
+ executableName = `${appName.replace(/[^a-zA-Z0-9.-]/g, "_")}.exe`;
526
+ }
527
+
528
+ // Create MCP server configuration
529
+ const mcpConfig = {
530
+ version: 1,
531
+ mcpServers: [
532
+ {
533
+ name: `${config.packageName || config.appName}MCPServer`,
534
+ type: "stdio",
535
+ command: executableName,
536
+ },
537
+ ],
538
+ };
539
+
540
+ const configPath = path.join(assetsDir, "mcpServerConfig.json");
541
+ await fs.writeFile(configPath, JSON.stringify(mcpConfig, null, 2));
542
+
543
+ console.log("📄 Created MCP server configuration: mcpServerConfig.json");
544
+ } catch (error) {
545
+ console.warn(
546
+ `Warning: Could not create MCP server configuration: ${error.message}`,
547
+ );
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Installs Node.js dependencies in the package directory
553
+ * @param {string} packageDir - Package directory path
554
+ * @param {boolean} productionOnly - Whether to install only production dependencies
555
+ */
556
+ async function installNodeDependencies(packageDir, productionOnly = true) {
557
+ try {
558
+ const originalCwd = process.cwd();
559
+ process.chdir(packageDir);
560
+
561
+ try {
562
+ // Use bun install for faster, reliable, reproducible builds
563
+ const productionFlag = productionOnly ? "--production" : "";
564
+ executeCommand(`bun install ${productionFlag} --silent`);
565
+ } catch (error) {
566
+ // Fallback to bun install
567
+ console.log("bun install failed, falling back to bun install...");
568
+ const productionFlag = productionOnly ? "--production" : "";
569
+ executeCommand(`bun install ${productionFlag} --silent`);
570
+ }
571
+
572
+ process.chdir(originalCwd);
573
+ console.log("Dependencies installed successfully");
574
+ } catch (error) {
575
+ throw new MSIXError(
576
+ `Failed to install Node.js dependencies: ${error.message}`,
577
+ );
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Ensures Node.js runtime is available in the package
583
+ * @param {string} packageDir - Package directory path
584
+ * @param {Object} config - Configuration object
585
+ */
586
+ async function ensureNodeRuntime(packageDir, config) {
587
+ try {
588
+ const nodeExecutablePath = path.join(packageDir, "node.exe");
589
+
590
+ // Always remove existing node.exe to ensure fresh copy
591
+ if (await fs.pathExists(nodeExecutablePath)) {
592
+ console.log(
593
+ "🗑️ Removing existing Node.js runtime to ensure fresh copy...",
594
+ );
595
+ await fs.remove(nodeExecutablePath);
596
+ }
597
+
598
+ // Check if we have a bundled Node.js runtime
599
+ const bundledNodePath = path.join(
600
+ __dirname,
601
+ "..",
602
+ "runtime",
603
+ "nodejs",
604
+ "node.exe",
605
+ );
606
+ if (await fs.pathExists(bundledNodePath)) {
607
+ console.log("Copying bundled Node.js runtime...");
608
+ await fs.copy(bundledNodePath, nodeExecutablePath);
609
+ return;
610
+ }
611
+
612
+ // Try to copy system Node.js executable using fresh binary detection
613
+ try {
614
+ console.log("🔍 Attempting to get fresh Node.js binary...");
615
+ const { getFreshNodeBinary } = require("./sea-handler");
616
+
617
+ // Use the enhanced fresh binary detection that avoids SEA markers
618
+ const freshNodePath = await getFreshNodeBinary();
619
+ console.log(`🔍 Fresh binary search result: ${freshNodePath}`);
620
+
621
+ if (freshNodePath && (await fs.pathExists(freshNodePath))) {
622
+ console.log(`Copying fresh Node.js runtime from: ${freshNodePath}`);
623
+
624
+ // Only copy the node.exe file, not entire directories
625
+ await fs.copyFile(freshNodePath, nodeExecutablePath);
626
+
627
+ // Verify the copy worked
628
+ if (await fs.pathExists(nodeExecutablePath)) {
629
+ console.log("✅ Fresh Node.js runtime copied successfully");
630
+
631
+ // If config is using node.exe, also create a startup script
632
+ if (config.executable === "node.exe") {
633
+ await createNodeStartupWrapper(packageDir, config);
634
+ }
635
+
636
+ return;
637
+ }
638
+ }
639
+ } catch (error) {
640
+ console.warn(`Warning: Could not copy fresh Node.js: ${error.message}`);
641
+ console.log(`Error details: ${error.stack}`);
642
+
643
+ // Fallback to basic system node lookup
644
+ try {
645
+ console.log("🔙 Falling back to basic system Node.js lookup...");
646
+ const { executeCommand } = require("./utils");
647
+ const wherePath = process.platform === "win32" ? "where" : "which";
648
+ const systemNodePath = executeCommand(`${wherePath} node`, {
649
+ silent: true,
650
+ })
651
+ .trim()
652
+ .split("\n")[0];
653
+
654
+ if (systemNodePath && (await fs.pathExists(systemNodePath))) {
655
+ console.log(`Copying system Node.js runtime from: ${systemNodePath}`);
656
+
657
+ // Only copy the node.exe file, not entire directories
658
+ await fs.copyFile(systemNodePath, nodeExecutablePath);
659
+
660
+ // Verify the copy worked
661
+ if (await fs.pathExists(nodeExecutablePath)) {
662
+ console.log("✅ Node.js runtime copied successfully");
663
+
664
+ // If config is using node.exe, also create a startup script
665
+ if (config.executable === "node.exe") {
666
+ await createNodeStartupWrapper(packageDir, config);
667
+ }
668
+
669
+ return;
670
+ }
671
+ }
672
+ } catch (fallbackError) {
673
+ console.warn(
674
+ `Warning: Fallback Node.js copy also failed: ${fallbackError.message}`,
675
+ );
676
+ }
677
+ }
678
+
679
+ // If we can't find Node.js, try to download it
680
+ await downloadNodejs(packageDir, config.architecture || "x64");
681
+ } catch (error) {
682
+ console.warn(`Warning: Could not ensure Node.js runtime: ${error.message}`);
683
+ console.warn(
684
+ "📝 The application may not work without Node.js installed on the target system.",
685
+ );
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Creates a Node.js startup wrapper when using node.exe directly
691
+ * @param {string} packageDir - Package directory path
692
+ * @param {Object} config - Configuration object
693
+ */
694
+ async function createNodeStartupWrapper(packageDir, config) {
695
+ try {
696
+ // Create a launcher.js that handles the startup logic
697
+ const launcherContent = `const { spawn } = require('child_process');
698
+ const path = require('path');
699
+
700
+ // Determine the main script to run
701
+ const mainScript = 'app.js'; // Default for our sample app
702
+
703
+ console.log('Starting ${config.displayName || config.appName}...');
704
+
705
+ // Launch the main application
706
+ const child = spawn(process.execPath, [mainScript], {
707
+ stdio: 'inherit',
708
+ cwd: __dirname
709
+ });
710
+
711
+ child.on('error', (err) => {
712
+ console.error('Failed to start application:', err.message);
713
+ console.log('Press any key to exit...');
714
+ process.stdin.once('data', () => process.exit(1));
715
+ });
716
+
717
+ child.on('exit', (code) => {
718
+ if (code !== 0) {
719
+ console.log('\\nApplication exited with code:', code);
720
+ console.log('Press any key to exit...');
721
+ process.stdin.once('data', () => process.exit(code));
722
+ } else {
723
+ process.exit(0);
724
+ }
725
+ });
726
+
727
+ // Handle termination gracefully
728
+ process.on('SIGINT', () => {
729
+ child.kill('SIGINT');
730
+ });
731
+
732
+ process.on('SIGTERM', () => {
733
+ child.kill('SIGTERM');
734
+ });
735
+ `;
736
+
737
+ const launcherPath = path.join(packageDir, "launcher.js");
738
+ await fs.writeFile(launcherPath, launcherContent);
739
+
740
+ console.log("📄 Created Node.js startup wrapper");
741
+ } catch (error) {
742
+ console.warn(
743
+ `Warning: Could not create Node.js startup wrapper: ${error.message}`,
744
+ );
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Downloads Node.js runtime for the specified architecture
750
+ * @param {string} packageDir - Package directory path
751
+ * @param {string} architecture - Target architecture (x64, x86)
752
+ */
753
+ async function downloadNodejs(packageDir, architecture) {
754
+ try {
755
+ console.log(
756
+ `📥 Attempting to download Node.js runtime for ${architecture}...`,
757
+ );
758
+
759
+ // Get the latest Node.js version dynamically
760
+ const nodeVersion = await getLatestNodeVersion();
761
+ console.log(`🎯 Using Node.js version: ${nodeVersion}`);
762
+
763
+ const https = require("https");
764
+ const archMap = { x64: "x64", x86: "x86", arm64: "arm64" };
765
+ const nodeArch = archMap[architecture] || "x64";
766
+
767
+ const downloadUrl = `https://nodejs.org/dist/${nodeVersion}/win-${nodeArch}/node.exe`;
768
+ const nodeExecutablePath = path.join(packageDir, "node.exe");
769
+
770
+ await new Promise((resolve, reject) => {
771
+ const file = fs.createWriteStream(nodeExecutablePath);
772
+
773
+ https
774
+ .get(downloadUrl, (response) => {
775
+ if (response.statusCode === 200) {
776
+ response.pipe(file);
777
+ file.on("finish", () => {
778
+ file.close();
779
+ console.log("✅ Node.js runtime downloaded successfully");
780
+ resolve();
781
+ });
782
+ } else {
783
+ reject(
784
+ new Error(
785
+ `Failed to download Node.js: HTTP ${response.statusCode}`,
786
+ ),
787
+ );
788
+ }
789
+ })
790
+ .on("error", (err) => {
791
+ fs.unlink(nodeExecutablePath, () => {}); // Clean up on error
792
+ reject(err);
793
+ });
794
+ });
795
+ } catch (error) {
796
+ console.warn(
797
+ `Warning: Could not download Node.js runtime: ${error.message}`,
798
+ );
799
+ console.warn(
800
+ "💡 Consider manually placing node.exe in your input directory.",
801
+ );
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Validates the prepared package
807
+ * @param {string} packageDir - Package directory path
808
+ * @returns {Object} Validation results
809
+ */
810
+ async function validatePackage(packageDir) {
811
+ const issues = [];
812
+ const warnings = [];
813
+
814
+ try {
815
+ // Check for required files
816
+ const requiredFiles = ["AppxManifest.xml"];
817
+ for (const file of requiredFiles) {
818
+ const filePath = path.join(packageDir, file);
819
+ if (!(await fs.pathExists(filePath))) {
820
+ issues.push(`Missing required file: ${file}`);
821
+ }
822
+ }
823
+
824
+ // Check for executable
825
+ const manifestPath = path.join(packageDir, "AppxManifest.xml");
826
+ if (await fs.pathExists(manifestPath)) {
827
+ const manifestContent = await fs.readFile(manifestPath, "utf8");
828
+ const executableMatch = manifestContent.match(/Executable="([^"]+)"/);
829
+
830
+ if (executableMatch) {
831
+ const executablePath = path.join(packageDir, executableMatch[1]);
832
+ if (!(await fs.pathExists(executablePath))) {
833
+ issues.push(`Executable not found: ${executableMatch[1]}`);
834
+ }
835
+ }
836
+ }
837
+
838
+ // Check for assets
839
+ const assetsDir = path.join(packageDir, "Assets");
840
+ const requiredAssets = [
841
+ "Square44x44Logo.png",
842
+ "Square150x150Logo.png",
843
+ "StoreLogo.png",
844
+ ];
845
+
846
+ for (const asset of requiredAssets) {
847
+ const assetPath = path.join(assetsDir, asset);
848
+ if (!(await fs.pathExists(assetPath))) {
849
+ warnings.push(`Missing asset: ${asset}`);
850
+ }
851
+ }
852
+
853
+ // Check package size
854
+ const packageSize = await getDirectorySize(packageDir);
855
+ if (packageSize > CONSTANTS.MAX_PACKAGE_SIZE) {
856
+ warnings.push(
857
+ `Package size (${formatFileSize(packageSize)}) exceeds recommended maximum (${formatFileSize(CONSTANTS.MAX_PACKAGE_SIZE)})`,
858
+ );
859
+ }
860
+
861
+ return {
862
+ isValid: issues.length === 0,
863
+ issues,
864
+ warnings,
865
+ packageSize: formatFileSize(packageSize),
866
+ };
867
+ } catch (error) {
868
+ return {
869
+ isValid: false,
870
+ issues: [`Validation failed: ${error.message}`],
871
+ warnings: [],
872
+ packageSize: "Unknown",
873
+ };
874
+ }
875
+ }
876
+
877
+ /**
878
+ * Gets the total size of a directory
879
+ * @param {string} dirPath - Directory path
880
+ * @returns {number} Size in bytes
881
+ */
882
+ async function getDirectorySize(dirPath) {
883
+ let totalSize = 0;
884
+
885
+ async function calculateSize(currentPath) {
886
+ const stats = await fs.stat(currentPath);
887
+
888
+ if (stats.isFile()) {
889
+ totalSize += stats.size;
890
+ } else if (stats.isDirectory()) {
891
+ const items = await fs.readdir(currentPath);
892
+ for (const item of items) {
893
+ await calculateSize(path.join(currentPath, item));
894
+ }
895
+ }
896
+ }
897
+
898
+ await calculateSize(dirPath);
899
+ return totalSize;
900
+ }
901
+
902
+ module.exports = {
903
+ createMsixPackage,
904
+ signMsixPackage,
905
+ preparePackageDirectory,
906
+ validatePackage,
907
+ installNodeDependencies,
908
+ ensureNodeRuntime,
909
+ };