@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/utils.js ADDED
@@ -0,0 +1,292 @@
1
+ const path = require("path");
2
+ const fs = require("fs-extra");
3
+ const { execSync } = require("child_process");
4
+ const https = require("https");
5
+ const { CONSTANTS, ToolNotFoundError, MSIXError } = require("./constants");
6
+
7
+ /**
8
+ * Executes a command safely with error handling
9
+ * @param {string} command - Command to execute
10
+ * @param {Object} options - Execution options
11
+ * @returns {string} Command output
12
+ * @throws {MSIXError} When command execution fails
13
+ */
14
+ function executeCommand(command, options = {}) {
15
+ try {
16
+ const result = execSync(command, {
17
+ encoding: "utf8",
18
+ stdio: options.silent ? "pipe" : "inherit",
19
+ windowsHide: true,
20
+ ...options,
21
+ });
22
+ return result;
23
+ } catch (error) {
24
+ const errorMessage = `Command failed: ${command}\n${error.message}`;
25
+ throw new MSIXError(errorMessage);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Finds Windows SDK tools
31
+ * @returns {Object} Object containing paths to makeappx and signtool
32
+ * @throws {ToolNotFoundError} When tools are not found
33
+ */
34
+ function findWindowsSDKTools() {
35
+ const possiblePaths = CONSTANTS.WINDOWS_SDK_PATHS;
36
+
37
+ for (const basePath of possiblePaths) {
38
+ try {
39
+ const makeappxPath = path.join(basePath, "makeappx.exe");
40
+ const signtoolPath = path.join(basePath, "signtool.exe");
41
+
42
+ if (fs.existsSync(makeappxPath) && fs.existsSync(signtoolPath)) {
43
+ return {
44
+ makeappx: makeappxPath,
45
+ signtool: signtoolPath,
46
+ };
47
+ }
48
+ } catch (error) {
49
+ // Continue searching
50
+ }
51
+ }
52
+
53
+ throw new ToolNotFoundError(
54
+ "Windows SDK tools (makeappx.exe, signtool.exe) not found. Please install Windows SDK.",
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Generates a unique temporary directory name
60
+ * @param {string} prefix - Prefix for the temporary directory
61
+ * @returns {string} Temporary directory path
62
+ */
63
+ function getTempDir(prefix = "msix-temp") {
64
+ const tempBase =
65
+ process.env.TEMP || process.env.TMP || path.join(process.cwd(), "temp");
66
+ const timestamp = Date.now();
67
+ const random = Math.random().toString(36).substring(2, 8);
68
+ return path.join(tempBase, `${prefix}-${timestamp}-${random}`);
69
+ }
70
+
71
+ /**
72
+ * Cleans up temporary directories and files
73
+ * @param {string[]} paths - Array of paths to clean up
74
+ */
75
+ async function cleanup(paths) {
76
+ for (const cleanupPath of paths) {
77
+ try {
78
+ if (await fs.pathExists(cleanupPath)) {
79
+ await fs.remove(cleanupPath);
80
+ console.log(`Cleaned up: ${cleanupPath}`);
81
+ }
82
+ } catch (error) {
83
+ console.warn(
84
+ `Warning: Could not clean up ${cleanupPath}: ${error.message}`,
85
+ );
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Creates a directory structure ensuring all parent directories exist
92
+ * @param {string} dirPath - Directory path to create
93
+ */
94
+ async function ensureDir(dirPath) {
95
+ try {
96
+ await fs.ensureDir(dirPath);
97
+ } catch (error) {
98
+ throw new MSIXError(
99
+ `Failed to create directory ${dirPath}: ${error.message}`,
100
+ );
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Copies files with progress indication
106
+ * @param {string} src - Source path
107
+ * @param {string} dest - Destination path
108
+ * @param {boolean} showProgress - Whether to show progress
109
+ */
110
+ async function copyFiles(src, dest, showProgress = true) {
111
+ try {
112
+ if (showProgress) {
113
+ console.log(`Copying files from ${src} to ${dest}...`);
114
+ }
115
+
116
+ await fs.copy(src, dest, {
117
+ overwrite: true,
118
+ errorOnExist: false,
119
+ filter: (src) => {
120
+ // Skip node_modules and common temporary directories
121
+ const relativePath = path.relative(src, src);
122
+ return !CONSTANTS.COPY_EXCLUDE_PATTERNS.some((pattern) =>
123
+ relativePath.includes(pattern),
124
+ );
125
+ },
126
+ });
127
+
128
+ if (showProgress) {
129
+ console.log("Files copied successfully");
130
+ }
131
+ } catch (error) {
132
+ throw new MSIXError(`Failed to copy files: ${error.message}`);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Reads and parses package.json from a directory
138
+ * @param {string} directoryPath - Path to directory containing package.json
139
+ * @returns {Object} Parsed package.json content
140
+ * @throws {MSIXError} When package.json cannot be read or parsed
141
+ */
142
+ async function readPackageJson(directoryPath) {
143
+ const packageJsonPath = path.join(directoryPath, "package.json");
144
+
145
+ try {
146
+ const content = await fs.readFile(packageJsonPath, "utf8");
147
+ return JSON.parse(content);
148
+ } catch (error) {
149
+ throw new MSIXError(
150
+ `Failed to read package.json from ${directoryPath}: ${error.message}`,
151
+ );
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Formats file size for human reading
157
+ * @param {number} bytes - Size in bytes
158
+ * @returns {string} Formatted size string
159
+ */
160
+ function formatFileSize(bytes) {
161
+ if (bytes === 0) return "0 Bytes";
162
+
163
+ const k = 1024;
164
+ const sizes = ["Bytes", "KB", "MB", "GB"];
165
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
166
+
167
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
168
+ }
169
+
170
+ /**
171
+ * Validates that all required tools are available
172
+ * @throws {ToolNotFoundError} When required tools are missing
173
+ */
174
+ function validateRequiredTools() {
175
+ try {
176
+ // Check for Node.js
177
+ execSync("node --version", { stdio: "pipe" });
178
+ } catch {
179
+ throw new ToolNotFoundError("Node.js is required but not found in PATH");
180
+ }
181
+
182
+ try {
183
+ // Check for bun
184
+ execSync("bun --version", { stdio: "pipe" });
185
+ } catch {
186
+ throw new ToolNotFoundError("bun is required but not found in PATH");
187
+ }
188
+
189
+ // Check for Windows SDK tools
190
+ findWindowsSDKTools(); // This will throw if not found
191
+ }
192
+
193
+ /**
194
+ * Gets system information for debugging
195
+ * @returns {Object} System information
196
+ */
197
+ function getSystemInfo() {
198
+ try {
199
+ const nodeVersion = execSync("node --version", { encoding: "utf8" }).trim();
200
+ const bunVersion = execSync("bun --version", { encoding: "utf8" }).trim();
201
+ const osVersion = require("os").release();
202
+ const platform = process.platform;
203
+ const arch = process.arch;
204
+
205
+ return {
206
+ node: nodeVersion,
207
+ bun: bunVersion,
208
+ os: osVersion,
209
+ platform,
210
+ architecture: arch,
211
+ timestamp: new Date().toISOString(),
212
+ };
213
+ } catch (error) {
214
+ return {
215
+ error: "Could not gather system information",
216
+ timestamp: new Date().toISOString(),
217
+ };
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Gets the latest Node.js LTS version dynamically
223
+ * @returns {Promise<string>} Latest LTS version (e.g., 'v22.8.0')
224
+ */
225
+ async function getLatestNodeVersion() {
226
+ try {
227
+ return new Promise((resolve, reject) => {
228
+ const options = {
229
+ hostname: "nodejs.org",
230
+ path: "/dist/index.json",
231
+ method: "GET",
232
+ timeout: 10000,
233
+ };
234
+
235
+ const req = https.request(options, (res) => {
236
+ let data = "";
237
+ res.on("data", (chunk) => (data += chunk));
238
+ res.on("end", () => {
239
+ try {
240
+ const releases = JSON.parse(data);
241
+ // Find latest LTS version
242
+ const latestLTS = releases.find((release) => release.lts);
243
+ if (latestLTS) {
244
+ resolve(latestLTS.version);
245
+ } else {
246
+ // Fallback to latest stable
247
+ resolve(releases[0].version);
248
+ }
249
+ } catch (parseError) {
250
+ console.warn(
251
+ "Warning: Could not parse Node.js releases, using fallback version",
252
+ );
253
+ resolve("v22.8.0"); // Fallback version
254
+ }
255
+ });
256
+ });
257
+
258
+ req.on("error", (error) => {
259
+ console.warn(
260
+ "Warning: Could not fetch latest Node.js version, using fallback",
261
+ );
262
+ resolve("v22.8.0"); // Fallback version
263
+ });
264
+
265
+ req.on("timeout", () => {
266
+ console.warn(
267
+ "Warning: Timeout fetching Node.js version, using fallback",
268
+ );
269
+ resolve("v22.8.0"); // Fallback version
270
+ });
271
+
272
+ req.end();
273
+ });
274
+ } catch (error) {
275
+ console.warn("Warning: Error getting Node.js version, using fallback");
276
+ return "v22.8.0"; // Fallback version
277
+ }
278
+ }
279
+
280
+ module.exports = {
281
+ executeCommand,
282
+ findWindowsSDKTools,
283
+ getTempDir,
284
+ cleanup,
285
+ ensureDir,
286
+ copyFiles,
287
+ readPackageJson,
288
+ formatFileSize,
289
+ validateRequiredTools,
290
+ getSystemInfo,
291
+ getLatestNodeVersion,
292
+ };
@@ -0,0 +1,228 @@
1
+ const path = require("path");
2
+ const fs = require("fs-extra");
3
+ const { CONSTANTS, ValidationError } = require("./constants");
4
+
5
+ /**
6
+ * Validates configuration object for MSIX package creation
7
+ * @param {Object} config - Configuration object to validate
8
+ * @throws {ValidationError} When validation fails
9
+ */
10
+ function validateConfig(config) {
11
+ const errors = [];
12
+
13
+ // Required fields
14
+ const requiredFields = [
15
+ { field: "inputPath", type: "string" },
16
+ { field: "outputPath", type: "string" },
17
+ { field: "appName", type: "string" },
18
+ { field: "publisher", type: "string" },
19
+ ];
20
+
21
+ for (const { field, type } of requiredFields) {
22
+ if (!config[field]) {
23
+ errors.push(`Missing required field: ${field}`);
24
+ } else if (typeof config[field] !== type) {
25
+ errors.push(
26
+ `Field ${field} must be of type ${type}, got ${typeof config[field]}`,
27
+ );
28
+ }
29
+ }
30
+
31
+ // Validate publisher format
32
+ if (config.publisher && !CONSTANTS.PUBLISHER_PATTERN.test(config.publisher)) {
33
+ errors.push('Publisher must start with "CN=" (e.g., "CN=MyCompany")');
34
+ }
35
+
36
+ // Validate version format
37
+ if (config.version) {
38
+ if (!CONSTANTS.VERSION_PATTERN.test(config.version)) {
39
+ errors.push('Version must be in format x.x.x.x (e.g., "1.0.0.0")');
40
+ }
41
+ }
42
+
43
+ // Validate architecture
44
+ if (
45
+ config.architecture &&
46
+ !CONSTANTS.SUPPORTED_ARCHITECTURES.includes(config.architecture)
47
+ ) {
48
+ errors.push(
49
+ `Architecture must be one of: ${CONSTANTS.SUPPORTED_ARCHITECTURES.join(", ")}`,
50
+ );
51
+ }
52
+
53
+ // Validate package name length
54
+ if (
55
+ config.packageName &&
56
+ config.packageName.length > CONSTANTS.MAX_PACKAGE_NAME_LENGTH
57
+ ) {
58
+ errors.push(
59
+ `Package name must be ${CONSTANTS.MAX_PACKAGE_NAME_LENGTH} characters or less`,
60
+ );
61
+ }
62
+
63
+ // Validate capabilities
64
+ if (config.capabilities && !Array.isArray(config.capabilities)) {
65
+ errors.push("Capabilities must be an array");
66
+ }
67
+
68
+ if (errors.length > 0) {
69
+ throw new ValidationError(
70
+ "config",
71
+ "valid configuration object",
72
+ errors.join("; "),
73
+ );
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Validates that required paths exist
79
+ * @param {Object} config - Configuration object
80
+ * @throws {ValidationError} When paths don't exist
81
+ */
82
+ async function validatePaths(config) {
83
+ const errors = [];
84
+
85
+ // Check input path exists
86
+ if (!(await fs.pathExists(config.inputPath))) {
87
+ errors.push(`Input path does not exist: ${config.inputPath}`);
88
+ } else {
89
+ // Check if it's a directory
90
+ const stats = await fs.stat(config.inputPath);
91
+ if (!stats.isDirectory()) {
92
+ errors.push(`Input path must be a directory: ${config.inputPath}`);
93
+ }
94
+
95
+ // Check for package.json in input directory
96
+ const packageJsonPath = path.join(config.inputPath, "package.json");
97
+ if (!(await fs.pathExists(packageJsonPath))) {
98
+ errors.push(
99
+ `No package.json found in input directory: ${config.inputPath}`,
100
+ );
101
+ }
102
+ }
103
+
104
+ // Validate icon path if provided
105
+ if (config.icon && !(await fs.pathExists(config.icon))) {
106
+ errors.push(`Icon file does not exist: ${config.icon}`);
107
+ }
108
+
109
+ if (errors.length > 0) {
110
+ throw new ValidationError("paths", "existing paths", errors.join("; "));
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Sanitizes configuration values
116
+ * @param {Object} config - Configuration object to sanitize
117
+ * @returns {Object} Sanitized configuration
118
+ */
119
+ function sanitizeConfig(config) {
120
+ const sanitized = { ...config };
121
+
122
+ // Set defaults
123
+ sanitized.version = sanitized.version || CONSTANTS.DEFAULT_VERSION;
124
+ sanitized.architecture =
125
+ sanitized.architecture || CONSTANTS.DEFAULT_ARCHITECTURE;
126
+ sanitized.executable = sanitized.executable || CONSTANTS.DEFAULT_EXECUTABLE;
127
+ sanitized.timestampUrl =
128
+ sanitized.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL;
129
+ sanitized.capabilities =
130
+ sanitized.capabilities || CONSTANTS.DEFAULT_CAPABILITIES;
131
+
132
+ // Sanitize strings
133
+ sanitized.appName = sanitized.appName.trim();
134
+ sanitized.publisher = sanitized.publisher.trim();
135
+
136
+ // Generate package name if not provided
137
+ if (!sanitized.packageName) {
138
+ const publisherName = sanitized.publisher
139
+ .replace(/^CN=/, "")
140
+ .split(",")[0]
141
+ .trim();
142
+ const appNameSafe = sanitized.appName.replace(/[^a-zA-Z0-9]/g, "");
143
+ sanitized.packageName = `${publisherName}.${appNameSafe}`.substring(
144
+ 0,
145
+ CONSTANTS.MAX_PACKAGE_NAME_LENGTH,
146
+ );
147
+ }
148
+
149
+ // Generate display name if not provided
150
+ sanitized.displayName = sanitized.displayName || sanitized.appName;
151
+
152
+ // Set description if not provided
153
+ sanitized.description =
154
+ sanitized.description ||
155
+ `${sanitized.appName} - Node.js application packaged as MSIX`;
156
+
157
+ // Set build options
158
+ sanitized.skipBuild = sanitized.skipBuild || false;
159
+ sanitized.installDevDeps = sanitized.installDevDeps !== false; // Default to true
160
+
161
+ // Normalize paths
162
+ sanitized.inputPath = path.resolve(sanitized.inputPath);
163
+ sanitized.outputPath = path.resolve(sanitized.outputPath);
164
+
165
+ if (sanitized.icon) {
166
+ sanitized.icon = path.resolve(sanitized.icon);
167
+ }
168
+
169
+ return sanitized;
170
+ }
171
+
172
+ /**
173
+ * Validates certificate configuration
174
+ * @param {Object} signingConfig - Signing configuration
175
+ * @throws {ValidationError} When signing configuration is invalid
176
+ */
177
+ function validateSigningConfig(signingConfig) {
178
+ if (!signingConfig.sign) {
179
+ return; // No validation needed if signing is disabled
180
+ }
181
+
182
+ const errors = [];
183
+
184
+ // At least one certificate identification method should be provided
185
+ const hasCertId =
186
+ signingConfig.certificateThumbprint ||
187
+ signingConfig.certificateSubject ||
188
+ signingConfig.certificatePath;
189
+
190
+ if (!hasCertId) {
191
+ // This is okay - we'll auto-discover certificates
192
+ return;
193
+ }
194
+
195
+ // If certificatePath is provided, check if password is needed
196
+ if (signingConfig.certificatePath && !signingConfig.certificatePassword) {
197
+ // Warning but not error - some PFX files don't need passwords
198
+ }
199
+
200
+ // Validate thumbprint format if provided
201
+ if (signingConfig.certificateThumbprint) {
202
+ const thumbprintPattern = /^[A-Fa-f0-9]{40}$/;
203
+ if (
204
+ !thumbprintPattern.test(
205
+ signingConfig.certificateThumbprint.replace(/\s/g, ""),
206
+ )
207
+ ) {
208
+ errors.push(
209
+ "Certificate thumbprint must be a 40-character hexadecimal string",
210
+ );
211
+ }
212
+ }
213
+
214
+ if (errors.length > 0) {
215
+ throw new ValidationError(
216
+ "signingConfig",
217
+ "valid signing configuration",
218
+ errors.join("; "),
219
+ );
220
+ }
221
+ }
222
+
223
+ module.exports = {
224
+ validateConfig,
225
+ validatePaths,
226
+ sanitizeConfig,
227
+ validateSigningConfig,
228
+ };