@lakutata/builder 3.1.1 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/README.zh-CN.md +5 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/lib/Archive.js +3 -2
- package/dist/esm/lib/Bundle.js +12 -5
- package/dist/esm/lib/Directories.js +18 -0
- package/dist/esm/lib/Executable.js +27 -6
- package/dist/esm/lib/Files.js +44 -5
- package/dist/esm/lib/Installer.js +26 -12
- package/dist/esm/lib/Native.js +58 -4
- package/dist/esm/lib/NativeRuntime.js +2 -0
- package/dist/esm/lib/linux/Linux.js +57 -8
- package/dist/esm/lib/mac/Mac.js +39 -11
- package/dist/esm/lib/nwjs/Addons.js +307 -0
- package/dist/esm/lib/nwjs/Package.js +149 -0
- package/dist/esm/lib/nwjs/Runtime.js +214 -0
- package/dist/esm/lib/nwjs/Stage.js +92 -0
- package/dist/esm/lib/win/Windows.js +63 -41
- package/dist/types/index.d.ts +4 -0
- package/dist/types/lib/Bundle.d.ts +5 -0
- package/dist/types/lib/Directories.d.ts +8 -0
- package/dist/types/lib/Executable.d.ts +7 -0
- package/dist/types/lib/Files.d.ts +11 -1
- package/dist/types/lib/Installer.d.ts +33 -4
- package/dist/types/lib/Native.d.ts +23 -1
- package/dist/types/lib/mac/Mac.d.ts +1 -1
- package/dist/types/lib/nwjs/Addons.d.ts +51 -0
- package/dist/types/lib/nwjs/Package.d.ts +52 -0
- package/dist/types/lib/nwjs/Runtime.d.ts +116 -0
- package/dist/types/lib/nwjs/Stage.d.ts +104 -0
- package/dist/types/lib/win/Windows.d.ts +28 -0
- package/package.json +2 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { cp, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { programName } from '../Bundle.js';
|
|
6
|
+
import { applicationIcon, icnsFile, icoFile, setExecutableIcon } from '../Icons.js';
|
|
7
|
+
import { runCommand } from '../Processes.js';
|
|
8
|
+
import { downloadNwjs } from './Runtime.js';
|
|
9
|
+
import { stageNwjsApp } from './Stage.js';
|
|
10
|
+
/**
|
|
11
|
+
* Run a command which must succeed
|
|
12
|
+
* @param command
|
|
13
|
+
* @param args
|
|
14
|
+
* @param cwd
|
|
15
|
+
* @param what
|
|
16
|
+
*/
|
|
17
|
+
async function must(command, args, cwd, what) {
|
|
18
|
+
const result = await runCommand(command, args, cwd);
|
|
19
|
+
if (result.code !== 0)
|
|
20
|
+
throw new Error(`${what}: ${result.output.trim()}`);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Give the files of an application to every user: readable, its directories and its programs runnable. The archives of
|
|
24
|
+
* the older releases of NW.js (0.72) give their files to their owner only: installed by root (/opt, /Applications), the
|
|
25
|
+
* application would run for root only. The files of Windows have no modes
|
|
26
|
+
* @param directory
|
|
27
|
+
*/
|
|
28
|
+
async function shareFiles(directory) {
|
|
29
|
+
if (process.platform !== 'win32')
|
|
30
|
+
await must('chmod', ['-R', 'a+rX', directory], directory, 'The files of the application cannot be given to every user');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The four numbers of a version of Windows (1.2.3 → 1.2.3.0)
|
|
34
|
+
* @param version
|
|
35
|
+
*/
|
|
36
|
+
function windowsVersion(version) {
|
|
37
|
+
const parts = version.split(/[.+-]/).slice(0, 3).map((part) => Math.min(parseInt(part, 10) || 0, 65535));
|
|
38
|
+
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0, 0];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Give the executable of NW.js of Windows the identity of the application: its icon, and the names and the version of its
|
|
42
|
+
* version information (the ones the task manager and the properties of the file show)
|
|
43
|
+
* @param file
|
|
44
|
+
* @param identity
|
|
45
|
+
*/
|
|
46
|
+
async function windowsIdentity(file, identity) {
|
|
47
|
+
if (identity.ico)
|
|
48
|
+
await setExecutableIcon(file, identity.ico);
|
|
49
|
+
const { NtExecutable, NtExecutableResource, Resource } = await import('resedit');
|
|
50
|
+
const executable = NtExecutable.from(await readFile(file), { ignoreCert: true });
|
|
51
|
+
const resource = NtExecutableResource.from(executable);
|
|
52
|
+
const information = Resource.VersionInfo.fromEntries(resource.entries)[0] ?? Resource.VersionInfo.createEmpty();
|
|
53
|
+
const numbers = windowsVersion(identity.version);
|
|
54
|
+
information.setFileVersion(...numbers);
|
|
55
|
+
information.setProductVersion(...numbers);
|
|
56
|
+
const languages = information.getAllLanguagesForStringValues();
|
|
57
|
+
for (const language of languages.length ? languages : [{ lang: 1033, codepage: 1200 }]) {
|
|
58
|
+
information.setStringValues(language, {
|
|
59
|
+
FileDescription: identity.productName,
|
|
60
|
+
ProductName: identity.productName,
|
|
61
|
+
CompanyName: identity.company,
|
|
62
|
+
InternalName: identity.program,
|
|
63
|
+
OriginalFilename: `${identity.program}.exe`,
|
|
64
|
+
FileVersion: identity.version,
|
|
65
|
+
ProductVersion: identity.version
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
information.outputToResourceEntries(resource.entries);
|
|
69
|
+
resource.outputResource(executable);
|
|
70
|
+
await writeFile(file, Buffer.from(executable.generate()));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Make the application of NW.js of a project for this system: the runtime of NW.js (normal) with the application
|
|
74
|
+
* (stageNwjsApp: its bundle, its native addons made to load in NW.js, its resources, its manifest), named after it.
|
|
75
|
+
* On Windows and Linux, the files of the application are next to its executable (nw.exe, nw renamed after its program:
|
|
76
|
+
* the name of its installers, lakutata.installer.name, else the program of its package; on Windows, its icon and its
|
|
77
|
+
* version information are the ones of the application); on macOS, they are in the bundle
|
|
78
|
+
* (<product>.app/Contents/Resources/app.nw), whose Info.plist gives its name, its identifier (the appId of the
|
|
79
|
+
* installer), its version and its icon, signed again (ad hoc). The icon is lakutata.icon. Its files are readable by
|
|
80
|
+
* every user, its programs runnable (once installed by root, the application runs for the users)
|
|
81
|
+
* @param options
|
|
82
|
+
* @example
|
|
83
|
+
* import {buildNwjsApp} from '@lakutata/builder'
|
|
84
|
+
*
|
|
85
|
+
* //dist/nwjs/desk-osx-arm64/Desk.app on a Mac with Apple silicon
|
|
86
|
+
* const app = await buildNwjsApp({})
|
|
87
|
+
* console.log(app.directory, app.runtime.version)
|
|
88
|
+
*/
|
|
89
|
+
export async function buildNwjsApp(options) {
|
|
90
|
+
const project = path.resolve(options.project ?? process.cwd());
|
|
91
|
+
const manifest = JSON.parse(await readFile(path.join(project, 'package.json'), 'utf8'));
|
|
92
|
+
//The program is named after the package of its installers, else after the program of the package (the package of an
|
|
93
|
+
//application of a workspace is often named app)
|
|
94
|
+
const program = String(manifest.lakutata?.installer?.name ?? programName(manifest));
|
|
95
|
+
const productName = String(manifest.productName ?? manifest.lakutata?.installer?.productName ?? program);
|
|
96
|
+
const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-nwjs-app-'));
|
|
97
|
+
try {
|
|
98
|
+
const stage = await stageNwjsApp({ ...options, project: project, outdir: path.join(work, 'app'), minify: options.minify ?? true });
|
|
99
|
+
const runtime = await downloadNwjs({ version: stage.release.version, flavor: 'normal', platform: options.platform, arch: options.arch });
|
|
100
|
+
const directory = path.join(path.resolve(project, options.outdir ?? path.join('dist', 'nwjs')), `${program}-${runtime.platform}`);
|
|
101
|
+
await rm(directory, { recursive: true, force: true });
|
|
102
|
+
await mkdir(directory, { recursive: true });
|
|
103
|
+
const icon = applicationIcon(project, manifest);
|
|
104
|
+
const version = String(manifest.version ?? '0.0.0');
|
|
105
|
+
if (runtime.platform.startsWith('osx-')) {
|
|
106
|
+
//ditto keeps the links of the frameworks and the signatures of the helpers
|
|
107
|
+
const bundle = path.join(directory, `${productName}.app`);
|
|
108
|
+
await must('ditto', [path.join(runtime.directory, 'nwjs.app'), bundle], directory, 'The bundle of macOS cannot be copied');
|
|
109
|
+
await must('ditto', [stage.directory, path.join(bundle, 'Contents', 'Resources', 'app.nw')], directory, 'The application cannot be copied into the bundle');
|
|
110
|
+
const plist = path.join(bundle, 'Contents', 'Info.plist');
|
|
111
|
+
const appId = String(manifest.lakutata?.installer?.appId ?? `com.${program}.app`);
|
|
112
|
+
for (const [key, value] of Object.entries({ CFBundleName: productName, CFBundleDisplayName: productName, CFBundleIdentifier: appId, CFBundleShortVersionString: version, CFBundleVersion: version })) {
|
|
113
|
+
await must('plutil', ['-replace', key, '-string', value, plist], directory, `${key} cannot be written into the Info.plist`);
|
|
114
|
+
}
|
|
115
|
+
if (icon)
|
|
116
|
+
await writeFile(path.join(bundle, 'Contents', 'Resources', 'app.icns'), icnsFile(icon.image));
|
|
117
|
+
await shareFiles(bundle);
|
|
118
|
+
//Its files changed, the bundle is signed again (ad hoc: the signature of a developer is given by its installer)
|
|
119
|
+
await must('codesign', ['--force', '--deep', '--sign', '-', bundle], directory, 'The bundle cannot be signed');
|
|
120
|
+
return { directory: bundle, executable: path.join(bundle, 'Contents', 'MacOS', 'nwjs'), program: program, productName: productName, runtime: runtime, stage: stage };
|
|
121
|
+
}
|
|
122
|
+
//The links of the runtime of Linux kept as they are (cp -a); the runtime of Windows has none
|
|
123
|
+
for (const source of [runtime.directory, stage.directory]) {
|
|
124
|
+
if (process.platform === 'win32')
|
|
125
|
+
await cp(source, directory, { recursive: true });
|
|
126
|
+
else
|
|
127
|
+
await must('cp', ['-a', `${source}/.`, directory], directory, 'The application cannot be copied');
|
|
128
|
+
}
|
|
129
|
+
const windows = runtime.platform.startsWith('win-');
|
|
130
|
+
const executable = path.join(directory, windows ? `${program}.exe` : program);
|
|
131
|
+
await rename(path.join(directory, windows ? 'nw.exe' : 'nw'), executable);
|
|
132
|
+
if (windows) {
|
|
133
|
+
await windowsIdentity(executable, {
|
|
134
|
+
productName: productName,
|
|
135
|
+
program: program,
|
|
136
|
+
version: version,
|
|
137
|
+
company: String(manifest.lakutata?.installer?.vendor ?? (typeof manifest.author === 'string' ? manifest.author : manifest.author?.name) ?? productName),
|
|
138
|
+
ico: icon && icoFile(icon.image)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (!existsSync(executable))
|
|
142
|
+
throw new Error(`The executable of NW.js ${runtime.version} (${runtime.platform}) is missing`);
|
|
143
|
+
await shareFiles(directory);
|
|
144
|
+
return { directory: directory, executable: executable, program: program, productName: productName, runtime: runtime, stage: stage };
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
await rm(work, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, mkdtemp, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { runCommand } from '../Processes.js';
|
|
6
|
+
import { cacheDirectory } from '../Tools.js';
|
|
7
|
+
/**
|
|
8
|
+
* The oldest Node.js lakutata runs on: the releases of NW.js embedding an older one are refused
|
|
9
|
+
*/
|
|
10
|
+
const OLDEST_NODE = '18.10.0';
|
|
11
|
+
/**
|
|
12
|
+
* The aliases of the versions
|
|
13
|
+
*/
|
|
14
|
+
const ALIASES = { win7: '0.72.0' };
|
|
15
|
+
/**
|
|
16
|
+
* The releases are read again after a day
|
|
17
|
+
*/
|
|
18
|
+
const RELEASES_TTL = 24 * 60 * 60 * 1000;
|
|
19
|
+
/**
|
|
20
|
+
* Where the releases are downloaded from: LAKUTATA_NWJS_MIRROR (a mirror of dl.nwjs.io), else dl.nwjs.io
|
|
21
|
+
*/
|
|
22
|
+
function mirror() {
|
|
23
|
+
return (process.env.LAKUTATA_NWJS_MIRROR ?? 'https://dl.nwjs.io').replace(/\/+$/, '');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compare two versions (1.2.3)
|
|
27
|
+
* @param left
|
|
28
|
+
* @param right
|
|
29
|
+
*/
|
|
30
|
+
export function compareVersions(left, right) {
|
|
31
|
+
const parts = (version) => version.replace(/^v/, '').split('.').map((part) => parseInt(part, 10) || 0);
|
|
32
|
+
const [a, b] = [parts(left), parts(right)];
|
|
33
|
+
for (let index = 0; index < Math.max(a.length, b.length); index++) {
|
|
34
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
35
|
+
if (difference)
|
|
36
|
+
return difference;
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The releases of NW.js, newest first: the list of nwjs.io (LAKUTATA_NWJS_VERSIONS), kept a day in the cache; the
|
|
42
|
+
* cached one when it cannot be read
|
|
43
|
+
* @example
|
|
44
|
+
* import {nwjsReleases} from '@lakutata/builder'
|
|
45
|
+
*
|
|
46
|
+
* for (const release of (await nwjsReleases()).slice(0, 3)) console.log(release.version, release.node, release.chromium)
|
|
47
|
+
*/
|
|
48
|
+
export async function nwjsReleases() {
|
|
49
|
+
const cache = path.join(cacheDirectory(), 'nwjs', 'versions.json');
|
|
50
|
+
const fresh = existsSync(cache) && Date.now() - (await stat(cache)).mtimeMs < RELEASES_TTL;
|
|
51
|
+
let list;
|
|
52
|
+
if (!fresh) {
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(process.env.LAKUTATA_NWJS_VERSIONS ?? 'https://nwjs.io/versions.json');
|
|
55
|
+
if (!response.ok)
|
|
56
|
+
throw new Error(`${response.status}`);
|
|
57
|
+
list = await response.json();
|
|
58
|
+
await mkdir(path.dirname(cache), { recursive: true });
|
|
59
|
+
await writeFile(cache, JSON.stringify(list));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!existsSync(cache))
|
|
63
|
+
throw new Error(`The releases of NW.js cannot be read (${error.message}): LAKUTATA_NWJS_VERSIONS gives another list`, { cause: error });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
list ??= JSON.parse(await readFile(cache, 'utf8'));
|
|
67
|
+
const versions = list.versions ?? [];
|
|
68
|
+
return versions.map((entry) => ({
|
|
69
|
+
version: String(entry.version).replace(/^v/, ''),
|
|
70
|
+
date: String(entry.date ?? ''),
|
|
71
|
+
node: String(entry.components?.node ?? ''),
|
|
72
|
+
chromium: String(entry.components?.chromium ?? ''),
|
|
73
|
+
platforms: (entry.files ?? []).map((file) => String(file).replace(/-symbol$/, '')).filter((file) => /^(win|osx|linux)-/.test(file))
|
|
74
|
+
})).sort((a, b) => compareVersions(b.version, a.version));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A release of NW.js: a version, latest (the latest release) or win7 (0.72.0, the last one running on Windows 7, 8
|
|
78
|
+
* and 8.1). Refused when it embeds a Node.js older than the one lakutata runs on (18.10)
|
|
79
|
+
* @param version
|
|
80
|
+
* @example
|
|
81
|
+
* import {nwjsRelease} from '@lakutata/builder'
|
|
82
|
+
*
|
|
83
|
+
* const release = await nwjsRelease('win7')
|
|
84
|
+
* console.log(release.version, release.node, release.chromium) //0.72.0 19.3.0 109.0.5414.87
|
|
85
|
+
*/
|
|
86
|
+
export async function nwjsRelease(version) {
|
|
87
|
+
const releases = await nwjsReleases();
|
|
88
|
+
const wanted = ALIASES[version] ?? version.replace(/^v/, '');
|
|
89
|
+
const release = wanted === 'latest' ? releases[0] : releases.find((item) => item.version === wanted);
|
|
90
|
+
if (!release)
|
|
91
|
+
throw new Error(`NW.js ${version} is not a release of NW.js (the latest: ${releases[0]?.version})`);
|
|
92
|
+
if (compareVersions(release.node, OLDEST_NODE) < 0)
|
|
93
|
+
throw new Error(`NW.js ${release.version} embeds Node.js ${release.node}: lakutata runs on Node.js ${OLDEST_NODE} or later (NW.js 0.70.0 or later)`);
|
|
94
|
+
return release;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The system and the architecture of a release as NW.js names them (osx-arm64), the x64 build on a Mac with Apple
|
|
98
|
+
* silicon when the release has no arm64 one
|
|
99
|
+
* @param release
|
|
100
|
+
* @param platform
|
|
101
|
+
* @param arch
|
|
102
|
+
*/
|
|
103
|
+
export function nwjsPlatform(release, platform = process.platform, arch = process.arch) {
|
|
104
|
+
const system = { win32: 'win', darwin: 'osx', linux: 'linux' }[platform];
|
|
105
|
+
if (!system)
|
|
106
|
+
throw new Error(`NW.js has no build for ${platform}`);
|
|
107
|
+
const name = `${system}-${arch}`;
|
|
108
|
+
if (release.platforms.includes(name))
|
|
109
|
+
return name;
|
|
110
|
+
if (system === 'osx' && arch === 'arm64' && release.platforms.includes('osx-x64'))
|
|
111
|
+
return 'osx-x64';
|
|
112
|
+
throw new Error(`NW.js ${release.version} has no build for ${name} (${release.platforms.join(', ')})`);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The sha256 of the files of a release, from its SHASUMS256.txt
|
|
116
|
+
* @param version
|
|
117
|
+
*/
|
|
118
|
+
async function checksums(version) {
|
|
119
|
+
const url = `${mirror()}/v${version}/SHASUMS256.txt`;
|
|
120
|
+
const response = await fetch(url);
|
|
121
|
+
if (!response.ok)
|
|
122
|
+
throw new Error(`The checksums of NW.js ${version} cannot be read (${url}: ${response.status})`);
|
|
123
|
+
const sums = new Map();
|
|
124
|
+
for (const line of (await response.text()).split('\n')) {
|
|
125
|
+
const match = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/);
|
|
126
|
+
if (match)
|
|
127
|
+
sums.set(match[2], match[1]);
|
|
128
|
+
}
|
|
129
|
+
return sums;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Download a file, its sha256 computed while it is written
|
|
133
|
+
* @param url
|
|
134
|
+
* @param file
|
|
135
|
+
*/
|
|
136
|
+
async function download(url, file) {
|
|
137
|
+
const response = await fetch(url);
|
|
138
|
+
if (!response.ok || !response.body)
|
|
139
|
+
throw new Error(`${url} cannot be downloaded (${response.status})`);
|
|
140
|
+
const hash = createHash('sha256');
|
|
141
|
+
const handle = await open(file, 'w');
|
|
142
|
+
try {
|
|
143
|
+
for await (const chunk of response.body) {
|
|
144
|
+
hash.update(chunk);
|
|
145
|
+
await handle.write(chunk);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await handle.close();
|
|
150
|
+
}
|
|
151
|
+
return hash.digest('hex');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The executable of a runtime in its directory
|
|
155
|
+
* @param platform
|
|
156
|
+
*/
|
|
157
|
+
function executableOf(platform) {
|
|
158
|
+
if (platform.startsWith('win-'))
|
|
159
|
+
return 'nw.exe';
|
|
160
|
+
if (platform.startsWith('osx-'))
|
|
161
|
+
return path.join('nwjs.app', 'Contents', 'MacOS', 'nwjs');
|
|
162
|
+
return 'nw';
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A runtime of NW.js for a system, downloaded once into the cache (~/.cache/lakutata/nwjs, LAKUTATA_CACHE_DIR) from
|
|
166
|
+
* dl.nwjs.io (LAKUTATA_NWJS_MIRROR: a mirror of it) and checked with the SHASUMS256.txt of its release
|
|
167
|
+
* @param options
|
|
168
|
+
* @example
|
|
169
|
+
* import {downloadNwjs} from '@lakutata/builder'
|
|
170
|
+
*
|
|
171
|
+
* //The NW.js of the development (its developer tools), for the machine
|
|
172
|
+
* const runtime = await downloadNwjs({version: 'latest', flavor: 'sdk'})
|
|
173
|
+
* console.log(runtime.executable, runtime.node)
|
|
174
|
+
*/
|
|
175
|
+
export async function downloadNwjs(options) {
|
|
176
|
+
const release = await nwjsRelease(options.version);
|
|
177
|
+
const flavor = options.flavor ?? 'normal';
|
|
178
|
+
const platform = nwjsPlatform(release, options.platform, options.arch);
|
|
179
|
+
const name = `nwjs${flavor === 'sdk' ? '-sdk' : ''}-v${release.version}-${platform}`;
|
|
180
|
+
const archive = `${name}.${platform.startsWith('linux-') ? 'tar.gz' : 'zip'}`;
|
|
181
|
+
const target = path.join(cacheDirectory(), 'nwjs', name);
|
|
182
|
+
const runtime = { ...release, flavor: flavor, platform: platform, directory: target, executable: path.join(target, executableOf(platform)) };
|
|
183
|
+
if (existsSync(runtime.executable))
|
|
184
|
+
return runtime;
|
|
185
|
+
const sum = (await checksums(release.version)).get(archive);
|
|
186
|
+
if (!sum)
|
|
187
|
+
throw new Error(`NW.js ${release.version} has no ${archive} in its checksums`);
|
|
188
|
+
//Extracted aside, then renamed: the builds running together do not see it partly written
|
|
189
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
190
|
+
const temporary = await mkdtemp(`${target}.`);
|
|
191
|
+
try {
|
|
192
|
+
const file = path.join(temporary, archive);
|
|
193
|
+
const downloaded = await download(`${mirror()}/v${release.version}/${archive}`, file);
|
|
194
|
+
if (downloaded !== sum)
|
|
195
|
+
throw new Error(`The ${archive} downloaded is not the one of NW.js ${release.version} (sha256 ${downloaded}, ${sum} expected)`);
|
|
196
|
+
//ditto keeps the links of the frameworks of macOS; the tar of Windows reads the zip archives
|
|
197
|
+
const extracted = process.platform === 'darwin' && archive.endsWith('.zip')
|
|
198
|
+
? await runCommand('ditto', ['-x', '-k', archive, '.'], temporary)
|
|
199
|
+
: await runCommand('tar', ['-xf', archive], temporary);
|
|
200
|
+
if (extracted.code !== 0)
|
|
201
|
+
throw new Error(`${archive} cannot be extracted: ${extracted.output.trim()}`);
|
|
202
|
+
if (!existsSync(path.join(temporary, name, executableOf(platform))))
|
|
203
|
+
throw new Error(`${archive} has no ${name}/${executableOf(platform).split(path.sep).join('/')}`);
|
|
204
|
+
await rename(path.join(temporary, name), target).catch((error) => {
|
|
205
|
+
//Extracted by another build
|
|
206
|
+
if (!existsSync(runtime.executable))
|
|
207
|
+
throw error;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
await rm(temporary, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
return runtime;
|
|
214
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { bundleProject } from '../Bundle.js';
|
|
5
|
+
import { nwjsPlatform, nwjsRelease } from './Runtime.js';
|
|
6
|
+
import { prepareNwjsAddons } from './Addons.js';
|
|
7
|
+
/**
|
|
8
|
+
* The background script of the application in NW.js: its Node.js context, where the application runs. Its timers are
|
|
9
|
+
* the ones of Node.js: the older releases (0.72) give the background page the timers of the browser, whose ids are
|
|
10
|
+
* numbers (without unref and refresh)
|
|
11
|
+
*/
|
|
12
|
+
const BACKGROUND_SCRIPT = `//The background script of the application in NW.js (its Node.js context): the timers of Node.js, then the application, bundled
|
|
13
|
+
const timers = require('timers')
|
|
14
|
+
for (const name of ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'setImmediate', 'clearImmediate']) globalThis[name] = timers[name]
|
|
15
|
+
require('./app.cjs')
|
|
16
|
+
`;
|
|
17
|
+
/**
|
|
18
|
+
* The NW.js of an application, from the "lakutata.nwjs" of its package.json
|
|
19
|
+
* @param manifest
|
|
20
|
+
*/
|
|
21
|
+
export function nwjsConfig(manifest) {
|
|
22
|
+
const config = manifest.lakutata?.nwjs;
|
|
23
|
+
if (!config || typeof config !== 'object' || typeof config.version !== 'string')
|
|
24
|
+
throw new Error('The application has no version of NW.js: give it with lakutata nwjs use <version> (latest, win7, 0.119.2…), which writes "lakutata": {"nwjs": {"version": …}} into its package.json');
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The name of an application in NW.js: lower case, the characters of the names of the packages
|
|
29
|
+
* @param manifest
|
|
30
|
+
* @param config
|
|
31
|
+
*/
|
|
32
|
+
function nwjsName(manifest, config) {
|
|
33
|
+
const name = String(config.name ?? manifest.lakutata?.installer?.appId ?? manifest.name ?? 'app');
|
|
34
|
+
return name.toLowerCase().replace(/^@/, '').replace(/[^a-z0-9._-]+/g, '-');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Make the application of NW.js of a project: its bundle (CommonJS, for the Node.js of the release), its native tree
|
|
38
|
+
* and its resources (lakutata.resources), the manifest of NW.js (its name, its background script, the partition of the
|
|
39
|
+
* webviews holding its user interface) and the background script running the bundle
|
|
40
|
+
* @param options
|
|
41
|
+
* @example
|
|
42
|
+
* import {stageNwjsApp} from '@lakutata/builder'
|
|
43
|
+
*
|
|
44
|
+
* //The application of the development, run by: nw .nwjs/app
|
|
45
|
+
* const stage = await stageNwjsApp({outdir: '.nwjs/app'})
|
|
46
|
+
* console.log(stage.directory, stage.release.version, stage.name)
|
|
47
|
+
*/
|
|
48
|
+
export async function stageNwjsApp(options) {
|
|
49
|
+
const project = path.resolve(options.project ?? process.cwd());
|
|
50
|
+
const manifestFile = path.join(project, 'package.json');
|
|
51
|
+
if (!existsSync(manifestFile))
|
|
52
|
+
throw new Error(`${project} has no package.json`);
|
|
53
|
+
const manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
|
|
54
|
+
const config = options.version ? { ...manifest.lakutata?.nwjs, version: options.version } : nwjsConfig(manifest);
|
|
55
|
+
const release = await nwjsRelease(config.version);
|
|
56
|
+
//nwjs.json, what the user interface is built for (its Chromium), follows the version of the application
|
|
57
|
+
const derived = `${JSON.stringify({ version: release.version, node: release.node, chromium: release.chromium }, null, 2)}\n`;
|
|
58
|
+
const derivedFile = path.join(project, 'nwjs.json');
|
|
59
|
+
if (!existsSync(derivedFile) || await readFile(derivedFile, 'utf8') !== derived)
|
|
60
|
+
await writeFile(derivedFile, derived);
|
|
61
|
+
const directory = path.resolve(project, options.outdir);
|
|
62
|
+
//The directory is made again: never the project or a directory holding it
|
|
63
|
+
if (!path.relative(directory, project).startsWith('..'))
|
|
64
|
+
throw new Error(`The application of NW.js is made in a directory of its own, not ${directory}`);
|
|
65
|
+
await rm(directory, { recursive: true, force: true });
|
|
66
|
+
await mkdir(directory, { recursive: true });
|
|
67
|
+
const bundle = await bundleProject({
|
|
68
|
+
project: project,
|
|
69
|
+
entry: options.entry,
|
|
70
|
+
outfile: path.join(directory, 'app.cjs'),
|
|
71
|
+
node: release.node,
|
|
72
|
+
minify: options.minify ?? false,
|
|
73
|
+
external: options.external,
|
|
74
|
+
directories: { native: 'app.native', resources: 'resources' }
|
|
75
|
+
});
|
|
76
|
+
//Its addons loaded by the runtime: its architecture, its headers, its library of Node.js on Windows
|
|
77
|
+
const platform = nwjsPlatform(release, options.platform, options.arch);
|
|
78
|
+
const addons = bundle.native && bundle.nativeDirectory ? await prepareNwjsAddons(project, bundle.nativeDirectory, bundle.native.files, release, platform) : { renamed: [], rebuilt: [] };
|
|
79
|
+
const name = nwjsName(manifest, config);
|
|
80
|
+
const nwManifest = {
|
|
81
|
+
name: name,
|
|
82
|
+
productName: manifest.productName ?? manifest.name,
|
|
83
|
+
version: manifest.version ?? '0.0.0',
|
|
84
|
+
main: 'main.js',
|
|
85
|
+
//The pages of the user interface, held by the webviews of the windows (without Node.js), are the files of the application
|
|
86
|
+
webview: { partitions: [{ name: 'trusted', accessible_resources: ['<all_urls>'] }] },
|
|
87
|
+
...(config.chromiumArgs ? { 'chromium-args': config.chromiumArgs } : {})
|
|
88
|
+
};
|
|
89
|
+
await writeFile(path.join(directory, 'package.json'), `${JSON.stringify(nwManifest, null, 2)}\n`);
|
|
90
|
+
await writeFile(path.join(directory, 'main.js'), BACKGROUND_SCRIPT);
|
|
91
|
+
return { directory: directory, release: release, name: name, platform: platform, bundle: bundle, addons: addons };
|
|
92
|
+
}
|