@atom-js-org/cli 0.4.0-alpha.0 → 0.4.2-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.2-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
@@ -106,7 +106,7 @@ async function localBuild(project, target, options = {}) {
106
106
  }
107
107
 
108
108
  const manifest = {
109
- atomjsVersion: '0.3.0-alpha.0',
109
+ atomjsVersion: '0.4.2-alpha.0',
110
110
  target,
111
111
  productName,
112
112
  appId: project.config.appId,
@@ -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,79 @@ 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
+ const coffHeaderSize = 24;
371
+ if (peOffset < 0x40 || peOffset + coffHeaderSize > image.length || image.readUInt32LE(peOffset) !== 0x00004550) {
372
+ throw new Error('AtomJS expected a Windows PE executable but the PE header is invalid.');
373
+ }
374
+
375
+ const optionalHeaderSize = image.readUInt16LE(peOffset + 20);
376
+ const optionalHeaderOffset = peOffset + coffHeaderSize;
377
+ const optionalHeaderEnd = optionalHeaderOffset + optionalHeaderSize;
378
+ if (optionalHeaderSize < 2 || optionalHeaderEnd > image.length) {
379
+ throw new Error('AtomJS expected a complete Windows PE optional header.');
380
+ }
381
+
382
+ const magic = image.readUInt16LE(optionalHeaderOffset);
383
+ if (magic !== 0x10b && magic !== 0x20b) {
384
+ throw new Error(`AtomJS does not support Windows PE optional-header magic 0x${magic.toString(16)}.`);
385
+ }
386
+
387
+ const dataDirectoryOffset = optionalHeaderOffset + (magic === 0x20b ? 112 : 96);
388
+ const certificateDirectoryEnd = dataDirectoryOffset + (8 * 5);
389
+ if (certificateDirectoryEnd > optionalHeaderEnd) {
390
+ throw new Error('AtomJS expected the Windows PE certificate data directory.');
391
+ }
392
+
393
+ return {
394
+ optionalHeaderOffset,
395
+ dataDirectoryOffset
396
+ };
397
+ }
398
+
330
399
  function createEmbeddedLauncherSource(productName) {
331
400
  return `
332
401
  'use strict';
@@ -410,6 +479,14 @@ async function packageWindows({ project, buildRoot, unpacked, executableName, pr
410
479
  const nsisPath = path.join(buildRoot, 'installer.nsi');
411
480
  const installerPath = path.join(buildRoot, `${productName} Installer.exe`);
412
481
  const escapedSource = unpacked.replace(/\\/g, '\\\\');
482
+ const version = String(project.packageJson.version || '0.0.0');
483
+ const author = project.packageJson.author;
484
+ const publisher = typeof author === 'string'
485
+ ? author
486
+ : author && typeof author === 'object' && author.name
487
+ ? author.name
488
+ : 'AtomJS application';
489
+ const uninstallKey = `Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${escapeNsis(project.config.appId)}`;
413
490
  const credit = project.config.installerCredit
414
491
  ? `!define MUI_WELCOMEPAGE_TEXT "This installer will install ${escapeNsis(productName)}.$\\r$\\n$\\r$\\nPowered by AtomJS — https://github.com/Atom-js-org/atom"`
415
492
  : '';
@@ -427,27 +504,40 @@ ${credit}
427
504
  !insertmacro MUI_PAGE_WELCOME
428
505
  !insertmacro MUI_PAGE_DIRECTORY
429
506
  !insertmacro MUI_PAGE_INSTFILES
507
+ !define MUI_FINISHPAGE_RUN "$INSTDIR\\${escapeNsis(executableName)}"
430
508
  !insertmacro MUI_PAGE_FINISH
509
+ !insertmacro MUI_UNPAGE_CONFIRM
510
+ !insertmacro MUI_UNPAGE_INSTFILES
431
511
  !insertmacro MUI_LANGUAGE "English"
432
512
  Section "Install"
513
+ SetShellVarContext current
433
514
  SetOutPath "$INSTDIR"
434
515
  File /r "${escapedSource}\\*"
435
516
  CreateDirectory "$SMPROGRAMS\\${escapeNsis(productName)}"
436
517
  CreateShortcut "$SMPROGRAMS\\${escapeNsis(productName)}\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
437
518
  CreateShortcut "$DESKTOP\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
438
519
  WriteUninstaller "$INSTDIR\\Uninstall.exe"
520
+ WriteRegStr HKCU "${uninstallKey}" "DisplayName" "${escapeNsis(productName)}"
521
+ WriteRegStr HKCU "${uninstallKey}" "DisplayVersion" "${escapeNsis(version)}"
522
+ WriteRegStr HKCU "${uninstallKey}" "Publisher" "${escapeNsis(publisher)}"
523
+ WriteRegStr HKCU "${uninstallKey}" "DisplayIcon" "$INSTDIR\\${escapeNsis(executableName)}"
524
+ WriteRegStr HKCU "${uninstallKey}" "UninstallString" "$\\\"$INSTDIR\\Uninstall.exe$\\\""
525
+ WriteRegDWORD HKCU "${uninstallKey}" "NoModify" 1
526
+ WriteRegDWORD HKCU "${uninstallKey}" "NoRepair" 1
439
527
  SectionEnd
440
528
  Section "Uninstall"
529
+ SetShellVarContext current
441
530
  Delete "$DESKTOP\\${escapeNsis(productName)}.lnk"
442
531
  RMDir /r "$SMPROGRAMS\\${escapeNsis(productName)}"
443
- DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${escapeNsis(project.config.appId)}"
532
+ DeleteRegKey HKCU "${uninstallKey}"
444
533
  RMDir /r "$INSTDIR"
445
534
  SectionEnd
446
535
  `;
447
536
  await fs.promises.writeFile(nsisPath, script.trimStart(), 'utf8');
448
537
 
449
- if (commandExists('makensis', ['/VERSION'])) {
450
- await run('makensis', [nsisPath]);
538
+ const makensis = resolveNsisExecutable();
539
+ if (makensis) {
540
+ await run(makensis, [nsisPath]);
451
541
  if (fs.existsSync(installerPath)) outputs.push(installerPath);
452
542
  } else {
453
543
  console.warn('NSIS was not found; installer.nsi was generated but the .exe installer was skipped.');
@@ -455,6 +545,19 @@ SectionEnd
455
545
  return outputs;
456
546
  }
457
547
 
548
+ function resolveNsisExecutable() {
549
+ const candidates = [
550
+ process.env.MAKENSIS_PATH,
551
+ process.env.NSIS_HOME && path.join(process.env.NSIS_HOME, 'makensis.exe'),
552
+ process.env['ProgramFiles(x86)'] && path.join(process.env['ProgramFiles(x86)'], 'NSIS', 'makensis.exe'),
553
+ process.env.ProgramFiles && path.join(process.env.ProgramFiles, 'NSIS', 'makensis.exe'),
554
+ process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Programs', 'NSIS', 'makensis.exe')
555
+ ].filter(Boolean);
556
+
557
+ if (commandExists('makensis', ['/VERSION'])) return 'makensis';
558
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
559
+ }
560
+
458
561
  async function packageMacOS({ project, buildRoot, unpacked, executableName, productName, hostSource }) {
459
562
  const outputs = [];
460
563
  const appBundle = path.join(buildRoot, `${productName}.app`);
@@ -693,4 +796,4 @@ function xml(value) {
693
796
  return String(value).replace(/[<>&'\"]/g, (char) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' })[char]);
694
797
  }
695
798
 
696
- module.exports = { buildCommand, localBuild, createApplicationPayload };
799
+ module.exports = { buildCommand, localBuild, createApplicationPayload, readPortableExecutableLayout };
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 };
package/src/init.cjs CHANGED
@@ -30,14 +30,14 @@ async function initCommand(directory, options = {}) {
30
30
  start: 'atom run build'
31
31
  },
32
32
  dependencies: {
33
- '@atom-js-org/runtime': '0.3.0-alpha.0',
34
- electron: 'npm:@atom-js-org/electron@0.3.0-alpha.0'
33
+ '@atom-js-org/runtime': '0.4.2-alpha.0',
34
+ electron: 'npm:@atom-js-org/electron@0.4.2-alpha.0'
35
35
  },
36
36
  optionalDependencies: {
37
37
  'webview-nodejs': '0.5.0'
38
38
  },
39
39
  devDependencies: {
40
- '@atom-js-org/cli': '0.3.0-alpha.0'
40
+ '@atom-js-org/cli': '0.4.2-alpha.0'
41
41
  },
42
42
  overrides: {
43
43
  tar: '7.5.20'