@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,301 @@
1
+ const path = require("path");
2
+ const fs = require("fs-extra");
3
+ const { executeCommand } = require("./utils");
4
+
5
+ /**
6
+ * Creates a Single Executable Application (SEA) using Node.js built-in capabilities with NCC bundling
7
+ * @param {string} packageDir - Directory containing the package
8
+ * @param {Object} config - Configuration object
9
+ * @param {Object} packageJson - package.json content
10
+ * @returns {Promise<boolean>} Success status
11
+ */
12
+ async function createSingleExecutableApp(packageDir, config, packageJson) {
13
+ try {
14
+ console.log(
15
+ "🔨 Creating Single Executable Application (SEA) with NCC bundling...",
16
+ );
17
+
18
+ const appName = config.appName || "app";
19
+ const executableName = `${appName.replace(/[^a-zA-Z0-9.-]/g, "_")}.exe`;
20
+ const executablePath = path.join(packageDir, executableName);
21
+
22
+ // Step 1: Install NCC if not available
23
+ console.log("📦 Installing @vercel/ncc...");
24
+ try {
25
+ executeCommand("bun install @vercel/ncc --no-save", { cwd: packageDir });
26
+ } catch (nccError) {
27
+ console.log("Installing NCC globally...");
28
+ executeCommand("bun install -g @vercel/ncc", { cwd: packageDir });
29
+ }
30
+
31
+ // Step 2: Create entry point that handles both SEA and regular execution
32
+ const mainScript = packageJson.main || "index.js";
33
+ const entryContent = `#!/usr/bin/env node
34
+ // SEA Entry Point for ${config.displayName || config.appName}
35
+
36
+ console.log('🚀 Starting ${config.displayName || config.appName}...');
37
+
38
+ // Check if running as SEA
39
+ let isSEA = false;
40
+ try {
41
+ const { sea } = require('node:sea');
42
+ isSEA = sea.isSea();
43
+ if (isSEA) {
44
+ console.log('Running in SEA mode');
45
+ // Set process title
46
+ process.title = '${config.displayName || config.appName}';
47
+ }
48
+ } catch (err) {
49
+ // SEA module not available, running in regular Node.js
50
+ console.log('Running in development mode');
51
+ }
52
+
53
+ // Set working directory to executable directory
54
+ try {
55
+ const path = require('path');
56
+ const execDir = path.dirname(process.execPath);
57
+ process.chdir(execDir);
58
+ } catch (err) {
59
+ console.warn('Could not change working directory:', err.message);
60
+ }
61
+
62
+ // Load the main application
63
+ try {
64
+ require('./${mainScript}');
65
+ } catch (error) {
66
+ console.error('❌ Failed to start application:', error.message);
67
+ console.error(error.stack);
68
+ process.exit(1);
69
+ }
70
+ `;
71
+
72
+ const entryPath = path.join(packageDir, "sea-entry.js");
73
+ await fs.writeFile(entryPath, entryContent);
74
+
75
+ // Step 3: Bundle with NCC
76
+ console.log("📦 Bundling application with NCC...");
77
+ const bundleCommand = `npx ncc build sea-entry.js -o sea-dist --minify --no-source-map-register`;
78
+ executeCommand(bundleCommand, { cwd: packageDir });
79
+
80
+ const bundledPath = path.join(packageDir, "sea-dist", "index.js");
81
+ if (!(await fs.pathExists(bundledPath))) {
82
+ throw new Error("NCC bundling failed - bundled file not found");
83
+ }
84
+
85
+ // Step 4: Create SEA configuration
86
+ const seaConfig = {
87
+ main: "sea-dist/index.js",
88
+ output: "sea-prep.blob",
89
+ disableExperimentalSEAWarning: true,
90
+ useSnapshot: false,
91
+ useCodeCache: true,
92
+ };
93
+
94
+ const seaConfigPath = path.join(packageDir, "sea-config.json");
95
+ await fs.writeJson(seaConfigPath, seaConfig, { spaces: 2 });
96
+
97
+ // Step 5: Generate SEA blob
98
+ console.log("🗜️ Generating SEA blob...");
99
+ const originalCwd = process.cwd();
100
+ process.chdir(packageDir);
101
+
102
+ try {
103
+ executeCommand("node --experimental-sea-config sea-config.json");
104
+
105
+ const blobPath = path.join(packageDir, "sea-prep.blob");
106
+ if (!(await fs.pathExists(blobPath))) {
107
+ throw new Error("SEA blob generation failed");
108
+ }
109
+
110
+ console.log("✅ SEA blob created successfully");
111
+
112
+ // Step 6: Copy Node.js executable
113
+ console.log("📄 Creating executable base...");
114
+ const nodeBinaryPath = await getNodeBinaryPath();
115
+ await fs.copyFile(nodeBinaryPath, executablePath);
116
+
117
+ if (!(await fs.pathExists(executablePath))) {
118
+ throw new Error("Failed to create executable base");
119
+ }
120
+
121
+ // Step 7: Remove signature (Windows)
122
+ console.log("🔓 Removing executable signature...");
123
+ try {
124
+ const { findWindowsSDKTools } = require("./utils");
125
+ const tools = findWindowsSDKTools();
126
+ const removeSignCommand = `"${tools.signtool}" remove /s "${executableName}"`;
127
+ executeCommand(removeSignCommand);
128
+ } catch (sigError) {
129
+ console.warn("⚠️ Could not remove signature (this is usually fine)");
130
+ }
131
+
132
+ // Step 8: Inject blob with postject
133
+ console.log("💉 Injecting SEA blob into executable...");
134
+ const postjectCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`;
135
+ executeCommand(postjectCommand);
136
+
137
+ // Step 9: Sign the executable
138
+ await signSEAExecutable(executablePath, config);
139
+
140
+ // Step 10: Cleanup temporary files
141
+ await fs.remove(entryPath);
142
+ await fs.remove(path.join(packageDir, "sea-dist"));
143
+ await fs.remove(seaConfigPath);
144
+ await fs.remove(blobPath);
145
+
146
+ // Update config
147
+ config.executable = executableName;
148
+ delete config.executableArgs;
149
+
150
+ console.log(`✅ SEA executable created: ${executableName}`);
151
+ return true;
152
+ } finally {
153
+ process.chdir(originalCwd);
154
+ }
155
+ } catch (error) {
156
+ console.warn(`⚠️ SEA creation failed: ${error.message}`);
157
+ console.log("Falling back to PKG...");
158
+ return false;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Gets the Node.js binary path for the current platform
164
+ * @returns {Promise<string>} Path to Node.js binary
165
+ */
166
+ async function getNodeBinaryPath() {
167
+ return process.execPath;
168
+ }
169
+
170
+ /**
171
+ * Signs the SEA executable
172
+ * @param {string} executablePath - Path to executable
173
+ * @param {Object} config - Configuration object
174
+ */
175
+ async function signSEAExecutable(executablePath, config) {
176
+ try {
177
+ const { determineSigningMethod } = require("./certificates");
178
+ const { findWindowsSDKTools } = require("./utils");
179
+
180
+ const signingMethod = await determineSigningMethod(config);
181
+ if (!signingMethod) {
182
+ console.log("No signing configuration found, skipping...");
183
+ return;
184
+ }
185
+
186
+ const tools = findWindowsSDKTools();
187
+ let signCommand;
188
+
189
+ if (signingMethod.method === "pfx") {
190
+ const { pfxPath, password, timestampUrl } = signingMethod.parameters;
191
+ signCommand = `"${tools.signtool}" sign /f "${pfxPath}" /p "${password}" /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${executablePath}"`;
192
+ } else if (signingMethod.method === "store") {
193
+ const { thumbprint, store, timestampUrl } = signingMethod.parameters;
194
+ const storeLocation = store.toLowerCase() === "localmachine" ? "/sm" : "";
195
+ signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${executablePath}"`;
196
+ }
197
+
198
+ if (signCommand) {
199
+ executeCommand(signCommand);
200
+ console.log("✅ SEA executable signed successfully");
201
+ }
202
+ } catch (error) {
203
+ console.warn(`Warning: Could not sign SEA executable: ${error.message}`);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Creates a PKG fallback executable
209
+ * @param {string} packageDir - Package directory path
210
+ * @param {Object} config - Configuration object
211
+ * @param {Object} packageJson - package.json content
212
+ */
213
+ async function createFallbackLauncher(packageDir, config, packageJson) {
214
+ try {
215
+ console.log("📦 Creating PKG fallback executable...");
216
+
217
+ const executableName = `${config.appName || "app"}.exe`;
218
+ const executablePath = path.join(packageDir, executableName);
219
+ const mainScript = packageJson.main || "index.js";
220
+
221
+ // Create launcher script with proper directory handling
222
+ const launcherContent = `#!/usr/bin/env node
223
+ // PKG Launcher for ${config.displayName || config.appName}
224
+
225
+ console.log('🚀 Starting ${config.displayName || config.appName}...');
226
+
227
+ // Set working directory - handle PKG snapshot case
228
+ try {
229
+ if (__dirname.includes('snapshot')) {
230
+ // Running from PKG, use the executable's directory
231
+ const path = require('path');
232
+ const execDir = path.dirname(process.execPath);
233
+ process.chdir(execDir);
234
+ } else {
235
+ // Running in development mode
236
+ process.chdir(__dirname);
237
+ }
238
+ } catch (err) {
239
+ console.warn('Could not change working directory:', err.message);
240
+ }
241
+
242
+ // Load the main application
243
+ try {
244
+ require('./${mainScript}');
245
+ } catch (error) {
246
+ console.error('❌ Failed to start application:', error.message);
247
+ console.error(error.stack);
248
+ process.exit(1);
249
+ }
250
+ `;
251
+
252
+ const launcherScript = path.join(packageDir, "launcher.js");
253
+ await fs.writeFile(launcherScript, launcherContent);
254
+
255
+ // Create PKG configuration
256
+ const pkgConfig = {
257
+ name: config.appName || "app",
258
+ version: packageJson.version || "1.0.0",
259
+ main: "launcher.js",
260
+ bin: "launcher.js",
261
+ pkg: {
262
+ scripts: [mainScript],
263
+ targets: ["node18-win-x64"],
264
+ outputPath: ".",
265
+ },
266
+ dependencies: packageJson.dependencies || {},
267
+ };
268
+
269
+ const pkgConfigPath = path.join(packageDir, "pkg-config.json");
270
+ await fs.writeJson(pkgConfigPath, pkgConfig, { spaces: 2 });
271
+
272
+ // Create executable with PKG
273
+ const finalExeName = path.basename(executablePath, ".exe");
274
+ const pkgCommand = `npx pkg launcher.js --target node18-win-x64 --output "${finalExeName}" --config pkg-config.json`;
275
+
276
+ executeCommand(pkgCommand, { cwd: packageDir });
277
+
278
+ // Verify executable was created
279
+ if (await fs.pathExists(executablePath)) {
280
+ console.log(`✅ Created executable with PKG: ${executableName}`);
281
+
282
+ // Clean up temporary files
283
+ await fs.remove(launcherScript);
284
+ await fs.remove(pkgConfigPath);
285
+
286
+ // Update config
287
+ config.executable = executableName;
288
+ delete config.executableArgs;
289
+
290
+ return true;
291
+ }
292
+ } catch (pkgError) {
293
+ console.warn(`⚠️ PKG fallback failed: ${pkgError.message}`);
294
+ return false;
295
+ }
296
+ }
297
+
298
+ module.exports = {
299
+ createSingleExecutableApp,
300
+ createFallbackLauncher,
301
+ };