@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.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Constants for MSIX package creation
3
+ */
4
+
5
+ const CONSTANTS = {
6
+ // Default configuration values
7
+ DEFAULT_VERSION: "1.0.0.0",
8
+ DEFAULT_ARCHITECTURE: "x64",
9
+ DEFAULT_EXECUTABLE: "node.exe",
10
+ DEFAULT_TIMESTAMP_URL: "http://timestamp.digicert.com",
11
+ DEFAULT_BACKGROUND_COLOR: "transparent",
12
+
13
+ // Package requirements
14
+ MIN_VERSION_PARTS: 4,
15
+ MAX_PACKAGE_NAME_LENGTH: 50,
16
+
17
+ // File paths and extensions
18
+ MANIFEST_FILE: "AppxManifest.xml",
19
+ ASSETS_FOLDER: "Assets",
20
+ APP_FOLDER: "app",
21
+
22
+ // Asset file names
23
+ ASSET_FILES: [
24
+ "Square150x150Logo.png",
25
+ "Square44x44Logo.png",
26
+ "StoreLogo.png",
27
+ "Wide310x150Logo.png",
28
+ "SplashScreen.png",
29
+ ],
30
+
31
+ // Windows SDK paths (ordered by preference)
32
+ WINDOWS_SDK_PATHS: [
33
+ "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.26100.0\\x64",
34
+ "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.22621.0\\x64",
35
+ "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.19041.0\\x64",
36
+ "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.18362.0\\x64",
37
+ "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.17763.0\\x64",
38
+ "C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\x64",
39
+ ],
40
+
41
+ // Certificate stores to search
42
+ CERTIFICATE_STORES: [
43
+ { location: "CurrentUser", store: "My" },
44
+ { location: "CurrentUser", store: "TrustedPublisher" },
45
+ { location: "CurrentUser", store: "TrustedPeople" },
46
+ { location: "LocalMachine", store: "My" },
47
+ { location: "LocalMachine", store: "TrustedPublisher" },
48
+ { location: "LocalMachine", store: "TrustedPeople" },
49
+ ],
50
+
51
+ // Default capabilities
52
+ DEFAULT_CAPABILITIES: ["internetClient", "runFullTrust"],
53
+
54
+ // Supported architectures
55
+ SUPPORTED_ARCHITECTURES: ["x64", "x86", "arm64"],
56
+
57
+ // Validation patterns
58
+ PUBLISHER_PATTERN: /^CN=/,
59
+ VERSION_PATTERN: /^\d+\.\d+\.\d+\.\d+$/,
60
+
61
+ // Files and directories to exclude when copying
62
+ COPY_EXCLUDE_PATTERNS: [
63
+ "node_modules/.cache",
64
+ ".git",
65
+ ".gitignore",
66
+ ".npm",
67
+ ".nyc_output",
68
+ "coverage",
69
+ "*.log",
70
+ "npm-debug.log*",
71
+ "yarn-debug.log*",
72
+ "yarn-error.log*",
73
+ ".DS_Store",
74
+ "Thumbs.db",
75
+ ],
76
+
77
+ // Configuration file name
78
+ CONFIG_FILENAME: "msix-config.json",
79
+
80
+ // Package size limits
81
+ MAX_PACKAGE_SIZE: 500 * 1024 * 1024, // 500MB
82
+
83
+ // Default 1x1 transparent PNG (base64)
84
+ DEFAULT_PNG_BASE64:
85
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jU77zgAAAABJRU5ErkJggg==",
86
+ };
87
+
88
+ // Error types for better error handling
89
+ class MsixError extends Error {
90
+ constructor(message, code, details) {
91
+ super(message);
92
+ this.name = "MsixError";
93
+ this.code = code;
94
+ this.details = details;
95
+ }
96
+ }
97
+
98
+ class ValidationError extends MsixError {
99
+ constructor(field, value, expected) {
100
+ super(
101
+ `Validation failed for field '${field}': expected ${expected}, got ${value}`,
102
+ "VALIDATION_ERROR",
103
+ { field, value, expected },
104
+ );
105
+ this.name = "ValidationError";
106
+ }
107
+ }
108
+
109
+ class ToolNotFoundError extends MsixError {
110
+ constructor(tool, suggestions = []) {
111
+ super(`Required tool '${tool}' not found`, "TOOL_NOT_FOUND", {
112
+ tool,
113
+ suggestions,
114
+ });
115
+ this.name = "ToolNotFoundError";
116
+ }
117
+ }
118
+
119
+ class CertificateError extends MsixError {
120
+ constructor(message, details) {
121
+ super(message, "CERTIFICATE_ERROR", details);
122
+ this.name = "CertificateError";
123
+ }
124
+ }
125
+
126
+ class SigningError extends MsixError {
127
+ constructor(message, details) {
128
+ super(message, "SIGNING_ERROR", details);
129
+ this.name = "SigningError";
130
+ }
131
+ }
132
+
133
+ module.exports = {
134
+ CONSTANTS,
135
+ MSIXError: MsixError,
136
+ ValidationError,
137
+ ToolNotFoundError,
138
+ CertificateError,
139
+ SigningError,
140
+ };
package/src/index.js ADDED
@@ -0,0 +1,414 @@
1
+ const path = require("path");
2
+ const fs = require("fs-extra");
3
+ const chalk = require("chalk");
4
+
5
+ // Import modular components
6
+ const {
7
+ validateConfig,
8
+ validatePaths,
9
+ sanitizeConfig,
10
+ validateSigningConfig,
11
+ } = require("./validation");
12
+ const {
13
+ getTempDir,
14
+ cleanup,
15
+ readPackageJson,
16
+ validateRequiredTools,
17
+ getSystemInfo,
18
+ } = require("./utils");
19
+ const { findCodeSigningCertificates } = require("./certificates");
20
+ const {
21
+ createMsixPackage: createPackage,
22
+ signMsixPackage: signPackage,
23
+ preparePackageDirectory,
24
+ validatePackage,
25
+ } = require("./package");
26
+ const { CONSTANTS, MSIXError, ValidationError } = require("./constants");
27
+
28
+ /**
29
+ * Creates an MSIX package from a Node.js application
30
+ *
31
+ * @param {Object} config - Configuration object
32
+ * @param {string} config.inputPath - Path to the Node.js application directory
33
+ * @param {string} config.outputPath - Output directory for the MSIX package
34
+ * @param {string} config.appName - Application name (required)
35
+ * @param {string} config.publisher - Publisher name in format "CN=PublisherName" (required)
36
+ * @param {string} config.version - Application version in format "x.x.x.x" (optional, defaults to "1.0.0.0")
37
+ * @param {string} config.description - Application description (optional)
38
+ * @param {string} config.executable - Main executable file (optional, defaults to "node.exe")
39
+ * @param {string} config.icon - Path to application icon file (optional)
40
+ * @param {string} config.architecture - Target architecture (optional, defaults to "x64")
41
+ * @param {string} config.displayName - Display name shown to users (optional, defaults to appName)
42
+ * @param {string} config.packageName - Package identity name (optional, auto-generated if not provided)
43
+ * @param {Array<string>} config.capabilities - Application capabilities (optional, defaults to ["internetClient"])
44
+ * @param {string} config.backgroundColor - Background color for tiles (optional, defaults to "transparent")
45
+ *
46
+ * @param {boolean} config.sign - Whether to sign the MSIX package (optional, defaults to true)
47
+ * @param {string} config.certificateThumbprint - Certificate thumbprint for signing (optional)
48
+ * @param {string} config.certificateSubject - Certificate subject name for signing (optional)
49
+ * @param {string} config.certificatePath - Path to PFX certificate file (optional)
50
+ * @param {string} config.certificatePassword - Password for PFX certificate (optional)
51
+ * @param {string} config.timestampUrl - Timestamp server URL (optional)
52
+ *
53
+ * @returns {Promise<Object>} Result object containing package information
54
+ * @throws {ValidationError} When configuration is invalid
55
+ * @throws {MSIXError} When package creation fails
56
+ *
57
+ * @example
58
+ * const { createMsixPackage } = require('node-msix');
59
+ *
60
+ * const result = await createMsixPackage({
61
+ * inputPath: './my-node-app',
62
+ * outputPath: './dist',
63
+ * appName: 'My Node App',
64
+ * publisher: 'CN=My Company',
65
+ * version: '1.0.0.0'
66
+ * });
67
+ *
68
+ * console.log('Package created:', result.packagePath);
69
+ */
70
+ async function createMsixPackage(config) {
71
+ const cleanupPaths = [];
72
+
73
+ try {
74
+ // Validate system requirements
75
+ console.log(chalk.blue("🔍 Validating system requirements..."));
76
+ validateRequiredTools();
77
+
78
+ // Validate and sanitize configuration
79
+ console.log(chalk.blue("✅ Validating configuration..."));
80
+ validateConfig(config);
81
+ await validatePaths(config);
82
+
83
+ const sanitizedConfig = sanitizeConfig(config);
84
+
85
+ // Set default signing configuration
86
+ const signingConfig = {
87
+ sign: sanitizedConfig.sign !== false, // Default to true
88
+ certificateThumbprint: sanitizedConfig.certificateThumbprint,
89
+ certificateSubject: sanitizedConfig.certificateSubject,
90
+ certificatePath: sanitizedConfig.certificatePath,
91
+ certificatePassword: sanitizedConfig.certificatePassword,
92
+ timestampUrl: sanitizedConfig.timestampUrl,
93
+ };
94
+
95
+ validateSigningConfig(signingConfig);
96
+
97
+ // Read package.json from input directory
98
+ console.log(chalk.blue("📖 Reading package.json..."));
99
+ const packageJson = await readPackageJson(sanitizedConfig.inputPath);
100
+
101
+ // Create temporary directory
102
+ const tempDir = getTempDir("msix-package");
103
+ cleanupPaths.push(tempDir);
104
+
105
+ // Prepare package directory
106
+ const packageDir = await preparePackageDirectory(
107
+ sanitizedConfig,
108
+ tempDir,
109
+ packageJson,
110
+ );
111
+
112
+ // Validate prepared package
113
+ console.log(chalk.blue("🔍 Validating package contents..."));
114
+ const validation = await validatePackage(packageDir);
115
+
116
+ if (!validation.isValid) {
117
+ throw new ValidationError(
118
+ "package contents",
119
+ "valid package structure",
120
+ validation.issues.join("; "),
121
+ );
122
+ }
123
+
124
+ if (validation.warnings.length > 0) {
125
+ console.log(chalk.yellow("⚠️ Package validation warnings:"));
126
+ validation.warnings.forEach((warning) => {
127
+ console.log(chalk.yellow(` • ${warning}`));
128
+ });
129
+ }
130
+
131
+ console.log(chalk.green(`📦 Package size: ${validation.packageSize}`));
132
+
133
+ // Generate output filename
134
+ const packageName = sanitizedConfig.packageName.replace(
135
+ /[^a-zA-Z0-9.-]/g,
136
+ "",
137
+ );
138
+ const version = sanitizedConfig.version;
139
+ const architecture = sanitizedConfig.architecture;
140
+ const outputFilename = `${packageName}_${version}_${architecture}.msix`;
141
+ const outputPath = path.join(sanitizedConfig.outputPath, outputFilename);
142
+
143
+ // Create MSIX package
144
+ console.log(chalk.blue("🏗️ Creating MSIX package..."));
145
+ await createPackage(packageDir, outputPath);
146
+
147
+ let signedSuccessfully = false;
148
+
149
+ // Sign the package if requested
150
+ if (signingConfig.sign) {
151
+ try {
152
+ console.log(chalk.blue("🔐 Signing MSIX package..."));
153
+ signedSuccessfully = await signPackage(outputPath, signingConfig);
154
+ } catch (error) {
155
+ console.log(
156
+ chalk.yellow(`⚠️ Package signing failed: ${error.message}`),
157
+ );
158
+ console.log(
159
+ chalk.yellow(
160
+ "The unsigned package has still been created successfully.",
161
+ ),
162
+ );
163
+ }
164
+ }
165
+
166
+ // Clean up temporary files
167
+ console.log(chalk.blue("🧹 Cleaning up temporary files..."));
168
+ await cleanup(cleanupPaths);
169
+
170
+ const result = {
171
+ success: true,
172
+ packagePath: outputPath,
173
+ packageSize: validation.packageSize,
174
+ signed: signedSuccessfully,
175
+ config: {
176
+ appName: sanitizedConfig.appName,
177
+ version: sanitizedConfig.version,
178
+ publisher: sanitizedConfig.publisher,
179
+ architecture: sanitizedConfig.architecture,
180
+ packageName: sanitizedConfig.packageName,
181
+ },
182
+ systemInfo: getSystemInfo(),
183
+ timestamp: new Date().toISOString(),
184
+ };
185
+
186
+ console.log(chalk.green("🎉 MSIX package created successfully!"));
187
+ console.log(chalk.green(`📍 Location: ${outputPath}`));
188
+
189
+ if (signedSuccessfully) {
190
+ console.log(chalk.green("🔐 Package signed successfully"));
191
+ } else if (signingConfig.sign) {
192
+ console.log(chalk.yellow("⚠️ Package created but not signed"));
193
+ }
194
+
195
+ return result;
196
+ } catch (error) {
197
+ // Clean up on error
198
+ if (cleanupPaths.length > 0) {
199
+ try {
200
+ await cleanup(cleanupPaths);
201
+ } catch (cleanupError) {
202
+ console.warn(
203
+ chalk.yellow(`Warning: Cleanup failed: ${cleanupError.message}`),
204
+ );
205
+ }
206
+ }
207
+
208
+ // Re-throw the original error
209
+ throw error;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Signs an existing MSIX package
215
+ *
216
+ * @param {string} packagePath - Path to the MSIX package to sign
217
+ * @param {Object} signingConfig - Signing configuration
218
+ * @param {string} signingConfig.certificateThumbprint - Certificate thumbprint (optional)
219
+ * @param {string} signingConfig.certificateSubject - Certificate subject (optional)
220
+ * @param {string} signingConfig.certificatePath - Path to PFX certificate (optional)
221
+ * @param {string} signingConfig.certificatePassword - PFX password (optional)
222
+ * @param {string} signingConfig.timestampUrl - Timestamp server URL (optional)
223
+ *
224
+ * @returns {Promise<boolean>} True if signing was successful
225
+ * @throws {ValidationError} When signing configuration is invalid
226
+ * @throws {SigningError} When signing fails
227
+ *
228
+ * @example
229
+ * const { signMsixPackage } = require('node-msix');
230
+ *
231
+ * const success = await signMsixPackage('./dist/MyApp.msix', {
232
+ * certificatePath: './certificates/mycert.pfx',
233
+ * certificatePassword: 'mypassword'
234
+ * });
235
+ */
236
+ async function signMsixPackage(packagePath, signingConfig = {}) {
237
+ try {
238
+ console.log(chalk.blue("🔐 Signing MSIX package..."));
239
+
240
+ // Validate inputs
241
+ if (!packagePath || !(await fs.pathExists(packagePath))) {
242
+ throw new ValidationError(
243
+ "packagePath",
244
+ "existing MSIX file",
245
+ packagePath,
246
+ );
247
+ }
248
+
249
+ // Default signing configuration
250
+ const config = {
251
+ sign: true,
252
+ timestampUrl:
253
+ signingConfig.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL,
254
+ ...signingConfig,
255
+ };
256
+
257
+ validateSigningConfig(config);
258
+
259
+ const result = await signPackage(packagePath, config);
260
+
261
+ if (result) {
262
+ console.log(chalk.green("🔐 Package signed successfully"));
263
+ }
264
+
265
+ return result;
266
+ } catch (error) {
267
+ console.error(chalk.red(`❌ Signing failed: ${error.message}`));
268
+ throw error;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Lists available code signing certificates on the system
274
+ *
275
+ * @returns {Promise<Array>} Array of certificate objects
276
+ * @throws {CertificateError} When certificate enumeration fails
277
+ *
278
+ * @example
279
+ * const { listCertificates } = require('node-msix');
280
+ *
281
+ * const certificates = await listCertificates();
282
+ * certificates.forEach(cert => {
283
+ * console.log(`Subject: ${cert.subject}`);
284
+ * console.log(`Thumbprint: ${cert.thumbprint}`);
285
+ * console.log(`Valid: ${cert.isValid}`);
286
+ * });
287
+ */
288
+ async function listCertificates() {
289
+ try {
290
+ console.log(chalk.blue("🔍 Searching for code signing certificates..."));
291
+
292
+ const certificates = await findCodeSigningCertificates();
293
+
294
+ if (certificates.length === 0) {
295
+ console.log(chalk.yellow("⚠️ No code signing certificates found"));
296
+ } else {
297
+ console.log(
298
+ chalk.green(`✅ Found ${certificates.length} certificate(s)`),
299
+ );
300
+ }
301
+
302
+ return certificates;
303
+ } catch (error) {
304
+ console.error(chalk.red(`❌ Certificate search failed: ${error.message}`));
305
+ throw error;
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Initializes a new MSIX configuration file
311
+ *
312
+ * @param {string} targetDir - Directory to create config file in (optional, defaults to current directory)
313
+ * @param {Object} options - Configuration options (optional)
314
+ * @returns {Promise<string>} Path to created config file
315
+ *
316
+ * @example
317
+ * const { initConfig } = require('node-msix');
318
+ *
319
+ * const configPath = await initConfig('./my-app', {
320
+ * appName: 'My Application',
321
+ * publisher: 'CN=My Company'
322
+ * });
323
+ */
324
+ async function initConfig(targetDir = process.cwd(), options = {}) {
325
+ try {
326
+ const configPath = path.join(targetDir, CONSTANTS.CONFIG_FILENAME);
327
+
328
+ // Check if config already exists
329
+ if (await fs.pathExists(configPath)) {
330
+ throw new ValidationError(
331
+ "config file",
332
+ "non-existing file",
333
+ "Config file already exists",
334
+ );
335
+ }
336
+
337
+ // Read package.json for defaults if it exists
338
+ let packageJson = {};
339
+ const packageJsonPath = path.join(targetDir, "package.json");
340
+ if (await fs.pathExists(packageJsonPath)) {
341
+ packageJson = await readPackageJson(targetDir);
342
+ }
343
+
344
+ // Create default configuration
345
+ const config = {
346
+ appName: options.appName || packageJson.name || path.basename(targetDir),
347
+ publisher: options.publisher || "CN=DefaultPublisher",
348
+ version:
349
+ options.version ||
350
+ (packageJson.version ? `${packageJson.version}.0` : "1.0.0.0"),
351
+ description:
352
+ options.description ||
353
+ packageJson.description ||
354
+ "Node.js application packaged as MSIX",
355
+ executable: options.executable || CONSTANTS.DEFAULT_EXECUTABLE,
356
+ architecture: options.architecture || CONSTANTS.DEFAULT_ARCHITECTURE,
357
+ capabilities: options.capabilities || CONSTANTS.DEFAULT_CAPABILITIES,
358
+ sign: options.sign !== false,
359
+
360
+ // Paths (relative to config file)
361
+ inputPath: options.inputPath || ".",
362
+ outputPath: options.outputPath || "./dist",
363
+ icon: options.icon || null,
364
+
365
+ // Optional signing configuration
366
+ certificateThumbprint: options.certificateThumbprint || null,
367
+ certificateSubject: options.certificateSubject || null,
368
+ certificatePath: options.certificatePath || null,
369
+ certificatePassword: options.certificatePassword || null,
370
+
371
+ // Generated info
372
+ _generated: {
373
+ version: require("../package.json").version,
374
+ timestamp: new Date().toISOString(),
375
+ node: process.version,
376
+ platform: process.platform,
377
+ },
378
+ };
379
+
380
+ // Write config file
381
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
382
+
383
+ console.log(chalk.green(`✅ Configuration file created: ${configPath}`));
384
+ console.log(
385
+ chalk.blue(
386
+ "💡 Edit the configuration file and then run the packaging command",
387
+ ),
388
+ );
389
+
390
+ return configPath;
391
+ } catch (error) {
392
+ console.error(
393
+ chalk.red(`❌ Config initialization failed: ${error.message}`),
394
+ );
395
+ throw error;
396
+ }
397
+ }
398
+
399
+ // Export main functions
400
+ module.exports = {
401
+ createMsixPackage,
402
+ signMsixPackage,
403
+ listCertificates,
404
+ initConfig,
405
+
406
+ // For backward compatibility
407
+ findCodeSigningCertificates: listCertificates,
408
+
409
+ // Version info
410
+ version: require("../package.json").version,
411
+
412
+ // Constants for external use
413
+ CONSTANTS,
414
+ };