@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/README.md +360 -0
- package/package.json +57 -0
- package/src/certificates.js +320 -0
- package/src/cli.js +383 -0
- package/src/constants.js +140 -0
- package/src/index.js +414 -0
- package/src/manifest.js +389 -0
- package/src/package.js +909 -0
- package/src/sea-handler-new.js +301 -0
- package/src/sea-handler.js +1124 -0
- package/src/utils.js +292 -0
- package/src/validation.js +228 -0
|
@@ -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
|
+
};
|