@atom-js-org/cli 0.4.0-alpha.0 → 0.4.1-alpha.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/cli",
3
- "version": "0.4.0-alpha.0",
3
+ "version": "0.4.1-alpha.0",
4
4
  "description": "CLI for running and building AtomJS desktop applications.",
5
5
  "bin": {
6
6
  "atom": "bin/atom.cjs"
package/src/build.cjs CHANGED
@@ -311,6 +311,9 @@ async function createSeaLauncher({ executablePath, appDir, target, productName,
311
311
  await run(process.execPath, ['--experimental-sea-config', configPath], { cwd: work });
312
312
  await fs.promises.copyFile(process.execPath, executablePath);
313
313
 
314
+ if (target === 'windows') {
315
+ await prepareWindowsExecutableForInjection(executablePath);
316
+ }
314
317
  if (target === 'macos' && commandExists('codesign', ['--version'])) {
315
318
  spawnSync('codesign', ['--remove-signature', executablePath], { stdio: 'ignore' });
316
319
  }
@@ -320,13 +323,66 @@ async function createSeaLauncher({ executablePath, appDir, target, productName,
320
323
  machoSegmentName: 'NODE_SEA'
321
324
  });
322
325
 
323
- if (target !== 'windows') await fs.promises.chmod(executablePath, 0o755);
326
+ if (target === 'windows') {
327
+ await setWindowsGuiSubsystem(executablePath);
328
+ } else {
329
+ await fs.promises.chmod(executablePath, 0o755);
330
+ }
324
331
  if (target === 'macos' && commandExists('codesign', ['--version'])) {
325
332
  await run('codesign', ['--force', '--sign', '-', executablePath]);
326
333
  }
327
334
  await fse.remove(work);
328
335
  }
329
336
 
337
+
338
+ async function prepareWindowsExecutableForInjection(executablePath) {
339
+ const image = await fs.promises.readFile(executablePath);
340
+ const pe = readPortableExecutableLayout(image);
341
+ const certificateDirectory = pe.dataDirectoryOffset + (8 * 4);
342
+ const certificateOffset = image.readUInt32LE(certificateDirectory);
343
+ const certificateSize = image.readUInt32LE(certificateDirectory + 4);
344
+
345
+ image.writeUInt32LE(0, certificateDirectory);
346
+ image.writeUInt32LE(0, certificateDirectory + 4);
347
+ image.writeUInt32LE(0, pe.optionalHeaderOffset + 64);
348
+
349
+ const certificateEnd = certificateOffset + certificateSize;
350
+ const output = certificateOffset > 0 && certificateSize > 0 && certificateEnd === image.length
351
+ ? image.subarray(0, certificateOffset)
352
+ : image;
353
+ await fs.promises.writeFile(executablePath, output);
354
+ }
355
+
356
+ async function setWindowsGuiSubsystem(executablePath) {
357
+ const image = await fs.promises.readFile(executablePath);
358
+ const pe = readPortableExecutableLayout(image);
359
+ image.writeUInt16LE(2, pe.optionalHeaderOffset + 68);
360
+ image.writeUInt32LE(0, pe.optionalHeaderOffset + 64);
361
+ await fs.promises.writeFile(executablePath, image);
362
+ }
363
+
364
+ function readPortableExecutableLayout(image) {
365
+ if (image.length < 0x100 || image.toString('ascii', 0, 2) !== 'MZ') {
366
+ throw new Error('AtomJS expected a Windows PE executable but the DOS header is missing.');
367
+ }
368
+
369
+ const peOffset = image.readUInt32LE(0x3c);
370
+ if (peOffset < 0x40 || peOffset + 0x100 > image.length || image.toString('ascii', peOffset, peOffset + 4) !== 'PE\\0\\0') {
371
+ throw new Error('AtomJS expected a Windows PE executable but the PE header is invalid.');
372
+ }
373
+
374
+ const optionalHeaderOffset = peOffset + 24;
375
+ const magic = image.readUInt16LE(optionalHeaderOffset);
376
+ if (magic !== 0x10b && magic !== 0x20b) {
377
+ throw new Error(`AtomJS does not support Windows PE optional-header magic 0x${magic.toString(16)}.`);
378
+ }
379
+
380
+ return {
381
+ optionalHeaderOffset,
382
+ dataDirectoryOffset: optionalHeaderOffset + (magic === 0x20b ? 112 : 96)
383
+ };
384
+ }
385
+
330
386
  function createEmbeddedLauncherSource(productName) {
331
387
  return `
332
388
  'use strict';
@@ -410,6 +466,14 @@ async function packageWindows({ project, buildRoot, unpacked, executableName, pr
410
466
  const nsisPath = path.join(buildRoot, 'installer.nsi');
411
467
  const installerPath = path.join(buildRoot, `${productName} Installer.exe`);
412
468
  const escapedSource = unpacked.replace(/\\/g, '\\\\');
469
+ const version = String(project.packageJson.version || '0.0.0');
470
+ const author = project.packageJson.author;
471
+ const publisher = typeof author === 'string'
472
+ ? author
473
+ : author && typeof author === 'object' && author.name
474
+ ? author.name
475
+ : 'AtomJS application';
476
+ const uninstallKey = `Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${escapeNsis(project.config.appId)}`;
413
477
  const credit = project.config.installerCredit
414
478
  ? `!define MUI_WELCOMEPAGE_TEXT "This installer will install ${escapeNsis(productName)}.$\\r$\\n$\\r$\\nPowered by AtomJS — https://github.com/Atom-js-org/atom"`
415
479
  : '';
@@ -427,27 +491,40 @@ ${credit}
427
491
  !insertmacro MUI_PAGE_WELCOME
428
492
  !insertmacro MUI_PAGE_DIRECTORY
429
493
  !insertmacro MUI_PAGE_INSTFILES
494
+ !define MUI_FINISHPAGE_RUN "$INSTDIR\\${escapeNsis(executableName)}"
430
495
  !insertmacro MUI_PAGE_FINISH
496
+ !insertmacro MUI_UNPAGE_CONFIRM
497
+ !insertmacro MUI_UNPAGE_INSTFILES
431
498
  !insertmacro MUI_LANGUAGE "English"
432
499
  Section "Install"
500
+ SetShellVarContext current
433
501
  SetOutPath "$INSTDIR"
434
502
  File /r "${escapedSource}\\*"
435
503
  CreateDirectory "$SMPROGRAMS\\${escapeNsis(productName)}"
436
504
  CreateShortcut "$SMPROGRAMS\\${escapeNsis(productName)}\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
437
505
  CreateShortcut "$DESKTOP\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
438
506
  WriteUninstaller "$INSTDIR\\Uninstall.exe"
507
+ WriteRegStr HKCU "${uninstallKey}" "DisplayName" "${escapeNsis(productName)}"
508
+ WriteRegStr HKCU "${uninstallKey}" "DisplayVersion" "${escapeNsis(version)}"
509
+ WriteRegStr HKCU "${uninstallKey}" "Publisher" "${escapeNsis(publisher)}"
510
+ WriteRegStr HKCU "${uninstallKey}" "DisplayIcon" "$INSTDIR\\${escapeNsis(executableName)}"
511
+ WriteRegStr HKCU "${uninstallKey}" "UninstallString" "$\\\"$INSTDIR\\Uninstall.exe$\\\""
512
+ WriteRegDWORD HKCU "${uninstallKey}" "NoModify" 1
513
+ WriteRegDWORD HKCU "${uninstallKey}" "NoRepair" 1
439
514
  SectionEnd
440
515
  Section "Uninstall"
516
+ SetShellVarContext current
441
517
  Delete "$DESKTOP\\${escapeNsis(productName)}.lnk"
442
518
  RMDir /r "$SMPROGRAMS\\${escapeNsis(productName)}"
443
- DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${escapeNsis(project.config.appId)}"
519
+ DeleteRegKey HKCU "${uninstallKey}"
444
520
  RMDir /r "$INSTDIR"
445
521
  SectionEnd
446
522
  `;
447
523
  await fs.promises.writeFile(nsisPath, script.trimStart(), 'utf8');
448
524
 
449
- if (commandExists('makensis', ['/VERSION'])) {
450
- await run('makensis', [nsisPath]);
525
+ const makensis = resolveNsisExecutable();
526
+ if (makensis) {
527
+ await run(makensis, [nsisPath]);
451
528
  if (fs.existsSync(installerPath)) outputs.push(installerPath);
452
529
  } else {
453
530
  console.warn('NSIS was not found; installer.nsi was generated but the .exe installer was skipped.');
@@ -455,6 +532,19 @@ SectionEnd
455
532
  return outputs;
456
533
  }
457
534
 
535
+ function resolveNsisExecutable() {
536
+ const candidates = [
537
+ process.env.MAKENSIS_PATH,
538
+ process.env.NSIS_HOME && path.join(process.env.NSIS_HOME, 'makensis.exe'),
539
+ process.env['ProgramFiles(x86)'] && path.join(process.env['ProgramFiles(x86)'], 'NSIS', 'makensis.exe'),
540
+ process.env.ProgramFiles && path.join(process.env.ProgramFiles, 'NSIS', 'makensis.exe'),
541
+ process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs', 'NSIS', 'makensis.exe')
542
+ ].filter(Boolean);
543
+
544
+ if (commandExists('makensis', ['/VERSION'])) return 'makensis';
545
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
546
+ }
547
+
458
548
  async function packageMacOS({ project, buildRoot, unpacked, executableName, productName, hostSource }) {
459
549
  const outputs = [];
460
550
  const appBundle = path.join(buildRoot, `${productName}.app`);
package/src/doctor.cjs CHANGED
@@ -12,8 +12,9 @@ async function doctorCommand(options = {}) {
12
12
 
13
13
  if (process.platform === 'win32') {
14
14
  rows.push(check('PowerShell', commandExists('powershell.exe', ['-NoProfile', '-Command', '$PSVersionTable.PSVersion.ToString()'])));
15
- rows.push(check('WebView2 Runtime', checkWindowsWebView2(), 'required by the native WebView'));
16
- rows.push(check('NSIS (optional installer)', commandExists('makensis', ['/VERSION'])));
15
+ const webView2Version = getWindowsWebView2Version();
16
+ rows.push(check('WebView2 Runtime', Boolean(webView2Version), webView2Version || 'required by the native WebView'));
17
+ rows.push(check('NSIS (optional installer)', Boolean(resolveNsisExecutable())));
17
18
  } else if (process.platform === 'darwin') {
18
19
  rows.push(check('Xcode Command Line Tools', commandExists('/usr/bin/xcrun', ['--version']), 'required to compile the native Cocoa host'));
19
20
  rows.push(check('Clang', commandExists('/usr/bin/xcrun', ['clang', '--version'])));
@@ -69,18 +70,43 @@ function pkgConfigExists(name) {
69
70
  return !result.error && result.status === 0;
70
71
  }
71
72
 
72
- function checkWindowsWebView2() {
73
+ function getWindowsWebView2Version() {
73
74
  const script = `
74
75
  $paths = @(
75
- 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}',
76
- 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}',
77
- 'HKCU:\\Software\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}'
76
+ 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}',
77
+ 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}',
78
+ 'HKCU:\\Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}'
78
79
  )
79
- foreach ($p in $paths) { if (Test-Path $p) { exit 0 } }
80
+ foreach ($p in $paths) {
81
+ try {
82
+ $version = Get-ItemPropertyValue -Path $p -Name 'pv' -ErrorAction Stop
83
+ if ($version -and $version -ne '0.0.0.0') {
84
+ Write-Output $version
85
+ exit 0
86
+ }
87
+ } catch {}
88
+ }
80
89
  exit 1
81
90
  `;
82
- const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], { stdio: 'ignore' });
83
- return !result.error && result.status === 0;
91
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], {
92
+ encoding: 'utf8',
93
+ windowsHide: true
94
+ });
95
+ if (result.error || result.status !== 0) return null;
96
+ return String(result.stdout || '').trim() || null;
97
+ }
98
+
99
+ function resolveNsisExecutable() {
100
+ const candidates = [
101
+ process.env.MAKENSIS_PATH,
102
+ process.env.NSIS_HOME && path.join(process.env.NSIS_HOME, 'makensis.exe'),
103
+ process.env['ProgramFiles(x86)'] && path.join(process.env['ProgramFiles(x86)'], 'NSIS', 'makensis.exe'),
104
+ process.env.ProgramFiles && path.join(process.env.ProgramFiles, 'NSIS', 'makensis.exe'),
105
+ process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs', 'NSIS', 'makensis.exe')
106
+ ].filter(Boolean);
107
+
108
+ if (commandExists('makensis', ['/VERSION'])) return 'makensis';
109
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
84
110
  }
85
111
 
86
112
  module.exports = { doctorCommand };