@atom-js-org/cli 0.2.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/LICENSE +31 -0
- package/README.md +14 -0
- package/bin/atom.cjs +52 -0
- package/package.json +38 -0
- package/src/build.cjs +505 -0
- package/src/doctor.cjs +85 -0
- package/src/electron-compat.cjs +79 -0
- package/src/index.cjs +8 -0
- package/src/init.cjs +254 -0
- package/src/run.cjs +67 -0
- package/src/utils.cjs +121 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
AtomJS Attribution License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AtomJS contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to use,
|
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
8
|
+
Software, and to permit persons to whom the Software is furnished to do so,
|
|
9
|
+
subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
1. Framework redistributions, forks, and modified framework distributions must
|
|
12
|
+
retain this license, the NOTICE file, and a reasonably visible attribution
|
|
13
|
+
that includes both the name "AtomJS" and the project link:
|
|
14
|
+
https://github.com/Atom-js-org/atom
|
|
15
|
+
|
|
16
|
+
2. The attribution requirement applies to distributions of the AtomJS
|
|
17
|
+
framework itself or a derivative framework. It does not require applications
|
|
18
|
+
created with AtomJS to display AtomJS branding, notices, splash screens, or
|
|
19
|
+
user-interface credit.
|
|
20
|
+
|
|
21
|
+
3. The names and logos of AtomJS may not be used to imply endorsement of a
|
|
22
|
+
modified distribution without prior permission. Forks may accurately state
|
|
23
|
+
that they are based on AtomJS.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
package/README.md
ADDED
package/bin/atom.cjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
const pkg = require('../package.json');
|
|
6
|
+
const { runCommand } = require('../src/run.cjs');
|
|
7
|
+
const { buildCommand } = require('../src/build.cjs');
|
|
8
|
+
const { doctorCommand } = require('../src/doctor.cjs');
|
|
9
|
+
const { initCommand } = require('../src/init.cjs');
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('atom')
|
|
15
|
+
.description('Build fast, lightweight desktop apps with the system WebView')
|
|
16
|
+
.version(pkg.version);
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.command('run')
|
|
20
|
+
.description('Run an AtomJS project')
|
|
21
|
+
.argument('<mode>', 'dev or build')
|
|
22
|
+
.option('-p, --project <path>', 'project directory', process.cwd())
|
|
23
|
+
.action(async (mode, options) => runCommand(mode, options));
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('build')
|
|
27
|
+
.description('Build an AtomJS project')
|
|
28
|
+
.argument('<target>', 'windows, macos, linux, or all')
|
|
29
|
+
.option('-p, --project <path>', 'project directory', process.cwd())
|
|
30
|
+
.option('--local', 'never use the remote build workflow')
|
|
31
|
+
.option('--remote', 'always use the remote build workflow')
|
|
32
|
+
.option('--skip-install', 'reuse staged node_modules when possible')
|
|
33
|
+
.action(async (target, options) => buildCommand(target, options));
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('doctor')
|
|
37
|
+
.description('Check Node.js and native WebView prerequisites')
|
|
38
|
+
.option('-p, --project <path>', 'project directory', process.cwd())
|
|
39
|
+
.action(async (options) => doctorCommand(options));
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command('init')
|
|
43
|
+
.description('Create a new AtomJS project')
|
|
44
|
+
.argument('[directory]', 'new project directory', '.')
|
|
45
|
+
.option('--name <name>', 'package name')
|
|
46
|
+
.action(async (directory, options) => initCommand(directory, options));
|
|
47
|
+
|
|
48
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
49
|
+
console.error(`\nAtomJS error: ${error.message}`);
|
|
50
|
+
if (process.env.ATOM_DEBUG === '1' && error.stack) console.error(error.stack);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atom-js-org/cli",
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
|
+
"description": "CLI for running and building AtomJS desktop applications.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"atom": "bin/atom.cjs"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/index.cjs",
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20.12"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"archiver": "7.0.1",
|
|
20
|
+
"commander": "^15.0.0",
|
|
21
|
+
"fs-extra": "^11.3.6",
|
|
22
|
+
"postject": "1.0.0-alpha.6"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/Atom-js-org/atom.git",
|
|
27
|
+
"directory": "packages/cli"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/Atom-js-org/atom#readme",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/Atom-js-org/atom/issues"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public",
|
|
35
|
+
"tag": "alpha"
|
|
36
|
+
},
|
|
37
|
+
"license": "LicenseRef-AtomJS-1.0"
|
|
38
|
+
}
|
package/src/build.cjs
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
|
+
const fse = require('fs-extra');
|
|
9
|
+
const archiver = require('archiver');
|
|
10
|
+
const { inject } = require('postject');
|
|
11
|
+
const {
|
|
12
|
+
loadProject,
|
|
13
|
+
hostTarget,
|
|
14
|
+
run,
|
|
15
|
+
capture,
|
|
16
|
+
commandExists,
|
|
17
|
+
validateTarget,
|
|
18
|
+
sanitizeFilename
|
|
19
|
+
} = require('./utils.cjs');
|
|
20
|
+
const { resolveElectronCompatibilityRoot } = require('./electron-compat.cjs');
|
|
21
|
+
|
|
22
|
+
async function buildCommand(targetInput, options = {}) {
|
|
23
|
+
const target = validateTarget(targetInput);
|
|
24
|
+
const project = loadProject(options.project);
|
|
25
|
+
const host = hostTarget();
|
|
26
|
+
|
|
27
|
+
if (options.local && (target === 'all' || target !== host)) {
|
|
28
|
+
throw new Error(`A local ${host} machine cannot produce a complete '${target}' release. Remove --local to use GitHub Actions.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const needsRemote = options.remote || target === 'all' || target !== host;
|
|
32
|
+
if (needsRemote) return remoteBuild(project, target);
|
|
33
|
+
return localBuild(project, host, options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function localBuild(project, target, options = {}) {
|
|
37
|
+
if (target !== hostTarget()) {
|
|
38
|
+
throw new Error(`Local target mismatch: host is ${hostTarget()}, requested ${target}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const buildRoot = path.join(project.root, 'build', target);
|
|
42
|
+
const unpacked = path.join(buildRoot, 'unpacked');
|
|
43
|
+
const appDir = path.join(unpacked, 'app');
|
|
44
|
+
const productName = sanitizeFilename(project.config.productName);
|
|
45
|
+
|
|
46
|
+
console.log(`\nAtomJS build (${target})`);
|
|
47
|
+
console.log(`Project: ${project.root}`);
|
|
48
|
+
console.log(`Output: ${buildRoot}`);
|
|
49
|
+
|
|
50
|
+
await fse.remove(buildRoot);
|
|
51
|
+
await fse.ensureDir(unpacked);
|
|
52
|
+
|
|
53
|
+
const stageRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'atomjs-stage-'));
|
|
54
|
+
const stagedApp = path.join(stageRoot, 'app');
|
|
55
|
+
try {
|
|
56
|
+
await copyApplication(project.root, stagedApp);
|
|
57
|
+
await vendorFramework(stagedApp, project, target);
|
|
58
|
+
await installProductionDependencies(stagedApp, options.skipInstall, target);
|
|
59
|
+
await fse.move(stagedApp, appDir, { overwrite: true });
|
|
60
|
+
} finally {
|
|
61
|
+
await fse.remove(stageRoot);
|
|
62
|
+
}
|
|
63
|
+
|
|
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);
|
|
70
|
+
|
|
71
|
+
const executableName = target === 'windows' ? `${productName}.exe` : productName;
|
|
72
|
+
const executablePath = path.join(unpacked, executableName);
|
|
73
|
+
await createSeaLauncher({ executablePath, appDir, target });
|
|
74
|
+
|
|
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
|
+
);
|
|
81
|
+
|
|
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));
|
|
98
|
+
|
|
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;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function copyApplication(projectRoot, appDir) {
|
|
106
|
+
await fse.copy(projectRoot, appDir, {
|
|
107
|
+
dereference: false,
|
|
108
|
+
filter(source) {
|
|
109
|
+
const relative = path.relative(projectRoot, source);
|
|
110
|
+
if (!relative) return true;
|
|
111
|
+
const first = relative.split(path.sep)[0];
|
|
112
|
+
return !['node_modules', 'build', '.git', '.atom'].includes(first);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function vendorFramework(appDir, project, target) {
|
|
118
|
+
const frameworkRoot = resolveFrameworkRoot(project.root);
|
|
119
|
+
const electronCompatRoot = resolveElectronCompatibilityRoot(project.root);
|
|
120
|
+
const vendorRoot = path.join(appDir, 'vendor', 'atomjs');
|
|
121
|
+
const electronVendorRoot = path.join(appDir, 'vendor', 'electron-compat');
|
|
122
|
+
await fse.ensureDir(path.dirname(vendorRoot));
|
|
123
|
+
await fse.copy(frameworkRoot, vendorRoot, {
|
|
124
|
+
filter(source) {
|
|
125
|
+
const relative = path.relative(frameworkRoot, source);
|
|
126
|
+
return !relative.startsWith('node_modules') && !relative.startsWith('.git');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
await fse.copy(electronCompatRoot, electronVendorRoot, {
|
|
130
|
+
filter(source) {
|
|
131
|
+
const relative = path.relative(electronCompatRoot, source);
|
|
132
|
+
return !relative.startsWith('node_modules') && !relative.startsWith('.git');
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const packagePath = path.join(appDir, 'package.json');
|
|
137
|
+
const pkg = JSON.parse(await fs.promises.readFile(packagePath, 'utf8'));
|
|
138
|
+
pkg.dependencies = {
|
|
139
|
+
...(pkg.dependencies || {}),
|
|
140
|
+
'@atom-js-org/runtime': 'file:vendor/atomjs',
|
|
141
|
+
electron: 'file:vendor/electron-compat'
|
|
142
|
+
};
|
|
143
|
+
if (target !== 'macos' && process.env.ATOM_SKIP_WEBVIEW_CHECK !== '1') {
|
|
144
|
+
pkg.dependencies['webview-nodejs'] = '0.5.0';
|
|
145
|
+
if (pkg.optionalDependencies) delete pkg.optionalDependencies['webview-nodejs'];
|
|
146
|
+
} else if (pkg.optionalDependencies) {
|
|
147
|
+
delete pkg.optionalDependencies['webview-nodejs'];
|
|
148
|
+
}
|
|
149
|
+
pkg.overrides = { ...(pkg.overrides || {}), tar: '7.5.20' };
|
|
150
|
+
delete pkg.devDependencies;
|
|
151
|
+
delete pkg.workspaces;
|
|
152
|
+
delete pkg.scripts?.prepare;
|
|
153
|
+
await fs.promises.writeFile(packagePath, JSON.stringify(pkg, null, 2));
|
|
154
|
+
await fse.remove(path.join(appDir, 'package-lock.json'));
|
|
155
|
+
await fse.remove(path.join(appDir, 'npm-shrinkwrap.json'));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveFrameworkRoot(projectRoot) {
|
|
159
|
+
try {
|
|
160
|
+
return path.dirname(require.resolve('@atom-js-org/runtime/package.json', { paths: [projectRoot, process.cwd()] }));
|
|
161
|
+
} catch {
|
|
162
|
+
const sibling = path.resolve(__dirname, '..', '..', 'atomjs');
|
|
163
|
+
if (fs.existsSync(path.join(sibling, 'package.json'))) return sibling;
|
|
164
|
+
throw new Error('Could not locate @atom-js-org/runtime. Install the AtomJS runtime in the project first.');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function installProductionDependencies(appDir, skipInstall, target) {
|
|
169
|
+
if (skipInstall && fs.existsSync(path.join(appDir, 'node_modules', '@atom-js-org', 'runtime'))) return;
|
|
170
|
+
console.log('Installing production dependencies for the target OS...');
|
|
171
|
+
await run(process.platform === 'win32' ? 'npm.cmd' : 'npm', [
|
|
172
|
+
'install',
|
|
173
|
+
'--omit=dev',
|
|
174
|
+
'--no-audit',
|
|
175
|
+
'--no-fund'
|
|
176
|
+
], { cwd: appDir });
|
|
177
|
+
|
|
178
|
+
const electronPackagePath = path.join(appDir, 'node_modules', 'electron', 'package.json');
|
|
179
|
+
if (!fs.existsSync(electronPackagePath)) {
|
|
180
|
+
throw new Error('The AtomJS Electron compatibility facade was not installed in the staged application.');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (target !== 'macos') {
|
|
184
|
+
const bindingPath = path.join(appDir, 'node_modules', 'webview-nodejs');
|
|
185
|
+
if (!fs.existsSync(bindingPath) && process.env.ATOM_SKIP_WEBVIEW_CHECK !== '1') {
|
|
186
|
+
throw new Error('webview-nodejs was not installed. Run `atom doctor`, install the platform prerequisites, and retry the build.');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
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');
|
|
197
|
+
|
|
198
|
+
const launcher = `
|
|
199
|
+
'use strict';
|
|
200
|
+
const path = require('node:path');
|
|
201
|
+
const fs = require('node:fs');
|
|
202
|
+
const { createRequire } = require('node:module');
|
|
203
|
+
|
|
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');
|
|
208
|
+
|
|
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);
|
|
213
|
+
}
|
|
214
|
+
|
|
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');
|
|
218
|
+
|
|
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
|
+
`;
|
|
228
|
+
await fs.promises.writeFile(launcherPath, launcher, 'utf8');
|
|
229
|
+
|
|
230
|
+
const seaConfig = {
|
|
231
|
+
main: launcherPath,
|
|
232
|
+
output: blobPath,
|
|
233
|
+
disableExperimentalSEAWarning: true,
|
|
234
|
+
useSnapshot: false,
|
|
235
|
+
useCodeCache: false
|
|
236
|
+
};
|
|
237
|
+
await fs.promises.writeFile(configPath, JSON.stringify(seaConfig, null, 2));
|
|
238
|
+
|
|
239
|
+
await run(process.execPath, ['--experimental-sea-config', configPath], { cwd: work });
|
|
240
|
+
await fs.promises.copyFile(process.execPath, executablePath);
|
|
241
|
+
|
|
242
|
+
if (target === 'macos' && commandExists('codesign', ['--version'])) {
|
|
243
|
+
spawnSync('codesign', ['--remove-signature', executablePath], { stdio: 'ignore' });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await inject(executablePath, 'NODE_SEA_BLOB', await fs.promises.readFile(blobPath), {
|
|
247
|
+
sentinelFuse: 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2',
|
|
248
|
+
machoSegmentName: 'NODE_SEA'
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (target !== 'windows') await fs.promises.chmod(executablePath, 0o755);
|
|
252
|
+
if (target === 'macos' && commandExists('codesign', ['--version'])) {
|
|
253
|
+
await run('codesign', ['--sign', '-', executablePath]);
|
|
254
|
+
}
|
|
255
|
+
await fse.remove(work);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function packageWindows({ project, buildRoot, unpacked, executableName, productName }) {
|
|
259
|
+
const outputs = [];
|
|
260
|
+
const zipPath = path.join(buildRoot, `${productName}-windows.zip`);
|
|
261
|
+
await archiveDirectory(unpacked, zipPath, 'zip');
|
|
262
|
+
outputs.push(zipPath);
|
|
263
|
+
|
|
264
|
+
const nsisPath = path.join(buildRoot, 'installer.nsi');
|
|
265
|
+
const installerPath = path.join(buildRoot, `${productName} Installer.exe`);
|
|
266
|
+
const escapedSource = unpacked.replace(/\\/g, '\\\\');
|
|
267
|
+
const credit = project.config.installerCredit
|
|
268
|
+
? `!define MUI_WELCOMEPAGE_TEXT "This installer will install ${escapeNsis(productName)}.$\\r$\\n$\\r$\\nPowered by AtomJS — https://github.com/Atom-js-org/atom"`
|
|
269
|
+
: '';
|
|
270
|
+
const script = `
|
|
271
|
+
Unicode true
|
|
272
|
+
!include "MUI2.nsh"
|
|
273
|
+
Name "${escapeNsis(productName)}"
|
|
274
|
+
OutFile "${installerPath.replace(/\\/g, '\\\\')}"
|
|
275
|
+
InstallDir "$LOCALAPPDATA\\Programs\\${escapeNsis(productName)}"
|
|
276
|
+
RequestExecutionLevel user
|
|
277
|
+
${credit}
|
|
278
|
+
!insertmacro MUI_PAGE_WELCOME
|
|
279
|
+
!insertmacro MUI_PAGE_DIRECTORY
|
|
280
|
+
!insertmacro MUI_PAGE_INSTFILES
|
|
281
|
+
!insertmacro MUI_PAGE_FINISH
|
|
282
|
+
!insertmacro MUI_LANGUAGE "English"
|
|
283
|
+
Section "Install"
|
|
284
|
+
SetOutPath "$INSTDIR"
|
|
285
|
+
File /r "${escapedSource}\\*"
|
|
286
|
+
CreateDirectory "$SMPROGRAMS\\${escapeNsis(productName)}"
|
|
287
|
+
CreateShortcut "$SMPROGRAMS\\${escapeNsis(productName)}\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
|
|
288
|
+
CreateShortcut "$DESKTOP\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
|
|
289
|
+
WriteUninstaller "$INSTDIR\\Uninstall.exe"
|
|
290
|
+
SectionEnd
|
|
291
|
+
Section "Uninstall"
|
|
292
|
+
Delete "$DESKTOP\\${escapeNsis(productName)}.lnk"
|
|
293
|
+
RMDir /r "$SMPROGRAMS\\${escapeNsis(productName)}"
|
|
294
|
+
RMDir /r "$INSTDIR"
|
|
295
|
+
SectionEnd
|
|
296
|
+
`;
|
|
297
|
+
await fs.promises.writeFile(nsisPath, script.trimStart(), 'utf8');
|
|
298
|
+
|
|
299
|
+
if (commandExists('makensis', ['/VERSION'])) {
|
|
300
|
+
await run('makensis', [nsisPath]);
|
|
301
|
+
if (fs.existsSync(installerPath)) outputs.push(installerPath);
|
|
302
|
+
} else {
|
|
303
|
+
console.warn('NSIS was not found; installer.nsi was generated but the .exe installer was skipped.');
|
|
304
|
+
}
|
|
305
|
+
return outputs;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function packageMacOS({ project, buildRoot, unpacked, executableName, productName }) {
|
|
309
|
+
const outputs = [];
|
|
310
|
+
const appBundle = path.join(buildRoot, `${productName}.app`);
|
|
311
|
+
const contents = path.join(appBundle, 'Contents');
|
|
312
|
+
const macosDir = path.join(contents, 'MacOS');
|
|
313
|
+
const resources = path.join(contents, 'Resources');
|
|
314
|
+
await fse.ensureDir(macosDir);
|
|
315
|
+
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'));
|
|
319
|
+
await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(resources, 'ATOMJS-CREDIT.txt'));
|
|
320
|
+
|
|
321
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
322
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
323
|
+
<plist version="1.0"><dict>
|
|
324
|
+
<key>CFBundleExecutable</key><string>${xml(productName)}</string>
|
|
325
|
+
<key>CFBundleIdentifier</key><string>${xml(project.config.appId)}</string>
|
|
326
|
+
<key>CFBundleName</key><string>${xml(productName)}</string>
|
|
327
|
+
<key>CFBundleDisplayName</key><string>${xml(productName)}</string>
|
|
328
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
329
|
+
<key>CFBundleShortVersionString</key><string>${xml(project.packageJson.version || '0.0.0')}</string>
|
|
330
|
+
<key>NSHighResolutionCapable</key><true/>
|
|
331
|
+
</dict></plist>`;
|
|
332
|
+
await fs.promises.writeFile(path.join(contents, 'Info.plist'), plist, 'utf8');
|
|
333
|
+
await fs.promises.chmod(path.join(macosDir, productName), 0o755);
|
|
334
|
+
|
|
335
|
+
if (commandExists('codesign', ['--version'])) await run('codesign', ['--force', '--deep', '--sign', '-', appBundle]);
|
|
336
|
+
|
|
337
|
+
const zipPath = path.join(buildRoot, `${productName}-macos.zip`);
|
|
338
|
+
if (commandExists('ditto', ['-h'])) {
|
|
339
|
+
await run('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', appBundle, zipPath]);
|
|
340
|
+
} else {
|
|
341
|
+
await archiveDirectory(appBundle, zipPath, 'zip', `${productName}.app`);
|
|
342
|
+
}
|
|
343
|
+
outputs.push(zipPath);
|
|
344
|
+
|
|
345
|
+
if (commandExists('hdiutil', ['help'])) {
|
|
346
|
+
const dmgPath = path.join(buildRoot, `${productName}.dmg`);
|
|
347
|
+
await run('hdiutil', ['create', '-volname', productName, '-srcfolder', appBundle, '-ov', '-format', 'UDZO', dmgPath]);
|
|
348
|
+
outputs.push(dmgPath);
|
|
349
|
+
}
|
|
350
|
+
return outputs;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function packageLinux({ project, buildRoot, unpacked, executableName, productName }) {
|
|
354
|
+
const outputs = [];
|
|
355
|
+
const tarPath = path.join(buildRoot, `${productName}-linux.tar.gz`);
|
|
356
|
+
await archiveDirectory(unpacked, tarPath, 'tar');
|
|
357
|
+
outputs.push(tarPath);
|
|
358
|
+
|
|
359
|
+
const appDir = path.join(buildRoot, `${productName}.AppDir`);
|
|
360
|
+
const usrBin = path.join(appDir, 'usr', 'bin');
|
|
361
|
+
const usrLibApp = path.join(appDir, 'usr', 'lib', sanitizeFilename(project.packageJson.name || productName));
|
|
362
|
+
await fse.ensureDir(usrBin);
|
|
363
|
+
await fse.ensureDir(usrLibApp);
|
|
364
|
+
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
|
+
await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(usrLibApp, 'ATOMJS-CREDIT.txt'));
|
|
368
|
+
|
|
369
|
+
const appRun = `#!/bin/sh\nHERE="$(dirname "$(readlink -f "$0")")"\nexec "$HERE/usr/bin/${productName}" "$@"\n`;
|
|
370
|
+
await fs.promises.writeFile(path.join(appDir, 'AppRun'), appRun, { mode: 0o755 });
|
|
371
|
+
const desktopName = sanitizeFilename(project.packageJson.name || productName).toLowerCase().replace(/\s+/g, '-');
|
|
372
|
+
const desktop = `[Desktop Entry]\nType=Application\nName=${productName}\nExec=${productName}\nIcon=${desktopName}\nCategories=Development;Utility;\nTerminal=false\n`;
|
|
373
|
+
await fs.promises.writeFile(path.join(appDir, `${desktopName}.desktop`), desktop, 'utf8');
|
|
374
|
+
|
|
375
|
+
const iconSource = project.config.icon ? path.resolve(project.root, project.config.icon) : null;
|
|
376
|
+
if (iconSource && fs.existsSync(iconSource)) {
|
|
377
|
+
await fse.copy(iconSource, path.join(appDir, `${desktopName}.png`));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const tool = findAppImageTool();
|
|
381
|
+
if (tool) {
|
|
382
|
+
const appImagePath = path.join(buildRoot, `${productName}.AppImage`);
|
|
383
|
+
await run(tool, [appDir, appImagePath], { env: { ...process.env, ARCH: process.arch === 'arm64' ? 'aarch64' : 'x86_64' } });
|
|
384
|
+
outputs.push(appImagePath);
|
|
385
|
+
} else {
|
|
386
|
+
console.warn('appimagetool was not found; AppDir was created but AppImage generation was skipped.');
|
|
387
|
+
}
|
|
388
|
+
return outputs;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function findAppImageTool() {
|
|
392
|
+
for (const name of ['appimagetool', 'appimagetool.AppImage']) {
|
|
393
|
+
if (commandExists(name, ['--version'])) return name;
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function archiveDirectory(source, output, format, rootName) {
|
|
399
|
+
await fse.ensureDir(path.dirname(output));
|
|
400
|
+
await new Promise((resolve, reject) => {
|
|
401
|
+
const stream = fs.createWriteStream(output);
|
|
402
|
+
const archive = format === 'zip'
|
|
403
|
+
? archiver('zip', { zlib: { level: 9 } })
|
|
404
|
+
: archiver('tar', { gzip: true, gzipOptions: { level: 9 } });
|
|
405
|
+
stream.on('close', resolve);
|
|
406
|
+
stream.on('error', reject);
|
|
407
|
+
archive.on('error', reject);
|
|
408
|
+
archive.pipe(stream);
|
|
409
|
+
archive.directory(source, rootName || false);
|
|
410
|
+
archive.finalize();
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function remoteBuild(project, target) {
|
|
415
|
+
if (!commandExists('gh', ['--version'])) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
"Cross-platform builds require GitHub CLI. Install it, run 'gh auth login', and try again."
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const repositoryRoot = resolveGitRepository(project.root);
|
|
422
|
+
const auth = spawnSync('gh', ['auth', 'status'], { cwd: repositoryRoot, stdio: 'ignore' });
|
|
423
|
+
if (auth.status !== 0) {
|
|
424
|
+
throw new Error("GitHub CLI is not authenticated. Run 'gh auth login' and try again.");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const workflow = project.config.github.workflow || 'atom-build.yml';
|
|
428
|
+
const branch = capture('git', ['branch', '--show-current'], { cwd: repositoryRoot }).trim() || 'main';
|
|
429
|
+
const relativeProject = path.relative(repositoryRoot, project.root).split(path.sep).join('/') || '.';
|
|
430
|
+
const dispatchTime = Date.now();
|
|
431
|
+
|
|
432
|
+
console.log(`Dispatching remote AtomJS build '${target}' through ${workflow}...`);
|
|
433
|
+
console.log(`Repository: ${repositoryRoot}`);
|
|
434
|
+
console.log(`Project: ${relativeProject}`);
|
|
435
|
+
|
|
436
|
+
await run('gh', [
|
|
437
|
+
'workflow', 'run', workflow,
|
|
438
|
+
'--ref', branch,
|
|
439
|
+
'-f', `target=${target}`,
|
|
440
|
+
'-f', `project=${relativeProject}`
|
|
441
|
+
], { cwd: repositoryRoot });
|
|
442
|
+
|
|
443
|
+
const runId = await waitForWorkflowRun(repositoryRoot, workflow, dispatchTime);
|
|
444
|
+
await run('gh', ['run', 'watch', String(runId), '--exit-status'], { cwd: repositoryRoot });
|
|
445
|
+
|
|
446
|
+
const temp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'atomjs-build-'));
|
|
447
|
+
try {
|
|
448
|
+
await run('gh', ['run', 'download', String(runId), '--dir', temp], { cwd: repositoryRoot });
|
|
449
|
+
const buildRoot = path.join(project.root, 'build');
|
|
450
|
+
await fse.ensureDir(buildRoot);
|
|
451
|
+
for (const entry of await fs.promises.readdir(temp, { withFileTypes: true })) {
|
|
452
|
+
if (!entry.isDirectory()) continue;
|
|
453
|
+
const match = entry.name.match(/^atom-build-(windows|macos|linux)$/);
|
|
454
|
+
if (!match) continue;
|
|
455
|
+
const osTarget = match[1];
|
|
456
|
+
await fse.remove(path.join(buildRoot, osTarget));
|
|
457
|
+
await fse.copy(path.join(temp, entry.name), path.join(buildRoot, osTarget));
|
|
458
|
+
console.log(`Downloaded build/${osTarget}`);
|
|
459
|
+
}
|
|
460
|
+
} finally {
|
|
461
|
+
await fse.remove(temp);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function resolveGitRepository(startDirectory) {
|
|
466
|
+
const result = spawnSync('git', ['-C', startDirectory, 'rev-parse', '--show-toplevel'], {
|
|
467
|
+
encoding: 'utf8',
|
|
468
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
if (result.status !== 0) {
|
|
472
|
+
throw new Error([
|
|
473
|
+
'Cross-platform builds require the project to be inside a GitHub repository.',
|
|
474
|
+
`Not a Git repository: ${startDirectory}`,
|
|
475
|
+
'',
|
|
476
|
+
'Create one, commit the project and push it to GitHub before running atom build all.'
|
|
477
|
+
].join('\n'));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return result.stdout.trim();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function waitForWorkflowRun(repositoryRoot, workflow, dispatchTime) {
|
|
484
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
485
|
+
const output = capture('gh', [
|
|
486
|
+
'run', 'list', '--workflow', workflow, '--event', 'workflow_dispatch',
|
|
487
|
+
'--limit', '10', '--json', 'databaseId,createdAt'
|
|
488
|
+
], { cwd: repositoryRoot });
|
|
489
|
+
const runs = JSON.parse(output);
|
|
490
|
+
const match = runs.find((run) => new Date(run.createdAt).getTime() >= dispatchTime - 5000);
|
|
491
|
+
if (match?.databaseId) return match.databaseId;
|
|
492
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
493
|
+
}
|
|
494
|
+
throw new Error('The workflow was dispatched, but AtomJS could not find the GitHub Actions run.');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function escapeNsis(value) {
|
|
498
|
+
return String(value).replace(/\$/g, '$$').replace(/"/g, '$\\"');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function xml(value) {
|
|
502
|
+
return String(value).replace(/[<>&'\"]/g, (char) => ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"' })[char]);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
module.exports = { buildCommand, localBuild };
|
package/src/doctor.cjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const { loadProject, commandExists } = require('./utils.cjs');
|
|
7
|
+
|
|
8
|
+
async function doctorCommand(options = {}) {
|
|
9
|
+
const rows = [];
|
|
10
|
+
rows.push(check('Node.js >= 20.12', isNodeSupported(), process.version));
|
|
11
|
+
rows.push(check('npm', commandExists(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['--version'])));
|
|
12
|
+
|
|
13
|
+
if (process.platform === 'win32') {
|
|
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'])));
|
|
17
|
+
} else if (process.platform === 'darwin') {
|
|
18
|
+
rows.push(check('osascript JavaScript host', fs.existsSync('/usr/bin/osascript')));
|
|
19
|
+
rows.push(check('WebKit framework', fs.existsSync('/System/Library/Frameworks/WebKit.framework')));
|
|
20
|
+
rows.push(check('codesign', commandExists('codesign', ['--version'])));
|
|
21
|
+
rows.push(check('hdiutil', commandExists('hdiutil', ['help'])));
|
|
22
|
+
} else {
|
|
23
|
+
rows.push(check('pkg-config', commandExists('pkg-config', ['--version'])));
|
|
24
|
+
rows.push(check('GTK 3', pkgConfigExists('gtk+-3.0'), 'install libgtk-3-dev'));
|
|
25
|
+
rows.push(check('WebKitGTK 4.1', pkgConfigExists('webkit2gtk-4.1'), 'install libwebkit2gtk-4.1-dev'));
|
|
26
|
+
rows.push(check('zenity (dialogs)', commandExists('zenity', ['--version'])));
|
|
27
|
+
rows.push(check('appimagetool (optional)', commandExists('appimagetool', ['--version']) || commandExists('appimagetool.AppImage', ['--version'])));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (process.platform === 'darwin') {
|
|
31
|
+
rows.push(check('macOS WKWebView backend', true, 'built in through JavaScript for Automation'));
|
|
32
|
+
} else {
|
|
33
|
+
try {
|
|
34
|
+
const project = loadProject(options.project);
|
|
35
|
+
const binding = require.resolve('webview-nodejs/package.json', { paths: [project.root, process.cwd()] });
|
|
36
|
+
rows.push(check('webview-nodejs package', true, path.dirname(binding)));
|
|
37
|
+
} catch {
|
|
38
|
+
rows.push(check('webview-nodejs package', false, 'install platform prerequisites, then run npm install webview-nodejs'));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log('\nAtomJS doctor\n');
|
|
43
|
+
let failed = false;
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
console.log(`${row.ok ? '✓' : '✗'} ${row.name}${row.detail ? ` — ${row.detail}` : ''}`);
|
|
46
|
+
if (!row.ok && !row.name.includes('optional')) failed = true;
|
|
47
|
+
}
|
|
48
|
+
console.log('');
|
|
49
|
+
if (failed) {
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
console.log('One or more required checks failed.');
|
|
52
|
+
} else {
|
|
53
|
+
console.log('Required checks passed.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isNodeSupported() {
|
|
58
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
59
|
+
return major > 20 || (major === 20 && minor >= 12);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function check(name, ok, detail = '') {
|
|
63
|
+
return { name, ok: Boolean(ok), detail };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pkgConfigExists(name) {
|
|
67
|
+
const result = spawnSync('pkg-config', ['--exists', name]);
|
|
68
|
+
return !result.error && result.status === 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function checkWindowsWebView2() {
|
|
72
|
+
const script = `
|
|
73
|
+
$paths = @(
|
|
74
|
+
'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}',
|
|
75
|
+
'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}',
|
|
76
|
+
'HKCU:\\Software\\Microsoft\\EdgeUpdate\\Clients\\{F1E7E4A4-BD05-43A5-BCC0-B7F5E0E9D7F5}'
|
|
77
|
+
)
|
|
78
|
+
foreach ($p in $paths) { if (Test-Path $p) { exit 0 } }
|
|
79
|
+
exit 1
|
|
80
|
+
`;
|
|
81
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], { stdio: 'ignore' });
|
|
82
|
+
return !result.error && result.status === 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { doctorCommand };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const fse = require('fs-extra');
|
|
6
|
+
|
|
7
|
+
async function ensureElectronCompatibility(projectRoot) {
|
|
8
|
+
const target = path.join(projectRoot, 'node_modules', 'electron');
|
|
9
|
+
const targetPackage = path.join(target, 'package.json');
|
|
10
|
+
|
|
11
|
+
if (fs.existsSync(targetPackage)) {
|
|
12
|
+
const existing = readJson(targetPackage);
|
|
13
|
+
if (existing && existing.atomjsElectronCompatibility === true) return target;
|
|
14
|
+
|
|
15
|
+
throw new Error(
|
|
16
|
+
"A real or third-party 'electron' package already exists in node_modules. " +
|
|
17
|
+
"AtomJS cannot safely replace it automatically. Remove that package and run AtomJS again; " +
|
|
18
|
+
"AtomJS will install its lightweight Electron compatibility facade."
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const source = resolveElectronCompatibilityRoot(projectRoot);
|
|
23
|
+
await fse.ensureDir(path.dirname(target));
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const resolvedSource = await fs.promises.realpath(source);
|
|
27
|
+
|
|
28
|
+
await fs.promises.symlink(
|
|
29
|
+
resolvedSource,
|
|
30
|
+
target,
|
|
31
|
+
process.platform === 'win32' ? 'junction' : 'dir'
|
|
32
|
+
);
|
|
33
|
+
} catch {
|
|
34
|
+
await fse.remove(target);
|
|
35
|
+
await fse.copy(source, target);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return target;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveElectronCompatibilityRoot(projectRoot) {
|
|
42
|
+
const sibling = path.resolve(__dirname, '..', '..', 'electron-compat');
|
|
43
|
+
if (isAtomCompatPackage(sibling)) return sibling;
|
|
44
|
+
|
|
45
|
+
for (const searchRoot of [projectRoot, process.cwd()]) {
|
|
46
|
+
try {
|
|
47
|
+
const packagePath = require.resolve('electron/package.json', { paths: [searchRoot] });
|
|
48
|
+
const root = path.dirname(packagePath);
|
|
49
|
+
if (isAtomCompatPackage(root)) return root;
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new Error(
|
|
54
|
+
'AtomJS could not locate its Electron compatibility package. ' +
|
|
55
|
+
'Reinstall the AtomJS distribution or add the compatibility package to the workspace.'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isAtomCompatPackage(directory) {
|
|
60
|
+
const pkg = readJson(path.join(directory, 'package.json'));
|
|
61
|
+
return Boolean(
|
|
62
|
+
pkg &&
|
|
63
|
+
['electron', '@atom-js-org/electron'].includes(pkg.name) &&
|
|
64
|
+
pkg.atomjsElectronCompatibility === true
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readJson(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
ensureElectronCompatibility,
|
|
78
|
+
resolveElectronCompatibilityRoot
|
|
79
|
+
};
|
package/src/index.cjs
ADDED
package/src/init.cjs
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const fse = require('fs-extra');
|
|
6
|
+
|
|
7
|
+
async function initCommand(directory, options = {}) {
|
|
8
|
+
const root = path.resolve(directory);
|
|
9
|
+
await fse.ensureDir(root);
|
|
10
|
+
const existing = await fs.promises.readdir(root);
|
|
11
|
+
if (existing.length > 0 && !existing.every((name) => name === '.git')) {
|
|
12
|
+
throw new Error(`Directory is not empty: ${root}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const packageName = sanitizePackageName(options.name || path.basename(root));
|
|
16
|
+
const productName = humanize(packageName);
|
|
17
|
+
|
|
18
|
+
await fse.ensureDir(path.join(root, 'src'));
|
|
19
|
+
await fse.ensureDir(path.join(root, '.github', 'workflows'));
|
|
20
|
+
|
|
21
|
+
await fs.promises.writeFile(path.join(root, 'package.json'), JSON.stringify({
|
|
22
|
+
name: packageName,
|
|
23
|
+
version: '0.1.0',
|
|
24
|
+
private: true,
|
|
25
|
+
main: 'src/main.js',
|
|
26
|
+
scripts: {
|
|
27
|
+
dev: 'atom run dev',
|
|
28
|
+
build: 'atom build all',
|
|
29
|
+
'build:current': `atom build ${hostTarget()}`,
|
|
30
|
+
start: 'atom run build'
|
|
31
|
+
},
|
|
32
|
+
dependencies: {
|
|
33
|
+
'@atom-js-org/runtime': '0.2.0-alpha.0',
|
|
34
|
+
electron: 'npm:@atom-js-org/electron@0.2.0-alpha.0'
|
|
35
|
+
},
|
|
36
|
+
optionalDependencies: {
|
|
37
|
+
'webview-nodejs': '0.5.0'
|
|
38
|
+
},
|
|
39
|
+
devDependencies: {
|
|
40
|
+
'@atom-js-org/cli': '0.2.0-alpha.0'
|
|
41
|
+
},
|
|
42
|
+
overrides: {
|
|
43
|
+
tar: '7.5.20'
|
|
44
|
+
}
|
|
45
|
+
}, null, 2));
|
|
46
|
+
|
|
47
|
+
await fs.promises.writeFile(path.join(root, 'atom.config.json'), JSON.stringify({
|
|
48
|
+
appId: `com.example.${packageName.replace(/[^a-z0-9]+/g, '')}`,
|
|
49
|
+
productName,
|
|
50
|
+
main: 'src/main.js',
|
|
51
|
+
icon: 'assets/icon.png',
|
|
52
|
+
installerCredit: true,
|
|
53
|
+
github: {
|
|
54
|
+
workflow: 'atom-build.yml'
|
|
55
|
+
}
|
|
56
|
+
}, null, 2));
|
|
57
|
+
|
|
58
|
+
await fse.ensureDir(path.join(root, 'assets'));
|
|
59
|
+
await fs.promises.writeFile(path.join(root, 'src', 'main.js'), mainTemplate());
|
|
60
|
+
await fs.promises.writeFile(path.join(root, 'src', 'preload.js'), preloadTemplate());
|
|
61
|
+
await fs.promises.writeFile(path.join(root, 'src', 'index.html'), htmlTemplate(productName));
|
|
62
|
+
await fs.promises.writeFile(path.join(root, 'src', 'renderer.js'), rendererTemplate());
|
|
63
|
+
await fs.promises.writeFile(path.join(root, '.gitignore'), 'node_modules/\nbuild/\n.atom/\n');
|
|
64
|
+
await fs.promises.writeFile(path.join(root, '.github', 'workflows', 'atom-build.yml'), workflowTemplate());
|
|
65
|
+
|
|
66
|
+
console.log(`Created AtomJS project in ${root}`);
|
|
67
|
+
console.log('Next: npm install && npm run dev');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function mainTemplate() {
|
|
71
|
+
return `'use strict';
|
|
72
|
+
|
|
73
|
+
const path = require('node:path');
|
|
74
|
+
const fs = require('node:fs/promises');
|
|
75
|
+
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
|
|
76
|
+
|
|
77
|
+
function createWindow() {
|
|
78
|
+
const win = new BrowserWindow({
|
|
79
|
+
width: 1000,
|
|
80
|
+
height: 700,
|
|
81
|
+
title: app.getName(),
|
|
82
|
+
webPreferences: {
|
|
83
|
+
preload: path.join(__dirname, 'preload.js')
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
win.loadFile(path.join(__dirname, 'index.html'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ipcMain.handle('file:open', async () => {
|
|
91
|
+
const result = await dialog.showOpenDialog({ properties: ['openFile'] });
|
|
92
|
+
if (result.canceled) return null;
|
|
93
|
+
return {
|
|
94
|
+
path: result.filePaths[0],
|
|
95
|
+
content: await fs.readFile(result.filePaths[0], 'utf8')
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
app.whenReady().then(createWindow);
|
|
100
|
+
app.on('window-all-closed', () => app.quit());
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function preloadTemplate() {
|
|
105
|
+
return `'use strict';
|
|
106
|
+
|
|
107
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
108
|
+
|
|
109
|
+
contextBridge.exposeInMainWorld('atom', {
|
|
110
|
+
openFile: () => ipcRenderer.invoke('file:open'),
|
|
111
|
+
onMessage: (listener) => ipcRenderer.on('app:message', (_event, message) => listener(message))
|
|
112
|
+
});
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function htmlTemplate(productName) {
|
|
117
|
+
return `<!doctype html>
|
|
118
|
+
<html lang="en">
|
|
119
|
+
<head>
|
|
120
|
+
<meta charset="UTF-8">
|
|
121
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
122
|
+
<title>${escapeHtml(productName)}</title>
|
|
123
|
+
<style>
|
|
124
|
+
:root { color-scheme: dark; font-family: Inter, system-ui, sans-serif; }
|
|
125
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #202124; color: #f5f7fb; }
|
|
126
|
+
main { width: min(720px, calc(100% - 48px)); }
|
|
127
|
+
h1 { font-size: clamp(2.5rem, 8vw, 5rem); margin: 0 0 12px; }
|
|
128
|
+
p { color: #aeb8c8; font-size: 1.1rem; }
|
|
129
|
+
button { border: 0; border-radius: 10px; padding: 12px 18px; font: inherit; cursor: pointer; background: #7ddff2; color: #14202a; font-weight: 700; }
|
|
130
|
+
pre { min-height: 160px; overflow: auto; padding: 18px; border-radius: 12px; background: #17181a; white-space: pre-wrap; }
|
|
131
|
+
</style>
|
|
132
|
+
</head>
|
|
133
|
+
<body>
|
|
134
|
+
<main>
|
|
135
|
+
<h1>${escapeHtml(productName)}</h1>
|
|
136
|
+
<p>Fast, lightweight desktop UI powered by the system WebView.</p>
|
|
137
|
+
<button id="open">Open a text file</button>
|
|
138
|
+
<pre id="output">Ready.</pre>
|
|
139
|
+
</main>
|
|
140
|
+
<script src="renderer.js"></script>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function rendererTemplate() {
|
|
147
|
+
return `document.querySelector('#open').addEventListener('click', async () => {
|
|
148
|
+
const result = await window.atom.openFile();
|
|
149
|
+
document.querySelector('#output').textContent = result ? result.content : 'Cancelled.';
|
|
150
|
+
});
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function workflowTemplate() {
|
|
155
|
+
return `name: AtomJS Build
|
|
156
|
+
|
|
157
|
+
on:
|
|
158
|
+
workflow_dispatch:
|
|
159
|
+
inputs:
|
|
160
|
+
target:
|
|
161
|
+
description: windows, macos, linux, or all
|
|
162
|
+
required: true
|
|
163
|
+
default: all
|
|
164
|
+
type: choice
|
|
165
|
+
options: [all, windows, macos, linux]
|
|
166
|
+
project:
|
|
167
|
+
description: Project directory inside the repository
|
|
168
|
+
required: true
|
|
169
|
+
default: .
|
|
170
|
+
|
|
171
|
+
jobs:
|
|
172
|
+
windows:
|
|
173
|
+
if: inputs.target == 'all' || inputs.target == 'windows'
|
|
174
|
+
runs-on: windows-latest
|
|
175
|
+
steps:
|
|
176
|
+
- uses: actions/checkout@v4
|
|
177
|
+
- uses: actions/setup-node@v4
|
|
178
|
+
with:
|
|
179
|
+
node-version: 22
|
|
180
|
+
cache: npm
|
|
181
|
+
- run: choco install nsis -y
|
|
182
|
+
- run: npm ci
|
|
183
|
+
working-directory: \${{ inputs.project }}
|
|
184
|
+
- run: npx atom build windows --local
|
|
185
|
+
working-directory: \${{ inputs.project }}
|
|
186
|
+
- uses: actions/upload-artifact@v4
|
|
187
|
+
with:
|
|
188
|
+
name: atom-build-windows
|
|
189
|
+
path: \${{ inputs.project }}/build/windows
|
|
190
|
+
if-no-files-found: error
|
|
191
|
+
|
|
192
|
+
macos:
|
|
193
|
+
if: inputs.target == 'all' || inputs.target == 'macos'
|
|
194
|
+
runs-on: macos-latest
|
|
195
|
+
steps:
|
|
196
|
+
- uses: actions/checkout@v4
|
|
197
|
+
- uses: actions/setup-node@v4
|
|
198
|
+
with:
|
|
199
|
+
node-version: 22
|
|
200
|
+
cache: npm
|
|
201
|
+
- run: npm ci
|
|
202
|
+
working-directory: \${{ inputs.project }}
|
|
203
|
+
- run: npx atom build macos --local
|
|
204
|
+
working-directory: \${{ inputs.project }}
|
|
205
|
+
- uses: actions/upload-artifact@v4
|
|
206
|
+
with:
|
|
207
|
+
name: atom-build-macos
|
|
208
|
+
path: \${{ inputs.project }}/build/macos
|
|
209
|
+
if-no-files-found: error
|
|
210
|
+
|
|
211
|
+
linux:
|
|
212
|
+
if: inputs.target == 'all' || inputs.target == 'linux'
|
|
213
|
+
runs-on: ubuntu-latest
|
|
214
|
+
steps:
|
|
215
|
+
- uses: actions/checkout@v4
|
|
216
|
+
- run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev zenity
|
|
217
|
+
- uses: actions/setup-node@v4
|
|
218
|
+
with:
|
|
219
|
+
node-version: 22
|
|
220
|
+
cache: npm
|
|
221
|
+
- run: |
|
|
222
|
+
sudo curl -L -o /usr/local/bin/appimagetool https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
|
223
|
+
sudo chmod +x /usr/local/bin/appimagetool
|
|
224
|
+
- run: npm ci
|
|
225
|
+
working-directory: \${{ inputs.project }}
|
|
226
|
+
- run: npx atom build linux --local
|
|
227
|
+
working-directory: \${{ inputs.project }}
|
|
228
|
+
env:
|
|
229
|
+
APPIMAGE_EXTRACT_AND_RUN: '1'
|
|
230
|
+
- uses: actions/upload-artifact@v4
|
|
231
|
+
with:
|
|
232
|
+
name: atom-build-linux
|
|
233
|
+
path: \${{ inputs.project }}/build/linux
|
|
234
|
+
if-no-files-found: error
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function sanitizePackageName(value) {
|
|
239
|
+
return String(value).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'atomjs-app';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function humanize(value) {
|
|
243
|
+
return String(value).split(/[-_.]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(' ');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hostTarget() {
|
|
247
|
+
return process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function escapeHtml(value) {
|
|
251
|
+
return String(value).replace(/[&<>\"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[char]);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = { initCommand };
|
package/src/run.cjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
const { loadProject, hostTarget } = require('./utils.cjs');
|
|
7
|
+
const { ensureElectronCompatibility } = require('./electron-compat.cjs');
|
|
8
|
+
|
|
9
|
+
async function runCommand(mode, options = {}) {
|
|
10
|
+
const normalized = String(mode).toLowerCase();
|
|
11
|
+
if (normalized === 'dev') return runDev(options);
|
|
12
|
+
if (normalized === 'build') return runBuild(options);
|
|
13
|
+
throw new Error(`Unknown run mode '${mode}'. Use dev or build.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function runDev(options) {
|
|
17
|
+
const project = loadProject(options.project);
|
|
18
|
+
const mainPath = path.resolve(project.root, project.config.main);
|
|
19
|
+
if (!fs.existsSync(mainPath)) throw new Error(`Main file not found: ${mainPath}`);
|
|
20
|
+
|
|
21
|
+
console.log(`AtomJS dev: ${project.config.productName}`);
|
|
22
|
+
console.log(`Main: ${path.relative(project.root, mainPath)}`);
|
|
23
|
+
|
|
24
|
+
await ensureElectronCompatibility(project.root);
|
|
25
|
+
|
|
26
|
+
const child = spawn(process.execPath, [mainPath], {
|
|
27
|
+
cwd: project.root,
|
|
28
|
+
env: {
|
|
29
|
+
...process.env,
|
|
30
|
+
ATOM_PROJECT_ROOT: project.root,
|
|
31
|
+
ATOM_DEV: '1'
|
|
32
|
+
},
|
|
33
|
+
stdio: 'inherit'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await new Promise((resolve, reject) => {
|
|
37
|
+
child.once('error', reject);
|
|
38
|
+
child.once('exit', (code, signal) => {
|
|
39
|
+
if (code === 0 || signal === 'SIGTERM' || signal === 'SIGINT') resolve();
|
|
40
|
+
else reject(new Error(`Application exited with code ${code}`));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runBuild(options) {
|
|
46
|
+
const project = loadProject(options.project);
|
|
47
|
+
const target = hostTarget();
|
|
48
|
+
const manifestPath = path.join(project.root, 'build', target, 'manifest.json');
|
|
49
|
+
if (!fs.existsSync(manifestPath)) {
|
|
50
|
+
throw new Error(`No ${target} build found. Run 'atom build ${target}' first.`);
|
|
51
|
+
}
|
|
52
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
53
|
+
const executable = path.resolve(project.root, 'build', target, manifest.run);
|
|
54
|
+
if (!fs.existsSync(executable)) throw new Error(`Built executable not found: ${executable}`);
|
|
55
|
+
|
|
56
|
+
console.log(`Running ${path.basename(executable)}`);
|
|
57
|
+
const child = spawn(executable, [], {
|
|
58
|
+
cwd: path.dirname(executable),
|
|
59
|
+
stdio: 'inherit'
|
|
60
|
+
});
|
|
61
|
+
await new Promise((resolve, reject) => {
|
|
62
|
+
child.once('error', reject);
|
|
63
|
+
child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`Build exited with code ${code}`)));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { runCommand };
|
package/src/utils.cjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const TARGETS = new Set(['windows', 'macos', 'linux', 'all']);
|
|
8
|
+
|
|
9
|
+
function hostTarget() {
|
|
10
|
+
if (process.platform === 'win32') return 'windows';
|
|
11
|
+
if (process.platform === 'darwin') return 'macos';
|
|
12
|
+
return 'linux';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveProject(input) {
|
|
16
|
+
let current = path.resolve(input || process.cwd());
|
|
17
|
+
if (fs.existsSync(current) && fs.statSync(current).isFile()) current = path.dirname(current);
|
|
18
|
+
|
|
19
|
+
while (true) {
|
|
20
|
+
if (fs.existsSync(path.join(current, 'atom.config.json')) || fs.existsSync(path.join(current, 'package.json'))) {
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
const parent = path.dirname(current);
|
|
24
|
+
if (parent === current) break;
|
|
25
|
+
current = parent;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Could not find an AtomJS project from ${input || process.cwd()}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJson(filePath) {
|
|
31
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadProject(projectInput) {
|
|
35
|
+
const root = resolveProject(projectInput);
|
|
36
|
+
const packagePath = path.join(root, 'package.json');
|
|
37
|
+
if (!fs.existsSync(packagePath)) throw new Error(`Missing package.json in ${root}`);
|
|
38
|
+
const packageJson = readJson(packagePath);
|
|
39
|
+
const configPath = path.join(root, 'atom.config.json');
|
|
40
|
+
const config = fs.existsSync(configPath) ? readJson(configPath) : {};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
root,
|
|
44
|
+
packageJson,
|
|
45
|
+
config: {
|
|
46
|
+
appId: config.appId || `com.atomjs.${sanitizeId(packageJson.name || 'app')}`,
|
|
47
|
+
productName: config.productName || packageJson.productName || packageJson.name || 'AtomJS App',
|
|
48
|
+
main: config.main || packageJson.main || 'main.js',
|
|
49
|
+
icon: config.icon || null,
|
|
50
|
+
files: config.files || ['**/*'],
|
|
51
|
+
installerCredit: config.installerCredit !== false,
|
|
52
|
+
github: config.github || {}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function run(command, args, options = {}) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const child = spawn(command, args, {
|
|
60
|
+
cwd: options.cwd || process.cwd(),
|
|
61
|
+
env: options.env || process.env,
|
|
62
|
+
stdio: options.stdio || 'inherit',
|
|
63
|
+
shell: false
|
|
64
|
+
});
|
|
65
|
+
child.once('error', reject);
|
|
66
|
+
child.once('exit', (code, signal) => {
|
|
67
|
+
if (code === 0) resolve({ code, signal });
|
|
68
|
+
else reject(new Error(`${command} exited with code ${code}${signal ? ` (${signal})` : ''}`));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function capture(command, args, options = {}) {
|
|
74
|
+
const result = spawnSync(command, args, {
|
|
75
|
+
cwd: options.cwd || process.cwd(),
|
|
76
|
+
env: options.env || process.env,
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
shell: false
|
|
79
|
+
});
|
|
80
|
+
if (result.error) throw result.error;
|
|
81
|
+
if (result.status !== 0) {
|
|
82
|
+
const message = (result.stderr || result.stdout || '').trim();
|
|
83
|
+
throw new Error(message || `${command} exited with code ${result.status}`);
|
|
84
|
+
}
|
|
85
|
+
return (result.stdout || '').trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function commandExists(command, args = ['--version']) {
|
|
89
|
+
const result = spawnSync(command, args, { stdio: 'ignore', shell: false });
|
|
90
|
+
return !result.error && result.status === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateTarget(target) {
|
|
94
|
+
const normalized = String(target).toLowerCase();
|
|
95
|
+
if (!TARGETS.has(normalized)) {
|
|
96
|
+
throw new Error(`Unknown build target '${target}'. Use windows, macos, linux, or all.`);
|
|
97
|
+
}
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sanitizeId(value) {
|
|
102
|
+
return String(value).toLowerCase().replace(/[^a-z0-9.-]+/g, '-').replace(/^-+|-+$/g, '') || 'app';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sanitizeFilename(value) {
|
|
106
|
+
return String(value).replace(/[<>:"/\\|?*\x00-\x1F]/g, '-').replace(/\s+/g, ' ').trim() || 'AtomJS App';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
TARGETS,
|
|
111
|
+
hostTarget,
|
|
112
|
+
resolveProject,
|
|
113
|
+
readJson,
|
|
114
|
+
loadProject,
|
|
115
|
+
run,
|
|
116
|
+
capture,
|
|
117
|
+
commandExists,
|
|
118
|
+
validateTarget,
|
|
119
|
+
sanitizeId,
|
|
120
|
+
sanitizeFilename
|
|
121
|
+
};
|