@atom-js-org/cli 0.2.0-alpha.0 → 0.4.0-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.2.0-alpha.0",
3
+ "version": "0.4.0-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
@@ -39,8 +39,8 @@ async function localBuild(project, target, options = {}) {
39
39
  }
40
40
 
41
41
  const buildRoot = path.join(project.root, 'build', target);
42
- const unpacked = path.join(buildRoot, 'unpacked');
43
- const appDir = path.join(unpacked, 'app');
42
+ const unpacked = path.join(buildRoot, 'portable');
43
+ const appDir = null;
44
44
  const productName = sanitizeFilename(project.config.productName);
45
45
 
46
46
  console.log(`\nAtomJS build (${target})`);
@@ -50,56 +50,102 @@ async function localBuild(project, target, options = {}) {
50
50
  await fse.remove(buildRoot);
51
51
  await fse.ensureDir(unpacked);
52
52
 
53
- const stageRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'atomjs-stage-'));
53
+ const stageBase = await resolveShortStageBase();
54
+ const stageRoot = await fs.promises.mkdtemp(path.join(stageBase, 's-'));
54
55
  const stagedApp = path.join(stageRoot, 'app');
56
+ const payloadPath = path.join(stageRoot, 'atom-app.payload.gz');
57
+
55
58
  try {
56
59
  await copyApplication(project.root, stagedApp);
57
60
  await vendorFramework(stagedApp, project, target);
58
61
  await installProductionDependencies(stagedApp, options.skipInstall, target);
59
- await fse.move(stagedApp, appDir, { overwrite: true });
60
- } finally {
61
- await fse.remove(stageRoot);
62
- }
63
62
 
64
- const runtimeDir = path.join(unpacked, 'runtime');
65
- await fse.ensureDir(runtimeDir);
66
- const runtimeNodeName = target === 'windows' ? 'node.exe' : 'node';
67
- const runtimeNodePath = path.join(runtimeDir, runtimeNodeName);
68
- await fs.promises.copyFile(process.execPath, runtimeNodePath);
69
- if (target !== 'windows') await fs.promises.chmod(runtimeNodePath, 0o755);
63
+ console.log('Embedding application code and production dependencies into the executable...');
64
+ await createApplicationPayload(stagedApp, payloadPath);
65
+
66
+ const executableName = target === 'windows' ? `${productName}.exe` : productName;
67
+ const executablePath = path.join(unpacked, executableName);
68
+ await createSeaLauncher({
69
+ executablePath,
70
+ appDir,
71
+ target,
72
+ productName,
73
+ payloadPath
74
+ });
75
+
76
+ const creditPath = path.join(unpacked, 'ATOMJS-CREDIT.txt');
77
+ await fs.promises.writeFile(
78
+ creditPath,
79
+ 'Built with AtomJS\nhttps://github.com/Atom-js-org/atom\nCredit is optional inside applications.\n',
80
+ 'utf8'
81
+ );
70
82
 
71
- const executableName = target === 'windows' ? `${productName}.exe` : productName;
72
- const executablePath = path.join(unpacked, executableName);
73
- await createSeaLauncher({ executablePath, appDir, target });
83
+ let outputs = [];
84
+ let runPath = executablePath;
85
+ let unpackedPath = unpacked;
74
86
 
75
- const creditPath = path.join(unpacked, 'ATOMJS-CREDIT.txt');
76
- await fs.promises.writeFile(
77
- creditPath,
78
- 'Built with AtomJS\nhttps://github.com/Atom-js-org/atom\nCredit is optional inside applications.\n',
79
- 'utf8'
80
- );
87
+ if (target === 'windows') {
88
+ outputs = await packageWindows({ project, buildRoot, unpacked, executableName, productName });
89
+ }
90
+ if (target === 'macos') {
91
+ const packaged = await packageMacOS({
92
+ project,
93
+ buildRoot,
94
+ unpacked,
95
+ executableName,
96
+ productName,
97
+ hostSource: path.join(resolveFrameworkRoot(project.root), 'src', 'runtime', 'macos-native-host.m')
98
+ });
99
+ outputs = packaged.outputs;
100
+ runPath = packaged.appBundle;
101
+ unpackedPath = packaged.appBundle;
102
+ await fse.remove(unpacked);
103
+ }
104
+ if (target === 'linux') {
105
+ outputs = await packageLinux({ project, buildRoot, unpacked, executableName, productName });
106
+ }
81
107
 
82
- const outputs = [];
83
- if (target === 'windows') outputs.push(...await packageWindows({ project, buildRoot, unpacked, executableName, productName }));
84
- if (target === 'macos') outputs.push(...await packageMacOS({ project, buildRoot, unpacked, executableName, productName }));
85
- if (target === 'linux') outputs.push(...await packageLinux({ project, buildRoot, unpacked, executableName, productName }));
86
-
87
- const manifest = {
88
- atomjsVersion: '0.2.0-alpha.0',
89
- target,
90
- productName,
91
- appId: project.config.appId,
92
- createdAt: new Date().toISOString(),
93
- run: path.relative(buildRoot, executablePath),
94
- unpacked: path.relative(buildRoot, unpacked),
95
- outputs: outputs.map((file) => path.relative(buildRoot, file))
96
- };
97
- await fs.promises.writeFile(path.join(buildRoot, 'manifest.json'), JSON.stringify(manifest, null, 2));
108
+ const manifest = {
109
+ atomjsVersion: '0.3.0-alpha.0',
110
+ target,
111
+ productName,
112
+ appId: project.config.appId,
113
+ createdAt: new Date().toISOString(),
114
+ run: path.relative(buildRoot, runPath),
115
+ unpacked: path.relative(buildRoot, unpackedPath),
116
+ outputs: outputs.map((file) => path.relative(buildRoot, file))
117
+ };
118
+ await fs.promises.writeFile(path.join(buildRoot, 'manifest.json'), JSON.stringify(manifest, null, 2));
119
+
120
+ console.log('\nBuild complete:');
121
+ console.log(` ${path.relative(project.root, unpackedPath)}`);
122
+ for (const output of outputs) console.log(` ${path.relative(project.root, output)}`);
123
+ return manifest;
124
+ } finally {
125
+ await fse.remove(stageRoot);
126
+ }
127
+ }
98
128
 
99
- console.log('\nBuild complete:');
100
- console.log(` ${path.relative(project.root, unpacked)}`);
101
- for (const output of outputs) console.log(` ${path.relative(project.root, output)}`);
102
- return manifest;
129
+
130
+ async function resolveShortStageBase() {
131
+ const configured = process.env.ATOMJS_TEMP_DIR || process.env.ATOM_TEMP_DIR;
132
+ const candidates = configured
133
+ ? [path.resolve(configured)]
134
+ : process.platform === 'win32'
135
+ ? [path.join(path.parse(process.cwd()).root, '.atomjs-tmp'), path.join(os.tmpdir(), 'atomjs')]
136
+ : [path.join(os.tmpdir(), 'atomjs')];
137
+
138
+ let lastError;
139
+ for (const candidate of candidates) {
140
+ try {
141
+ await fse.ensureDir(candidate);
142
+ await fs.promises.access(candidate, fs.constants.W_OK);
143
+ return candidate;
144
+ } catch (error) {
145
+ lastError = error;
146
+ }
147
+ }
148
+ throw new Error(`AtomJS could not create a writable short staging directory: ${lastError ? lastError.message : 'unknown error'}`);
103
149
  }
104
150
 
105
151
  async function copyApplication(projectRoot, appDir) {
@@ -188,43 +234,68 @@ async function installProductionDependencies(appDir, skipInstall, target) {
188
234
  }
189
235
  }
190
236
 
191
- async function createSeaLauncher({ executablePath, appDir, target }) {
192
- const work = path.join(path.dirname(executablePath), '.sea-' + crypto.randomBytes(5).toString('hex'));
193
- await fse.ensureDir(work);
194
- const launcherPath = path.join(work, 'launcher.cjs');
195
- const blobPath = path.join(work, target === 'windows' ? 'sea-prep.blob.exe' : 'sea-prep.blob');
196
- const configPath = path.join(work, 'sea-config.json');
237
+ async function createApplicationPayload(appDir, outputPath) {
238
+ const files = [];
197
239
 
198
- const launcher = `
199
- 'use strict';
200
- const path = require('node:path');
201
- const fs = require('node:fs');
202
- const { createRequire } = require('node:module');
240
+ async function addFile(source, relative, stat) {
241
+ files.push({
242
+ path: relative.split(path.sep).join('/'),
243
+ mode: stat.mode & 0o777,
244
+ data: (await fs.promises.readFile(source)).toString('base64')
245
+ });
246
+ }
203
247
 
204
- const executableDir = path.dirname(process.execPath);
205
- const appDir = process.platform === 'darwin' && executableDir.includes('.app' + path.sep + 'Contents' + path.sep + 'MacOS')
206
- ? path.resolve(executableDir, '..', 'Resources', 'app')
207
- : path.join(executableDir, 'app');
248
+ async function visit(directory, relativePrefix = '', ancestors = new Set()) {
249
+ const realDirectory = await fs.promises.realpath(directory);
250
+ if (ancestors.has(realDirectory)) return;
251
+ const nextAncestors = new Set(ancestors);
252
+ nextAncestors.add(realDirectory);
253
+
254
+ const entries = await fs.promises.readdir(directory, { withFileTypes: true });
255
+ entries.sort((a, b) => a.name.localeCompare(b.name));
256
+
257
+ for (const entry of entries) {
258
+ const absolute = path.join(directory, entry.name);
259
+ const relative = relativePrefix ? path.join(relativePrefix, entry.name) : entry.name;
260
+
261
+ if (entry.isDirectory()) {
262
+ await visit(absolute, relative, nextAncestors);
263
+ continue;
264
+ }
265
+
266
+ if (entry.isSymbolicLink()) {
267
+ const resolved = await fs.promises.realpath(absolute);
268
+ const stat = await fs.promises.stat(resolved);
269
+ if (stat.isDirectory()) {
270
+ await visit(resolved, relative, nextAncestors);
271
+ } else if (stat.isFile()) {
272
+ await addFile(resolved, relative, stat);
273
+ }
274
+ continue;
275
+ }
276
+
277
+ if (entry.isFile()) {
278
+ const stat = await fs.promises.stat(absolute);
279
+ await addFile(absolute, relative, stat);
280
+ }
281
+ }
282
+ }
208
283
 
209
- const packagePath = path.join(appDir, 'package.json');
210
- if (!fs.existsSync(packagePath)) {
211
- console.error('AtomJS application files are missing:', appDir);
212
- process.exit(1);
284
+ await visit(appDir);
285
+ const payload = Buffer.from(JSON.stringify({ format: 1, files }));
286
+ const compressed = require('node:zlib').gzipSync(payload, { level: 9 });
287
+ await fs.promises.writeFile(outputPath, compressed);
213
288
  }
214
289
 
215
- const runtimeNode = process.platform === 'darwin' && executableDir.includes('.app' + path.sep + 'Contents' + path.sep + 'MacOS')
216
- ? path.resolve(executableDir, '..', 'Resources', 'runtime', 'node')
217
- : path.join(executableDir, 'runtime', process.platform === 'win32' ? 'node.exe' : 'node');
290
+ async function createSeaLauncher({ executablePath, appDir, target, productName, payloadPath = null }) {
291
+ const work = path.join(path.dirname(executablePath), '.sea-' + crypto.randomBytes(5).toString('hex'));
292
+ await fse.ensureDir(work);
293
+ const launcherPath = path.join(work, 'launcher.cjs');
294
+ const blobPath = path.join(work, target === 'windows' ? 'sea-prep.blob.exe' : 'sea-prep.blob');
295
+ const configPath = path.join(work, 'sea-config.json');
218
296
 
219
- process.chdir(appDir);
220
- process.env.ATOM_PROJECT_ROOT = appDir;
221
- process.env.ATOM_BUILD = '1';
222
- process.env.ATOM_NODE_EXECUTABLE = runtimeNode;
223
- const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
224
- const mainPath = path.resolve(appDir, pkg.main || 'main.js');
225
- const load = createRequire(packagePath);
226
- load(mainPath);
227
- `;
297
+ if (!payloadPath) throw new Error('AtomJS SEA payload is required.');
298
+ const launcher = createEmbeddedLauncherSource(productName);
228
299
  await fs.promises.writeFile(launcherPath, launcher, 'utf8');
229
300
 
230
301
  const seaConfig = {
@@ -232,7 +303,8 @@ load(mainPath);
232
303
  output: blobPath,
233
304
  disableExperimentalSEAWarning: true,
234
305
  useSnapshot: false,
235
- useCodeCache: false
306
+ useCodeCache: false,
307
+ assets: { 'atom-app': payloadPath }
236
308
  };
237
309
  await fs.promises.writeFile(configPath, JSON.stringify(seaConfig, null, 2));
238
310
 
@@ -250,11 +322,85 @@ load(mainPath);
250
322
 
251
323
  if (target !== 'windows') await fs.promises.chmod(executablePath, 0o755);
252
324
  if (target === 'macos' && commandExists('codesign', ['--version'])) {
253
- await run('codesign', ['--sign', '-', executablePath]);
325
+ await run('codesign', ['--force', '--sign', '-', executablePath]);
254
326
  }
255
327
  await fse.remove(work);
256
328
  }
257
329
 
330
+ function createEmbeddedLauncherSource(productName) {
331
+ return `
332
+ 'use strict';
333
+ const path = require('node:path');
334
+ const fs = require('node:fs');
335
+ const os = require('node:os');
336
+ const crypto = require('node:crypto');
337
+ const zlib = require('node:zlib');
338
+ const { createRequire } = require('node:module');
339
+ const { pathToFileURL } = require('node:url');
340
+ const { getAsset } = require('node:sea');
341
+
342
+ const productName = ${JSON.stringify(productName)};
343
+ const payload = Buffer.from(getAsset('atom-app'));
344
+ const payloadHash = crypto.createHash('sha256').update(payload).digest('hex').slice(0, 24);
345
+ const dataRoot = process.platform === 'darwin'
346
+ ? path.join(os.homedir(), 'Library', 'Application Support')
347
+ : process.platform === 'win32'
348
+ ? (process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'))
349
+ : (process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'));
350
+ const appDir = path.join(dataRoot, productName, 'AtomJS Runtime', payloadHash);
351
+ const marker = path.join(appDir, '.atom-ready');
352
+
353
+ if (!fs.existsSync(marker)) {
354
+ const temporary = appDir + '.tmp-' + process.pid;
355
+ fs.rmSync(temporary, { recursive: true, force: true });
356
+ fs.mkdirSync(temporary, { recursive: true });
357
+ const archive = JSON.parse(zlib.gunzipSync(payload).toString('utf8'));
358
+ if (!archive || archive.format !== 1 || !Array.isArray(archive.files)) {
359
+ throw new Error('AtomJS embedded application payload is invalid.');
360
+ }
361
+
362
+ for (const entry of archive.files) {
363
+ const destination = path.resolve(temporary, String(entry.path));
364
+ const root = path.resolve(temporary) + path.sep;
365
+ if (!destination.startsWith(root)) throw new Error('Unsafe path in AtomJS application payload.');
366
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
367
+ fs.writeFileSync(destination, Buffer.from(entry.data, 'base64'));
368
+ if (process.platform !== 'win32' && Number.isInteger(entry.mode)) fs.chmodSync(destination, entry.mode);
369
+ }
370
+ fs.writeFileSync(path.join(temporary, '.atom-ready'), payloadHash);
371
+ fs.mkdirSync(path.dirname(appDir), { recursive: true });
372
+ fs.rmSync(appDir, { recursive: true, force: true });
373
+ fs.renameSync(temporary, appDir);
374
+ }
375
+
376
+ const packagePath = path.join(appDir, 'package.json');
377
+ if (!fs.existsSync(packagePath)) throw new Error('AtomJS could not materialize the embedded application.');
378
+
379
+ const executableDir = path.dirname(process.execPath);
380
+ process.chdir(appDir);
381
+ process.env.ATOM_PROJECT_ROOT = appDir;
382
+ process.env.ATOM_BUILD = '1';
383
+ process.env.ATOM_EMBEDDED_RUNTIME = '1';
384
+ process.env.ATOM_WINDOW_HOST_ENTRY = path.join(appDir, 'vendor', 'atomjs', 'src', 'runtime', 'window-host.mjs');
385
+ process.env.ATOM_MACOS_HOST_EXECUTABLE = path.join(executableDir, 'AtomJSWindowHost');
386
+ const hostModeIndex = process.argv.indexOf('--atomjs-window-host');
387
+ if (hostModeIndex !== -1) {
388
+ const hostEntry = process.env.ATOM_WINDOW_HOST_ENTRY;
389
+ const configPath = process.argv[hostModeIndex + 1];
390
+ process.argv = [process.argv[0], hostEntry, configPath];
391
+ import(pathToFileURL(hostEntry).href).catch((error) => {
392
+ console.error(error && error.stack ? error.stack : error);
393
+ process.exit(1);
394
+ });
395
+ return;
396
+ }
397
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
398
+ const mainPath = path.resolve(appDir, pkg.main || 'main.js');
399
+ const load = createRequire(packagePath);
400
+ load(mainPath);
401
+ `;
402
+ }
403
+
258
404
  async function packageWindows({ project, buildRoot, unpacked, executableName, productName }) {
259
405
  const outputs = [];
260
406
  const zipPath = path.join(buildRoot, `${productName}-windows.zip`);
@@ -269,11 +415,14 @@ async function packageWindows({ project, buildRoot, unpacked, executableName, pr
269
415
  : '';
270
416
  const script = `
271
417
  Unicode true
418
+ SetCompressor /SOLID lzma
272
419
  !include "MUI2.nsh"
273
420
  Name "${escapeNsis(productName)}"
274
421
  OutFile "${installerPath.replace(/\\/g, '\\\\')}"
275
422
  InstallDir "$LOCALAPPDATA\\Programs\\${escapeNsis(productName)}"
276
423
  RequestExecutionLevel user
424
+ ShowInstDetails show
425
+ ShowUninstDetails show
277
426
  ${credit}
278
427
  !insertmacro MUI_PAGE_WELCOME
279
428
  !insertmacro MUI_PAGE_DIRECTORY
@@ -291,6 +440,7 @@ SectionEnd
291
440
  Section "Uninstall"
292
441
  Delete "$DESKTOP\\${escapeNsis(productName)}.lnk"
293
442
  RMDir /r "$SMPROGRAMS\\${escapeNsis(productName)}"
443
+ DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${escapeNsis(project.config.appId)}"
294
444
  RMDir /r "$INSTDIR"
295
445
  SectionEnd
296
446
  `;
@@ -305,19 +455,49 @@ SectionEnd
305
455
  return outputs;
306
456
  }
307
457
 
308
- async function packageMacOS({ project, buildRoot, unpacked, executableName, productName }) {
458
+ async function packageMacOS({ project, buildRoot, unpacked, executableName, productName, hostSource }) {
309
459
  const outputs = [];
310
460
  const appBundle = path.join(buildRoot, `${productName}.app`);
311
461
  const contents = path.join(appBundle, 'Contents');
312
462
  const macosDir = path.join(contents, 'MacOS');
313
463
  const resources = path.join(contents, 'Resources');
464
+ const mainExecutable = path.join(macosDir, productName);
465
+ const nativeHost = path.join(macosDir, 'AtomJSWindowHost');
466
+
314
467
  await fse.ensureDir(macosDir);
315
468
  await fse.ensureDir(resources);
316
- await fse.copy(path.join(unpacked, executableName), path.join(macosDir, productName));
317
- await fse.copy(path.join(unpacked, 'app'), path.join(resources, 'app'));
318
- await fse.copy(path.join(unpacked, 'runtime'), path.join(resources, 'runtime'));
469
+ await fse.copy(path.join(unpacked, executableName), mainExecutable);
319
470
  await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(resources, 'ATOMJS-CREDIT.txt'));
471
+ await fs.promises.chmod(mainExecutable, 0o755);
472
+
473
+ if (!fs.existsSync(hostSource)) {
474
+ throw new Error(`AtomJS macOS native host source was not found: ${hostSource}`);
475
+ }
476
+ if (!commandExists('/usr/bin/xcrun', ['--version'])) {
477
+ throw new Error('macOS builds require the Xcode Command Line Tools. Run `xcode-select --install`.');
478
+ }
479
+
480
+ await run('/usr/bin/xcrun', [
481
+ 'clang',
482
+ '-fobjc-arc',
483
+ '-fmodules',
484
+ '-mmacosx-version-min=12.0',
485
+ '-framework', 'Cocoa',
486
+ '-framework', 'WebKit',
487
+ hostSource,
488
+ '-o', nativeHost
489
+ ]);
490
+ await fs.promises.chmod(nativeHost, 0o755);
491
+
492
+ let iconEntry = '';
493
+ const iconSource = project.config.icon ? path.resolve(project.root, project.config.icon) : null;
494
+ if (iconSource && fs.existsSync(iconSource) && path.extname(iconSource).toLowerCase() === '.icns') {
495
+ await fse.copy(iconSource, path.join(resources, 'AppIcon.icns'));
496
+ iconEntry = '<key>CFBundleIconFile</key><string>AppIcon</string>';
497
+ }
320
498
 
499
+ const version = String(project.packageJson.version || '0.0.0');
500
+ const bundleVersion = (version.match(/\d+/g) || ['1']).slice(0, 3).join('.');
321
501
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
322
502
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
323
503
  <plist version="1.0"><dict>
@@ -326,13 +506,25 @@ async function packageMacOS({ project, buildRoot, unpacked, executableName, prod
326
506
  <key>CFBundleName</key><string>${xml(productName)}</string>
327
507
  <key>CFBundleDisplayName</key><string>${xml(productName)}</string>
328
508
  <key>CFBundlePackageType</key><string>APPL</string>
329
- <key>CFBundleShortVersionString</key><string>${xml(project.packageJson.version || '0.0.0')}</string>
509
+ <key>CFBundleShortVersionString</key><string>${xml(version)}</string>
510
+ <key>CFBundleVersion</key><string>${xml(bundleVersion)}</string>
511
+ <key>LSMinimumSystemVersion</key><string>12.0</string>
330
512
  <key>NSHighResolutionCapable</key><true/>
513
+ ${iconEntry}
331
514
  </dict></plist>`;
332
- await fs.promises.writeFile(path.join(contents, 'Info.plist'), plist, 'utf8');
333
- await fs.promises.chmod(path.join(macosDir, productName), 0o755);
515
+ const plistPath = path.join(contents, 'Info.plist');
516
+ await fs.promises.writeFile(plistPath, plist, 'utf8');
334
517
 
335
- if (commandExists('codesign', ['--version'])) await run('codesign', ['--force', '--deep', '--sign', '-', appBundle]);
518
+ if (commandExists('/usr/bin/plutil', ['-help'])) {
519
+ await run('/usr/bin/plutil', ['-lint', plistPath]);
520
+ }
521
+
522
+ if (commandExists('/usr/bin/codesign', ['--version'])) {
523
+ await run('/usr/bin/codesign', ['--force', '--sign', '-', nativeHost]);
524
+ await run('/usr/bin/codesign', ['--force', '--sign', '-', mainExecutable]);
525
+ await run('/usr/bin/codesign', ['--force', '--deep', '--sign', '-', appBundle]);
526
+ await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', appBundle]);
527
+ }
336
528
 
337
529
  const zipPath = path.join(buildRoot, `${productName}-macos.zip`);
338
530
  if (commandExists('ditto', ['-h'])) {
@@ -347,7 +539,8 @@ async function packageMacOS({ project, buildRoot, unpacked, executableName, prod
347
539
  await run('hdiutil', ['create', '-volname', productName, '-srcfolder', appBundle, '-ov', '-format', 'UDZO', dmgPath]);
348
540
  outputs.push(dmgPath);
349
541
  }
350
- return outputs;
542
+
543
+ return { outputs, appBundle };
351
544
  }
352
545
 
353
546
  async function packageLinux({ project, buildRoot, unpacked, executableName, productName }) {
@@ -362,8 +555,6 @@ async function packageLinux({ project, buildRoot, unpacked, executableName, prod
362
555
  await fse.ensureDir(usrBin);
363
556
  await fse.ensureDir(usrLibApp);
364
557
  await fse.copy(path.join(unpacked, executableName), path.join(usrBin, productName));
365
- await fse.copy(path.join(unpacked, 'app'), path.join(usrBin, 'app'));
366
- await fse.copy(path.join(unpacked, 'runtime'), path.join(usrBin, 'runtime'));
367
558
  await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(usrLibApp, 'ATOMJS-CREDIT.txt'));
368
559
 
369
560
  const appRun = `#!/bin/sh\nHERE="$(dirname "$(readlink -f "$0")")"\nexec "$HERE/usr/bin/${productName}" "$@"\n`;
@@ -502,4 +693,4 @@ function xml(value) {
502
693
  return String(value).replace(/[<>&'\"]/g, (char) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' })[char]);
503
694
  }
504
695
 
505
- module.exports = { buildCommand, localBuild };
696
+ module.exports = { buildCommand, localBuild, createApplicationPayload };
package/src/doctor.cjs CHANGED
@@ -15,9 +15,10 @@ async function doctorCommand(options = {}) {
15
15
  rows.push(check('WebView2 Runtime', checkWindowsWebView2(), 'required by the native WebView'));
16
16
  rows.push(check('NSIS (optional installer)', commandExists('makensis', ['/VERSION'])));
17
17
  } else if (process.platform === 'darwin') {
18
- rows.push(check('osascript JavaScript host', fs.existsSync('/usr/bin/osascript')));
18
+ rows.push(check('Xcode Command Line Tools', commandExists('/usr/bin/xcrun', ['--version']), 'required to compile the native Cocoa host'));
19
+ rows.push(check('Clang', commandExists('/usr/bin/xcrun', ['clang', '--version'])));
19
20
  rows.push(check('WebKit framework', fs.existsSync('/System/Library/Frameworks/WebKit.framework')));
20
- rows.push(check('codesign', commandExists('codesign', ['--version'])));
21
+ rows.push(check('codesign', commandExists('/usr/bin/codesign', ['--version'])));
21
22
  rows.push(check('hdiutil', commandExists('hdiutil', ['help'])));
22
23
  } else {
23
24
  rows.push(check('pkg-config', commandExists('pkg-config', ['--version'])));
@@ -28,7 +29,7 @@ async function doctorCommand(options = {}) {
28
29
  }
29
30
 
30
31
  if (process.platform === 'darwin') {
31
- rows.push(check('macOS WKWebView backend', true, 'built in through JavaScript for Automation'));
32
+ rows.push(check('macOS WKWebView backend', true, 'shared native Cocoa host; no osascript process'));
32
33
  } else {
33
34
  try {
34
35
  const project = loadProject(options.project);
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.2.0-alpha.0',
34
- electron: 'npm:@atom-js-org/electron@0.2.0-alpha.0'
33
+ '@atom-js-org/runtime': '0.3.0-alpha.0',
34
+ electron: 'npm:@atom-js-org/electron@0.3.0-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.2.0-alpha.0'
40
+ '@atom-js-org/cli': '0.3.0-alpha.0'
41
41
  },
42
42
  overrides: {
43
43
  tar: '7.5.20'
package/src/run.cjs CHANGED
@@ -54,7 +54,9 @@ async function runBuild(options) {
54
54
  if (!fs.existsSync(executable)) throw new Error(`Built executable not found: ${executable}`);
55
55
 
56
56
  console.log(`Running ${path.basename(executable)}`);
57
- const child = spawn(executable, [], {
57
+ const command = target === 'macos' && executable.endsWith('.app') ? 'open' : executable;
58
+ const args = command === 'open' ? ['-W', executable] : [];
59
+ const child = spawn(command, args, {
58
60
  cwd: path.dirname(executable),
59
61
  stdio: 'inherit'
60
62
  });
package/src/utils.cjs CHANGED
@@ -54,9 +54,17 @@ function loadProject(projectInput) {
54
54
  };
55
55
  }
56
56
 
57
+ function normalizeSpawn(command, args) {
58
+ if (process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(command)) {
59
+ return { command: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', command, ...args] };
60
+ }
61
+ return { command, args };
62
+ }
63
+
57
64
  function run(command, args, options = {}) {
58
65
  return new Promise((resolve, reject) => {
59
- const child = spawn(command, args, {
66
+ const normalized = normalizeSpawn(command, args);
67
+ const child = spawn(normalized.command, normalized.args, {
60
68
  cwd: options.cwd || process.cwd(),
61
69
  env: options.env || process.env,
62
70
  stdio: options.stdio || 'inherit',
@@ -71,7 +79,8 @@ function run(command, args, options = {}) {
71
79
  }
72
80
 
73
81
  function capture(command, args, options = {}) {
74
- const result = spawnSync(command, args, {
82
+ const normalized = normalizeSpawn(command, args);
83
+ const result = spawnSync(normalized.command, normalized.args, {
75
84
  cwd: options.cwd || process.cwd(),
76
85
  env: options.env || process.env,
77
86
  encoding: 'utf8',
@@ -86,7 +95,8 @@ function capture(command, args, options = {}) {
86
95
  }
87
96
 
88
97
  function commandExists(command, args = ['--version']) {
89
- const result = spawnSync(command, args, { stdio: 'ignore', shell: false });
98
+ const normalized = normalizeSpawn(command, args);
99
+ const result = spawnSync(normalized.command, normalized.args, { stdio: 'ignore', shell: false });
90
100
  return !result.error && result.status === 0;
91
101
  }
92
102