@lakutata/builder 3.1.3 → 3.1.6
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 +24 -1
- package/README.zh-CN.md +7 -1
- package/dist/esm/index.js +5 -0
- package/dist/esm/lib/Archive.js +3 -2
- package/dist/esm/lib/Bundle.js +2 -1
- package/dist/esm/lib/Directories.js +18 -0
- package/dist/esm/lib/Executable.js +27 -6
- package/dist/esm/lib/Installer.js +28 -14
- package/dist/esm/lib/NativeRuntime.js +27 -3
- package/dist/esm/lib/Processes.js +3 -2
- package/dist/esm/lib/Tools.js +98 -19
- package/dist/esm/lib/linux/Linux.js +60 -10
- package/dist/esm/lib/mac/Mac.js +39 -11
- package/dist/esm/lib/mobile/Addons.js +675 -0
- package/dist/esm/lib/mobile/Node.js +190 -0
- package/dist/esm/lib/mobile/Stage.js +180 -0
- package/dist/esm/lib/nwjs/Addons.js +310 -0
- package/dist/esm/lib/nwjs/Package.js +149 -0
- package/dist/esm/lib/nwjs/Runtime.js +188 -0
- package/dist/esm/lib/nwjs/Stage.js +92 -0
- package/dist/esm/lib/win/Windows.js +68 -45
- package/dist/types/index.d.ts +7 -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/Installer.d.ts +37 -4
- package/dist/types/lib/Processes.d.ts +2 -1
- package/dist/types/lib/Tools.d.ts +47 -1
- package/dist/types/lib/linux/Linux.d.ts +2 -1
- package/dist/types/lib/mac/Mac.d.ts +1 -1
- package/dist/types/lib/mobile/Addons.d.ts +154 -0
- package/dist/types/lib/mobile/Node.d.ts +89 -0
- package/dist/types/lib/mobile/Stage.d.ts +111 -0
- package/dist/types/lib/nwjs/Addons.d.ts +69 -0
- package/dist/types/lib/nwjs/Package.d.ts +56 -0
- package/dist/types/lib/nwjs/Runtime.d.ts +120 -0
- package/dist/types/lib/nwjs/Stage.d.ts +104 -0
- package/dist/types/lib/win/Windows.d.ts +30 -1
- package/package.json +2 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { runCommand } from '../Processes.js';
|
|
5
|
+
import { cacheDirectory, downloadFile, moveIntoCache } from '../Tools.js';
|
|
6
|
+
/**
|
|
7
|
+
* The version of Node.js of the mobile applications when their libnode does not tell it: the one of the release of
|
|
8
|
+
* lakutata/libnode they download (MOBILE_NODE_RELEASE)
|
|
9
|
+
*/
|
|
10
|
+
export const MOBILE_NODE_VERSION = '24.20.0';
|
|
11
|
+
/**
|
|
12
|
+
* The architectures of Android of a libnode built for them (the arch of Node.js names the native tree of the
|
|
13
|
+
* architecture, the target names the compilers of the NDK)
|
|
14
|
+
*/
|
|
15
|
+
export const ANDROID_ABIS = {
|
|
16
|
+
'arm64-v8a': { arch: 'arm64', target: 'aarch64-linux-android' },
|
|
17
|
+
'armeabi-v7a': { arch: 'arm', target: 'armv7a-linux-androideabi' },
|
|
18
|
+
'x86_64': { arch: 'x64', target: 'x86_64-linux-android' }
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The version of Node.js of a libnode: the one of its headers (include/node/node_version.h)
|
|
22
|
+
* @param directory the export of the libnode
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export function mobileNodeVersion(directory) {
|
|
26
|
+
const header = readFileSync(path.join(directory, 'include', 'node', 'node_version.h'), 'utf8');
|
|
27
|
+
const part = (name) => new RegExp(`#define NODE_${name}_VERSION (\\d+)`).exec(header)?.[1];
|
|
28
|
+
const [major, minor, patch] = [part('MAJOR'), part('MINOR'), part('PATCH')];
|
|
29
|
+
if (!major || !minor || !patch)
|
|
30
|
+
throw new Error(`${directory} holds no version of Node.js (include/node/node_version.h)`);
|
|
31
|
+
return `${major}.${minor}.${patch}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The architectures of Android a libnode is built for (libs/<abi>/libnode.so)
|
|
35
|
+
* @param directory the export of the libnode of Android
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export function mobileNodeAbis(directory) {
|
|
39
|
+
return Object.keys(ANDROID_ABIS).filter((abi) => existsSync(path.join(directory, 'libs', abi, 'libnode.so')));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check the export of a libnode of a system: its headers, and its libraries (Android: libs/<abi>/libnode.so, HarmonyOS:
|
|
43
|
+
* libs/arm64-v8a/libnode.so) or its framework (iOS: NodeMobile.xcframework)
|
|
44
|
+
* @param platform
|
|
45
|
+
* @param directory
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function checkMobileNode(platform, directory) {
|
|
49
|
+
const missing = (what) => new Error(`${directory} is not the libnode of ${platform} (${what} missing): download it (lakutata mobile node ${platform})`);
|
|
50
|
+
if (!existsSync(path.join(directory, 'include', 'node', 'node_api.h')))
|
|
51
|
+
throw missing('include/node/node_api.h');
|
|
52
|
+
if (platform === 'ios' && !existsSync(path.join(directory, 'NodeMobile.xcframework', 'Info.plist')))
|
|
53
|
+
throw missing('NodeMobile.xcframework');
|
|
54
|
+
if (platform === 'android' && !mobileNodeAbis(directory).length)
|
|
55
|
+
throw missing('libs/<abi>/libnode.so');
|
|
56
|
+
if (platform === 'harmony' && !existsSync(path.join(directory, 'libs', 'arm64-v8a', 'libnode.so')))
|
|
57
|
+
throw missing('libs/arm64-v8a/libnode.so');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The release of lakutata/libnode the mobile applications download their libnode from: its tag is v<release> (the
|
|
61
|
+
* version of Node.js, then -<revision> for a release of the same version built again)
|
|
62
|
+
*/
|
|
63
|
+
export const MOBILE_NODE_RELEASE = '24.20.0';
|
|
64
|
+
/**
|
|
65
|
+
* The architectures of HarmonyOS the mobile applications ship (the libs/arm64-v8a of their HAP)
|
|
66
|
+
*/
|
|
67
|
+
const HARMONY_ABIS = { 'arm64-v8a': { arch: 'arm64' } };
|
|
68
|
+
/**
|
|
69
|
+
* Where the releases of lakutata/libnode are downloaded from: LAKUTATA_LIBNODE_MIRROR (a mirror of them, its files laid
|
|
70
|
+
* out as the ones of GitHub: v<release>/<archive>), else the releases of the repository on GitHub
|
|
71
|
+
*/
|
|
72
|
+
function libnodeMirror() {
|
|
73
|
+
return (process.env.LAKUTATA_LIBNODE_MIRROR ?? 'https://github.com/lakutata/libnode/releases/download').replace(/\/+$/, '');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* An archive of the release, downloaded once into the cache (~/.cache/lakutata/mobile/libnode, LAKUTATA_CACHE_DIR),
|
|
77
|
+
* checked with the SHA256SUMS of the release and extracted
|
|
78
|
+
* @param name the archive without its extension: libnode_<platform>_<arch>
|
|
79
|
+
* @param sums the sha256 of the archives of the release, by their names
|
|
80
|
+
* @param progress
|
|
81
|
+
* @returns the directory extracted: lib/libnode.so or NodeMobile.xcframework, include/node, LICENSE, libnode.json
|
|
82
|
+
*/
|
|
83
|
+
async function libnodeArchive(name, sums, progress) {
|
|
84
|
+
const directory = path.join(cacheDirectory(), 'mobile', 'libnode', MOBILE_NODE_RELEASE, name);
|
|
85
|
+
if (existsSync(path.join(directory, '.complete')))
|
|
86
|
+
return directory;
|
|
87
|
+
const archive = `${name}.tar.gz`;
|
|
88
|
+
const url = `${libnodeMirror()}/v${MOBILE_NODE_RELEASE}/${archive}`;
|
|
89
|
+
const sum = (await sums()).get(archive);
|
|
90
|
+
if (!sum)
|
|
91
|
+
throw new Error(`The release ${MOBILE_NODE_RELEASE} of libnode has no ${archive} in its SHA256SUMS`);
|
|
92
|
+
progress?.(`${archive} (libnode ${MOBILE_NODE_RELEASE})`);
|
|
93
|
+
//Extracted aside, then moved into the cache: the builds running together do not see it partly written
|
|
94
|
+
await mkdir(path.dirname(directory), { recursive: true });
|
|
95
|
+
const temporary = await mkdtemp(`${directory}.`);
|
|
96
|
+
try {
|
|
97
|
+
const file = path.join(temporary, archive);
|
|
98
|
+
const downloaded = await downloadFile(url, file, { progress: progress }).catch((error) => {
|
|
99
|
+
throw new Error(`${error.message}: a mirror of the releases of lakutata/libnode is given by LAKUTATA_LIBNODE_MIRROR, a libnode of your own by LAKUTATA_MOBILE_NODE_DIR`);
|
|
100
|
+
});
|
|
101
|
+
if (downloaded !== sum)
|
|
102
|
+
throw new Error(`The ${archive} downloaded is not the one of the release ${MOBILE_NODE_RELEASE} of libnode (sha256 ${downloaded}, ${sum} expected)`);
|
|
103
|
+
const extracted = await runCommand('tar', ['-xf', archive], temporary);
|
|
104
|
+
if (extracted.code !== 0)
|
|
105
|
+
throw new Error(`${archive} cannot be extracted: ${extracted.output.trim()}`);
|
|
106
|
+
await rm(file);
|
|
107
|
+
const root = path.join(temporary, name);
|
|
108
|
+
if (!existsSync(path.join(root, 'include', 'node', 'node_api.h')))
|
|
109
|
+
throw new Error(`${archive} has no ${name}/include/node`);
|
|
110
|
+
await writeFile(path.join(root, '.complete'), '');
|
|
111
|
+
await moveIntoCache(root, directory, () => existsSync(path.join(directory, '.complete')));
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await rm(temporary, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
return directory;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Download the libnode of a mobile system from the releases of lakutata/libnode (LAKUTATA_LIBNODE_MIRROR: a mirror of
|
|
120
|
+
* them), once: its archives are checked with the SHA256SUMS of the release and kept in the cache
|
|
121
|
+
* (~/.cache/lakutata/mobile, LAKUTATA_CACHE_DIR). LAKUTATA_MOBILE_NODE_DIR gives exports of a libnode of your own instead
|
|
122
|
+
* (a directory per system: android, ios, harmony; built with the repository of lakutata/libnode)
|
|
123
|
+
* @param options
|
|
124
|
+
* @returns the directory of the export: its headers (include/node) and its libraries (libs/<abi>/libnode.so) or its
|
|
125
|
+
* framework (NodeMobile.xcframework)
|
|
126
|
+
* @example
|
|
127
|
+
* import {downloadMobileNode} from '@lakutata/builder'
|
|
128
|
+
*
|
|
129
|
+
* const directory = await downloadMobileNode({platform: 'android', abis: ['arm64-v8a'], progress: console.log})
|
|
130
|
+
* console.log(directory)
|
|
131
|
+
*/
|
|
132
|
+
export async function downloadMobileNode(options) {
|
|
133
|
+
const given = process.env.LAKUTATA_MOBILE_NODE_DIR;
|
|
134
|
+
if (given) {
|
|
135
|
+
const directory = path.resolve(given, options.platform);
|
|
136
|
+
checkMobileNode(options.platform, directory);
|
|
137
|
+
return directory;
|
|
138
|
+
}
|
|
139
|
+
//The archives by architecture: the ABI of their library in the export
|
|
140
|
+
const archives = options.platform === 'ios'
|
|
141
|
+
? [['libnode_ios_arm64', undefined]]
|
|
142
|
+
: options.platform === 'harmony'
|
|
143
|
+
? Object.entries(HARMONY_ABIS).map(([abi, { arch }]) => [`libnode_harmony_${arch}`, abi])
|
|
144
|
+
: [...new Set(options.abis ?? ['arm64-v8a', 'x86_64'])].map((abi) => {
|
|
145
|
+
if (!ANDROID_ABIS[abi])
|
|
146
|
+
throw new Error(`${abi} is not an architecture of Android (${Object.keys(ANDROID_ABIS).join(', ')})`);
|
|
147
|
+
return [`libnode_android_${ANDROID_ABIS[abi].arch}`, abi];
|
|
148
|
+
});
|
|
149
|
+
const target = path.join(cacheDirectory(), 'mobile', 'node', MOBILE_NODE_RELEASE, [options.platform, ...archives.map(([, abi]) => abi ?? '')].filter(Boolean).join('-'));
|
|
150
|
+
if (existsSync(path.join(target, '.complete')))
|
|
151
|
+
return target;
|
|
152
|
+
//The checksums of the release, read once and only when an archive is downloaded
|
|
153
|
+
let sums;
|
|
154
|
+
const checksums = () => sums ??= (async () => {
|
|
155
|
+
const response = await fetch(`${libnodeMirror()}/v${MOBILE_NODE_RELEASE}/SHA256SUMS`).catch((error) => {
|
|
156
|
+
throw new Error(`The checksums of the release ${MOBILE_NODE_RELEASE} of libnode cannot be downloaded (${error.message}): a mirror of the releases of lakutata/libnode is given by LAKUTATA_LIBNODE_MIRROR`);
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok)
|
|
159
|
+
throw new Error(`The checksums of the release ${MOBILE_NODE_RELEASE} of libnode cannot be downloaded (${response.url}: ${response.status})`);
|
|
160
|
+
return new Map((await response.text()).split('\n').map((line) => /^([0-9a-f]{64})\s+\*?(\S+)$/.exec(line.trim())).filter((match) => !!match).map((match) => [match[2], match[1]]));
|
|
161
|
+
})();
|
|
162
|
+
const extracted = [];
|
|
163
|
+
for (const [name, abi] of archives)
|
|
164
|
+
extracted.push([await libnodeArchive(name, checksums, options.progress), abi]);
|
|
165
|
+
//The export: the headers and the description of the first archive, the library of each architecture
|
|
166
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
167
|
+
const temporary = await mkdtemp(`${target}.`);
|
|
168
|
+
try {
|
|
169
|
+
const [first] = extracted[0];
|
|
170
|
+
await cp(path.join(first, 'include'), path.join(temporary, 'include'), { recursive: true });
|
|
171
|
+
for (const file of ['LICENSE', 'libnode.json'])
|
|
172
|
+
if (existsSync(path.join(first, file)))
|
|
173
|
+
await cp(path.join(first, file), path.join(temporary, file));
|
|
174
|
+
for (const [directory, abi] of extracted) {
|
|
175
|
+
if (abi) {
|
|
176
|
+
await mkdir(path.join(temporary, 'libs', abi), { recursive: true });
|
|
177
|
+
await cp(path.join(directory, 'lib', 'libnode.so'), path.join(temporary, 'libs', abi, 'libnode.so'));
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
await cp(path.join(directory, 'NodeMobile.xcframework'), path.join(temporary, 'NodeMobile.xcframework'), { recursive: true, verbatimSymlinks: true });
|
|
181
|
+
}
|
|
182
|
+
checkMobileNode(options.platform, temporary);
|
|
183
|
+
await writeFile(path.join(temporary, '.complete'), '');
|
|
184
|
+
await moveIntoCache(temporary, target, () => existsSync(path.join(target, '.complete')));
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await rm(temporary, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
return target;
|
|
190
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { lstat, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { bundleProject } from '../Bundle.js';
|
|
5
|
+
import { packNativeTree, writeNativeTree } from '../Native.js';
|
|
6
|
+
import { BUN_NATIVE_ASSETS } from '../NativeRuntime.js';
|
|
7
|
+
import { MOBILE_ADDONS_FILE, prepareAndroidAddons, prepareHarmonyAddons, prepareIosAddons } from './Addons.js';
|
|
8
|
+
import { checkMobileNode, MOBILE_NODE_VERSION, mobileNodeVersion } from './Node.js';
|
|
9
|
+
/**
|
|
10
|
+
* The package of a file of the native tree: the last one of the node_modules in its path
|
|
11
|
+
* @param file
|
|
12
|
+
*/
|
|
13
|
+
function packageOf(file) {
|
|
14
|
+
const parts = file.split('/');
|
|
15
|
+
const index = parts.lastIndexOf('node_modules');
|
|
16
|
+
if (index < 0)
|
|
17
|
+
return file;
|
|
18
|
+
return parts.slice(index + 1, parts[index + 1]?.startsWith('@') ? index + 3 : index + 2).join('/');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The files of the embedded native tree of HarmonyOS, next to the entry (names without a leading dot: the resources of
|
|
22
|
+
* the HAP leave those out)
|
|
23
|
+
*/
|
|
24
|
+
const NATIVE_DATA = 'native.gz';
|
|
25
|
+
const NATIVE_INDEX = 'native.json';
|
|
26
|
+
/**
|
|
27
|
+
* The entry of the Node.js project: the fetch() of Node.js put away without WebAssembly (V8 runs --jitless on
|
|
28
|
+
* HarmonyOS: its first call would throw an uncaught exception). The native tree embedded in the project (HarmonyOS) is
|
|
29
|
+
* given to the bundle, which extracts it at its first run into the directory LAKUTATA_NATIVE_DIR gives (the native
|
|
30
|
+
* module sets it). The addons of iOS and HarmonyOS are loaded from where the system keeps them (MOBILE_ADDONS_FILE: the
|
|
31
|
+
* frameworks of the application, the libraries of the HAP), in place of their placeholders in the tree. Then the
|
|
32
|
+
* application, bundled
|
|
33
|
+
*/
|
|
34
|
+
const ENTRY_SCRIPT = `//The entry of the application in the Node.js of the mobile application: fetch() put away without WebAssembly, the native tree embedded (HarmonyOS) given to the bundle, the native addons of iOS and HarmonyOS loaded from where the system keeps them, then the application, bundled
|
|
35
|
+
const fs = require('fs')
|
|
36
|
+
const path = require('path')
|
|
37
|
+
if (typeof WebAssembly === 'undefined') delete globalThis.fetch
|
|
38
|
+
const tree = path.join(__dirname, '${NATIVE_INDEX}')
|
|
39
|
+
if (fs.existsSync(tree)) globalThis[Symbol.for('${BUN_NATIVE_ASSETS}')] = {data: path.join(__dirname, '${NATIVE_DATA}'), index: tree}
|
|
40
|
+
const addons = path.join(__dirname, '${MOBILE_ADDONS_FILE}')
|
|
41
|
+
if (fs.existsSync(addons)) {
|
|
42
|
+
//The placeholders are keyed by their paths in the tree, wherever the tree is (next to the bundle, extracted)
|
|
43
|
+
const {directory, files} = JSON.parse(fs.readFileSync(addons, 'utf8'))
|
|
44
|
+
const base = path.resolve(fs.realpathSync(__dirname), directory)
|
|
45
|
+
const keys = Object.keys(files)
|
|
46
|
+
const dlopen = process.dlopen
|
|
47
|
+
process.dlopen = function (module, filename, ...rest) {
|
|
48
|
+
let real
|
|
49
|
+
try {
|
|
50
|
+
real = fs.realpathSync(filename).split(path.sep).join('/')
|
|
51
|
+
} catch {
|
|
52
|
+
real = undefined
|
|
53
|
+
}
|
|
54
|
+
const key = real && keys.find((key) => real.endsWith('/' + key))
|
|
55
|
+
return dlopen.call(this, module, key ? path.join(base, files[key]) : filename, ...rest)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
require('./app.cjs')
|
|
59
|
+
`;
|
|
60
|
+
/**
|
|
61
|
+
* The files of a native tree written in a directory, its links left out (they stay the links of the tree)
|
|
62
|
+
* @param directory
|
|
63
|
+
* @param base
|
|
64
|
+
*/
|
|
65
|
+
async function treeFiles(directory, base = directory) {
|
|
66
|
+
const files = [];
|
|
67
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
68
|
+
const file = path.join(directory, entry.name);
|
|
69
|
+
if (entry.isSymbolicLink())
|
|
70
|
+
continue;
|
|
71
|
+
if (entry.isDirectory())
|
|
72
|
+
files.push(...await treeFiles(file, base));
|
|
73
|
+
else if (entry.isFile())
|
|
74
|
+
files.push({ path: path.relative(base, file).split(path.sep).join('/'), source: file, mode: (await lstat(file)).mode & 0o777 });
|
|
75
|
+
}
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Make the Node.js project of a mobile application of React Native (Android, iOS, HarmonyOS), which the native module
|
|
80
|
+
* of the application runs in its libnode: the bundle of the application (CommonJS, for the version of Node.js of the
|
|
81
|
+
* libnode) and its resources (lakutata.resources), its manifest and its entry. The bundle holds the dependencies: the
|
|
82
|
+
* project is packed into the application as it is. The native addons are built for the system from the sources of their
|
|
83
|
+
* packages, against the headers and the libraries of the libnode: on Android, a native tree per architecture (node-gyp
|
|
84
|
+
* and the NDK of Android); on iOS, a framework per addon for each SDK (node-gyp and Xcode), in the directory of the
|
|
85
|
+
* frameworks the project of iOS embeds; on HarmonyOS, a library per addon (node-gyp and the native SDK of HarmonyOS),
|
|
86
|
+
* in the libraries of the HAP
|
|
87
|
+
* @param options
|
|
88
|
+
* @example
|
|
89
|
+
* import {stageMobileApp} from '@lakutata/builder'
|
|
90
|
+
*
|
|
91
|
+
* //In packages/app of a project of the mobile-react template, before the application of Android is built
|
|
92
|
+
* const stage = await stageMobileApp({
|
|
93
|
+
* platform: 'android',
|
|
94
|
+
* outdir: '../../android/app/src/main/assets/nodejs',
|
|
95
|
+
* node: '../ipc/native/node/out/android'
|
|
96
|
+
* })
|
|
97
|
+
* console.log(stage.directory, stage.node)
|
|
98
|
+
*/
|
|
99
|
+
export async function stageMobileApp(options) {
|
|
100
|
+
const project = path.resolve(options.project ?? process.cwd());
|
|
101
|
+
const manifestFile = path.join(project, 'package.json');
|
|
102
|
+
if (!existsSync(manifestFile))
|
|
103
|
+
throw new Error(`${project} has no package.json`);
|
|
104
|
+
const manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
|
|
105
|
+
const directory = path.resolve(project, options.outdir);
|
|
106
|
+
//The directory is made again: never the project or a directory holding it
|
|
107
|
+
if (!path.relative(directory, project).startsWith('..'))
|
|
108
|
+
throw new Error(`The Node.js project of the mobile application is made in a directory of its own, not ${directory}`);
|
|
109
|
+
const node = options.node ? path.resolve(project, options.node) : undefined;
|
|
110
|
+
if (node)
|
|
111
|
+
checkMobileNode(options.platform, node);
|
|
112
|
+
const version = node ? mobileNodeVersion(node) : MOBILE_NODE_VERSION;
|
|
113
|
+
await rm(directory, { recursive: true, force: true });
|
|
114
|
+
await mkdir(directory, { recursive: true });
|
|
115
|
+
//The frameworks of the addons of iOS made before for the SDKs built: made again, or none (the ones of the other SDK
|
|
116
|
+
//kept: the development builds for the simulators only); the libraries of the addons of HarmonyOS made before
|
|
117
|
+
const frameworks = options.platform === 'ios' && options.frameworks ? path.resolve(project, options.frameworks) : undefined;
|
|
118
|
+
if (frameworks)
|
|
119
|
+
for (const sdk of options.sdks?.length ? options.sdks : ['iphoneos', 'iphonesimulator'])
|
|
120
|
+
await rm(path.join(frameworks, sdk), { recursive: true, force: true });
|
|
121
|
+
const libraries = options.platform === 'harmony' && options.libraries ? path.resolve(project, options.libraries) : undefined;
|
|
122
|
+
if (libraries && existsSync(libraries)) {
|
|
123
|
+
for (const name of await readdir(libraries))
|
|
124
|
+
if (/^libaddon-.*\.so$/.test(name))
|
|
125
|
+
await rm(path.join(libraries, name), { force: true });
|
|
126
|
+
}
|
|
127
|
+
//The resources of the HAP of HarmonyOS leave out the files whose names start with a dot (the .lakutata of the native
|
|
128
|
+
//tree): there the tree is embedded, packed into the project, extracted at run time
|
|
129
|
+
const harmony = options.platform === 'harmony';
|
|
130
|
+
const bundle = await bundleProject({
|
|
131
|
+
project: project,
|
|
132
|
+
entry: options.entry,
|
|
133
|
+
outfile: path.join(directory, 'app.cjs'),
|
|
134
|
+
node: version,
|
|
135
|
+
minify: options.minify ?? true,
|
|
136
|
+
external: ['@lakutata/netif', ...options.external ?? []],
|
|
137
|
+
native: harmony ? 'embedded' : 'directory',
|
|
138
|
+
directories: { native: 'app.native', resources: 'resources' }
|
|
139
|
+
});
|
|
140
|
+
//The tree its addons are built in: next to the bundle, or written there to be packed (HarmonyOS)
|
|
141
|
+
const tree = harmony && bundle.native ? path.join(directory, 'app.native') : bundle.nativeDirectory;
|
|
142
|
+
if (harmony && bundle.native)
|
|
143
|
+
await writeNativeTree(bundle.native, tree);
|
|
144
|
+
//The native tree holds the scripts of the workers too: its addons only are built
|
|
145
|
+
const files = bundle.native?.files ?? [];
|
|
146
|
+
const addonFiles = files.map((file) => file.path).filter((file) => file.endsWith('.node'));
|
|
147
|
+
let addons;
|
|
148
|
+
if (addonFiles.length) {
|
|
149
|
+
const packages = [...new Set(addonFiles.map(packageOf))].join(', ');
|
|
150
|
+
try {
|
|
151
|
+
if (!node)
|
|
152
|
+
throw new Error(`The native addons of the application are built against the libnode of ${options.platform}: give the directory of its export to the stage (node; ${packages})`);
|
|
153
|
+
if (options.platform === 'ios' && !frameworks)
|
|
154
|
+
throw new Error(`The native addons of the application are built for iOS into the frameworks the project of iOS embeds: give their directory (frameworks) to the stage (${packages})`);
|
|
155
|
+
if (options.platform === 'harmony' && !libraries)
|
|
156
|
+
throw new Error(`The native addons of the application are built for HarmonyOS into the libraries of the HAP: give their directory (libraries) to the stage (${packages})`);
|
|
157
|
+
if (options.platform === 'ios')
|
|
158
|
+
addons = await prepareIosAddons(project, directory, tree, files, frameworks, node, { sdks: options.sdks, progress: options.progress });
|
|
159
|
+
else if (options.platform === 'harmony')
|
|
160
|
+
addons = await prepareHarmonyAddons(project, directory, tree, files, libraries, node, { progress: options.progress });
|
|
161
|
+
else
|
|
162
|
+
addons = await prepareAndroidAddons(project, tree, files, node, { abis: options.abis, progress: options.progress });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
await rm(directory, { recursive: true, force: true });
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (harmony && bundle.native) {
|
|
170
|
+
//Packed as the tree of an executable of Bun: the bundle extracts it as it is (its hash names the directory)
|
|
171
|
+
const links = bundle.native.links;
|
|
172
|
+
const packed = packNativeTree({ ...bundle.native, files: await treeFiles(tree), links: links });
|
|
173
|
+
await writeFile(path.join(directory, NATIVE_DATA), packed.data);
|
|
174
|
+
await writeFile(path.join(directory, NATIVE_INDEX), JSON.stringify(packed.index));
|
|
175
|
+
await rm(tree, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
await writeFile(path.join(directory, 'package.json'), `${JSON.stringify({ name: manifest.name ?? 'app', version: manifest.version ?? '0.0.0', private: true, main: 'main.js' }, null, 2)}\n`);
|
|
178
|
+
await writeFile(path.join(directory, 'main.js'), ENTRY_SCRIPT);
|
|
179
|
+
return { directory: directory, platform: options.platform, node: version, bundle: bundle, addons: addons };
|
|
180
|
+
}
|