@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,389 @@
1
+ const path = require("path");
2
+ const fs = require("fs-extra");
3
+ const { CONSTANTS, ValidationError } = require("./constants");
4
+
5
+ /**
6
+ * Generates a GUID for PhoneProductId
7
+ * @returns {string} A UUID v4 string
8
+ */
9
+ function generateGuid() {
10
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
11
+ const r = (Math.random() * 16) | 0;
12
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
13
+ return v.toString(16);
14
+ });
15
+ }
16
+
17
+ /**
18
+ * Generates the AppxManifest.xml content
19
+ * @param {Object} config - Configuration object
20
+ * @param {Object} packageJson - package.json content
21
+ * @returns {string} XML manifest content
22
+ */
23
+ function generateManifest(config, packageJson) {
24
+ // Validate required fields for manifest generation
25
+ validateManifestConfig(config);
26
+
27
+ const manifest = `<?xml version="1.0" encoding="utf-8"?>
28
+
29
+ <Package
30
+ xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
31
+ xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
32
+ xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
33
+ xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
34
+ xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
35
+ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
36
+ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
37
+ IgnorableNamespaces="uap uap3 uap5 desktop rescap">
38
+
39
+ <Identity
40
+ Name="${config.packageName}"
41
+ Publisher="${config.publisher}"
42
+ Version="${config.version}" />
43
+
44
+ <mp:PhoneIdentity PhoneProductId="${generateGuid()}" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
45
+
46
+ <Properties>
47
+ <DisplayName>${config.displayName}</DisplayName>
48
+ <PublisherDisplayName>${getPublisherDisplayName(config.publisher)}</PublisherDisplayName>
49
+ <Logo>Assets\\StoreLogo.png</Logo>
50
+ </Properties>
51
+
52
+ <Dependencies>
53
+ <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
54
+ <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
55
+ </Dependencies>
56
+
57
+ <Resources>
58
+ <Resource Language="en-US"/>
59
+ </Resources>
60
+
61
+ <Applications>
62
+ <Application Id="App"
63
+ Executable="${path.basename(config.executable)}"
64
+ EntryPoint="Windows.FullTrustApplication">
65
+ <uap:VisualElements
66
+ DisplayName="${config.displayName}"
67
+ Description="${config.description || config.displayName}"
68
+ BackgroundColor="${config.backgroundColor || "transparent"}"
69
+ Square150x150Logo="Assets\\Square150x150Logo.png"
70
+ Square44x44Logo="Assets\\Square44x44Logo.png">
71
+ <uap:DefaultTile Wide310x150Logo="Assets\\Wide310x150Logo.png" />
72
+ <uap:SplashScreen Image="Assets\\SplashScreen.png" />
73
+ </uap:VisualElements>
74
+ <Extensions>
75
+ ${
76
+ config.mcpServer
77
+ ? ` <uap3:Extension Category="windows.appExtension">
78
+ <uap3:AppExtension
79
+ Name="com.microsoft.windows.ai.mcpServer"
80
+ Id="MCPServer"
81
+ DisplayName="${config.displayName}"
82
+ PublicFolder="Assets">
83
+ <uap3:Properties>
84
+ <TrustedLaunch>true</TrustedLaunch>
85
+ <Registration>mcpServerConfig.json</Registration>
86
+ </uap3:Properties>
87
+ </uap3:AppExtension>
88
+ </uap3:Extension>`
89
+ : ""
90
+ }
91
+ <uap5:Extension Category="windows.appExecutionAlias">
92
+ <uap5:AppExecutionAlias>
93
+ <uap5:ExecutionAlias Alias="${config.alias || path.basename(config.executable)}"/>
94
+ </uap5:AppExecutionAlias>
95
+ </uap5:Extension>
96
+ ${generateDesktopExtension(config)}
97
+ </Extensions>
98
+ </Application>
99
+ </Applications>
100
+
101
+ <Capabilities>
102
+ ${generateCapabilities(config.capabilities)}
103
+ </Capabilities>
104
+ </Package>`;
105
+
106
+ return manifest;
107
+ }
108
+
109
+ /**
110
+ * Validates configuration for manifest generation
111
+ * @param {Object} config - Configuration object
112
+ * @throws {ValidationError} When validation fails
113
+ */
114
+ function validateManifestConfig(config) {
115
+ const requiredFields = [
116
+ "packageName",
117
+ "publisher",
118
+ "version",
119
+ "displayName",
120
+ "description",
121
+ "architecture",
122
+ "executable",
123
+ ];
124
+ const missing = requiredFields.filter((field) => !config[field]);
125
+
126
+ if (missing.length > 0) {
127
+ throw new ValidationError(
128
+ "manifest config",
129
+ "all required fields",
130
+ `Missing fields: ${missing.join(", ")}`,
131
+ );
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Extracts publisher display name from publisher string
137
+ * @param {string} publisher - Publisher string (e.g., "CN=MyCompany, O=MyOrg")
138
+ * @returns {string} Display name
139
+ */
140
+ function getPublisherDisplayName(publisher) {
141
+ // Extract CN (Common Name) from publisher string
142
+ const cnMatch = publisher.match(/CN=([^,]+)/);
143
+ return cnMatch ? cnMatch[1].trim() : publisher;
144
+ }
145
+
146
+ /**
147
+ * Generates capabilities XML section
148
+ * @param {Array} capabilities - Array of capabilities
149
+ * @returns {string} Capabilities XML
150
+ */
151
+ function generateCapabilities(capabilities = ["internetClient"]) {
152
+ // Always ensure runFullTrust capability is included for Node.js apps
153
+ const allCapabilities = [...capabilities];
154
+ if (!allCapabilities.includes("runFullTrust")) {
155
+ allCapabilities.push("runFullTrust");
156
+ }
157
+
158
+ // Always include internetClient if not specified
159
+ if (!allCapabilities.includes("internetClient")) {
160
+ allCapabilities.unshift("internetClient");
161
+ }
162
+
163
+ return allCapabilities
164
+ .map((capability) => {
165
+ // Handle different capability types
166
+ if (capability === "runFullTrust") {
167
+ return ' <rescap:Capability Name="runFullTrust" />';
168
+ } else if (capability.startsWith("rescap:")) {
169
+ return ` <rescap:Capability Name="${capability.replace("rescap:", "")}" />`;
170
+ } else if (capability.startsWith("uap:")) {
171
+ return ` <uap:Capability Name="${capability.replace("uap:", "")}" />`;
172
+ } else {
173
+ return ` <Capability Name="${capability}" />`;
174
+ }
175
+ })
176
+ .join("\n");
177
+ }
178
+
179
+ /**
180
+ * Generates desktop extension XML section for full trust process
181
+ * @param {Object} config - Configuration object
182
+ * @returns {string} Desktop extension XML
183
+ */
184
+ function generateDesktopExtension(config) {
185
+ // Only generate desktop extension if we have arguments and executable is node.exe
186
+ if (config.arguments && config.executable === "node.exe") {
187
+ return ` <desktop:Extension Category="windows.fullTrustProcess" Executable="${config.executable}">
188
+ <desktop:FullTrustProcess>
189
+ <desktop:ParameterGroup GroupId="NodeJsGroup" Parameters="${config.arguments}" />
190
+ </desktop:FullTrustProcess>
191
+ </desktop:Extension>`;
192
+ }
193
+ return "";
194
+ }
195
+
196
+ /**
197
+ * Creates default assets for the package
198
+ * @param {string} assetsDir - Directory to create assets in
199
+ * @param {string} iconPath - Path to source icon file (optional)
200
+ */
201
+ async function createDefaultAssets(assetsDir, iconPath = null) {
202
+ await fs.ensureDir(assetsDir);
203
+
204
+ // Asset sizes required by MSIX
205
+ const requiredAssets = [
206
+ { name: "Square44x44Logo.png", size: 44 },
207
+ { name: "Square150x150Logo.png", size: 150 },
208
+ { name: "Wide310x150Logo.png", width: 310, height: 150 },
209
+ { name: "StoreLogo.png", size: 50 },
210
+ { name: "SplashScreen.png", width: 620, height: 300 },
211
+ ];
212
+
213
+ if (iconPath && (await fs.pathExists(iconPath))) {
214
+ // If source icon is provided, try to use it
215
+ await copyAndResizeIcon(iconPath, assetsDir, requiredAssets);
216
+ } else {
217
+ // Create placeholder assets
218
+ await createPlaceholderAssets(assetsDir, requiredAssets);
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Copies and resizes icon for different asset sizes
224
+ * @param {string} iconPath - Source icon path
225
+ * @param {string} assetsDir - Assets directory
226
+ * @param {Array} requiredAssets - Required asset configurations
227
+ */
228
+ async function copyAndResizeIcon(iconPath, assetsDir, requiredAssets) {
229
+ try {
230
+ // Check if we have ImageMagick available for resizing
231
+ const { executeCommand } = require("./utils");
232
+
233
+ for (const asset of requiredAssets) {
234
+ const outputPath = path.join(assetsDir, asset.name);
235
+
236
+ try {
237
+ if (asset.size) {
238
+ // Square asset
239
+ executeCommand(
240
+ `magick "${iconPath}" -resize ${asset.size}x${asset.size} "${outputPath}"`,
241
+ { silent: true },
242
+ );
243
+ } else {
244
+ // Rectangular asset
245
+ executeCommand(
246
+ `magick "${iconPath}" -resize ${asset.width}x${asset.height} "${outputPath}"`,
247
+ { silent: true },
248
+ );
249
+ }
250
+ console.log(`Created asset: ${asset.name}`);
251
+ } catch (error) {
252
+ // Fallback to copying original icon
253
+ await fs.copy(iconPath, outputPath);
254
+ console.log(`Copied original icon as: ${asset.name} (resize failed)`);
255
+ }
256
+ }
257
+ } catch (error) {
258
+ console.warn("ImageMagick not available, creating placeholder assets");
259
+ await createPlaceholderAssets(assetsDir, requiredAssets);
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Creates placeholder PNG assets
265
+ * @param {string} assetsDir - Assets directory
266
+ * @param {Array} requiredAssets - Required asset configurations
267
+ */
268
+ async function createPlaceholderAssets(assetsDir, requiredAssets) {
269
+ // Create simple placeholder PNGs programmatically
270
+ for (const asset of requiredAssets) {
271
+ const outputPath = path.join(assetsDir, asset.name);
272
+
273
+ const width = asset.width || asset.size;
274
+ const height = asset.height || asset.size;
275
+
276
+ // Create a simple PNG placeholder
277
+ await createSimplePNG(outputPath, width, height);
278
+ console.log(
279
+ `Created placeholder asset: ${asset.name} (${width}x${height})`,
280
+ );
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Creates a simple PNG file programmatically
286
+ * @param {string} outputPath - Output file path
287
+ * @param {number} width - Image width
288
+ * @param {number} height - Image height
289
+ */
290
+ async function createSimplePNG(outputPath, width, height) {
291
+ // Create a minimal PNG file
292
+ // This is a very basic 1x1 transparent PNG that MSIX will accept
293
+ const minimalPNG = Buffer.from([
294
+ 0x89,
295
+ 0x50,
296
+ 0x4e,
297
+ 0x47,
298
+ 0x0d,
299
+ 0x0a,
300
+ 0x1a,
301
+ 0x0a, // PNG signature
302
+ 0x00,
303
+ 0x00,
304
+ 0x00,
305
+ 0x0d, // IHDR chunk size
306
+ 0x49,
307
+ 0x48,
308
+ 0x44,
309
+ 0x52, // IHDR
310
+ 0x00,
311
+ 0x00,
312
+ 0x00,
313
+ 0x01, // width: 1
314
+ 0x00,
315
+ 0x00,
316
+ 0x00,
317
+ 0x01, // height: 1
318
+ 0x08,
319
+ 0x06,
320
+ 0x00,
321
+ 0x00,
322
+ 0x00, // bit depth: 8, color type: 6 (RGBA), compression: 0, filter: 0, interlace: 0
323
+ 0x1f,
324
+ 0x15,
325
+ 0xc4,
326
+ 0x89, // IHDR CRC
327
+ 0x00,
328
+ 0x00,
329
+ 0x00,
330
+ 0x0a, // IDAT chunk size
331
+ 0x49,
332
+ 0x44,
333
+ 0x41,
334
+ 0x54, // IDAT
335
+ 0x78,
336
+ 0x9c,
337
+ 0x62,
338
+ 0x00,
339
+ 0x00,
340
+ 0x00,
341
+ 0x02,
342
+ 0x00,
343
+ 0x01, // compressed data (transparent pixel)
344
+ 0xe2,
345
+ 0x21,
346
+ 0xbc,
347
+ 0x33, // IDAT CRC
348
+ 0x00,
349
+ 0x00,
350
+ 0x00,
351
+ 0x00, // IEND chunk size
352
+ 0x49,
353
+ 0x45,
354
+ 0x4e,
355
+ 0x44, // IEND
356
+ 0xae,
357
+ 0x42,
358
+ 0x60,
359
+ 0x82, // IEND CRC
360
+ ]);
361
+
362
+ await fs.writeFile(outputPath, minimalPNG);
363
+ }
364
+
365
+ /**
366
+ * Generates a resource configuration file
367
+ * @param {Object} config - Configuration object
368
+ * @returns {string} Resource configuration content
369
+ */
370
+ function generateResourceConfig(config) {
371
+ const resourceConfig = {
372
+ packageName: config.packageName,
373
+ displayName: config.displayName,
374
+ description: config.description,
375
+ version: config.version,
376
+ architecture: config.architecture,
377
+ capabilities: config.capabilities,
378
+ generated: new Date().toISOString(),
379
+ };
380
+
381
+ return JSON.stringify(resourceConfig, null, 2);
382
+ }
383
+
384
+ module.exports = {
385
+ generateManifest,
386
+ createDefaultAssets,
387
+ generateResourceConfig,
388
+ validateManifestConfig,
389
+ };