@lakutata/cli 3.1.4 → 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.
@@ -0,0 +1,677 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { spawn } from 'node:child_process';
11
+ import { existsSync, readdirSync, readFileSync, statSync, watch } from 'node:fs';
12
+ import { cp, mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { Controller } from 'lakutata/com/entrypoint';
16
+ import { CLIAction } from 'lakutata/decorator/ctrl';
17
+ import { MobileDeployDTO } from '../dtos/MobileDeployDTO.js';
18
+ import { MobileNodeDTO } from '../dtos/MobileNodeDTO.js';
19
+ import { MobileRunDTO } from '../dtos/MobileRunDTO.js';
20
+ import { MobileStageDTO } from '../dtos/MobileStageDTO.js';
21
+ import { loadBuilder } from '../lib/Builder.js';
22
+ import { done, step, warn } from '../lib/Output.js';
23
+ import { runCommand, spawnCommand } from '../lib/Processes.js';
24
+ /**
25
+ * Where the native module of Android runs the Node.js project (copied there from the assets), and the marker of a copy
26
+ * written while developing, which the native module keeps instead of copying the assets again
27
+ */
28
+ const ANDROID_PROJECT = 'files/nodejs';
29
+ const ANDROID_DEVELOPMENT = 'files/nodejs.dev';
30
+ /**
31
+ * The port of Metro, which the debug builds of the user interface load it from
32
+ */
33
+ const METRO_PORT = 8081;
34
+ /**
35
+ * Run a command, its output given back; its input written to it (a stream of tar)
36
+ * @param command
37
+ * @param args
38
+ * @param input
39
+ */
40
+ function run(command, args, input) {
41
+ return new Promise((resolve, reject) => {
42
+ const child = spawn(command, args, { stdio: [input ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
43
+ let stdout = '';
44
+ let stderr = '';
45
+ child.stdout.on('data', (chunk) => stdout += chunk.toString());
46
+ child.stderr.on('data', (chunk) => stderr += chunk.toString());
47
+ child.once('error', reject);
48
+ child.once('close', (code) => resolve({ code: code ?? 1, stdout: stdout, stderr: stderr }));
49
+ if (input)
50
+ input.pipe(child.stdin);
51
+ });
52
+ }
53
+ /**
54
+ * Run a command, which must succeed
55
+ * @param what
56
+ * @param command
57
+ * @param args
58
+ * @param input
59
+ */
60
+ async function must(what, command, args, input) {
61
+ const result = await run(command, args, input);
62
+ if (result.code !== 0)
63
+ throw new Error(`${what}: ${(result.stderr || result.stdout).trim() || `${command} exited with ${result.code}`}`);
64
+ return result.stdout;
65
+ }
66
+ /**
67
+ * Run a command whose output is shown as it runs (the builds of the native projects), which must succeed
68
+ * @param command
69
+ * @param args
70
+ * @param cwd
71
+ * @param env
72
+ */
73
+ async function shown(command, args, cwd, env) {
74
+ step(`${path.basename(command)} ${args.join(' ')}`);
75
+ const code = await new Promise((resolve, reject) => {
76
+ const child = spawnCommand(command, args, { cwd: cwd, env: env, stdio: 'inherit' });
77
+ child.once('error', reject);
78
+ child.once('exit', resolve);
79
+ });
80
+ if (code !== 0)
81
+ throw new Error(`${path.basename(command)} ${args.join(' ')} failed (${code})`);
82
+ }
83
+ /**
84
+ * The project of a mobile application holding a directory: the first one up holding packages/ipc/native/NodeHost.cpp
85
+ * and packages/app
86
+ * @param directory
87
+ */
88
+ function mobileProject(directory) {
89
+ for (let current = directory;; current = path.dirname(current)) {
90
+ const packages = path.join(current, 'packages');
91
+ if (existsSync(path.join(packages, 'ipc', 'native', 'NodeHost.cpp')) && existsSync(path.join(packages, 'app', 'package.json'))) {
92
+ return { root: current, app: path.join(packages, 'app'), native: path.join(packages, 'ipc', 'native') };
93
+ }
94
+ if (current === path.dirname(current))
95
+ throw new Error(`${directory} is in no project of a mobile application (packages/app and packages/ipc/native/NodeHost.cpp, as the mobile-react template lays them out)`);
96
+ }
97
+ }
98
+ /**
99
+ * The libnode of a system put into the project (packages/ipc/native/node/out/<platform>)
100
+ * @param project
101
+ * @param platform
102
+ */
103
+ function nodeOf(project, platform) {
104
+ return path.join(project.native, 'node', 'out', platform);
105
+ }
106
+ /**
107
+ * Download the libnode of a system from the releases of lakutata/libnode (once: kept in the cache of lakutata) and put
108
+ * it into the project, where its native project takes it
109
+ * @param project
110
+ * @param platform
111
+ * @param abis the architectures of Android
112
+ * @returns where it is put
113
+ */
114
+ async function putNode(project, platform, abis) {
115
+ const { downloadMobileNode } = await loadBuilder(project.app);
116
+ const downloaded = await downloadMobileNode({ platform: platform, abis: abis, progress: (line) => step(`Downloading ${line}`) });
117
+ const out = nodeOf(project, platform);
118
+ await rm(out, { recursive: true, force: true });
119
+ await mkdir(path.dirname(out), { recursive: true });
120
+ //The files themselves, not the links of an export given (LAKUTATA_MOBILE_NODE_DIR): CocoaPods does not follow them
121
+ await cp(downloaded, out, { recursive: true, dereference: true, filter: (file) => path.basename(file) !== '.complete' });
122
+ //The libraries of the HAP hold libnode.so, which the addons and the runtime of the native module load by its name
123
+ if (platform === 'harmony') {
124
+ await mkdir(harmonyLibraries(project), { recursive: true });
125
+ await cp(path.join(out, 'libs', 'arm64-v8a', 'libnode.so'), path.join(harmonyLibraries(project), 'libnode.so'));
126
+ }
127
+ return out;
128
+ }
129
+ /**
130
+ * Where the native project of a system takes the Node.js project of the application
131
+ * @param project
132
+ * @param platform
133
+ */
134
+ function stageOf(project, platform) {
135
+ if (platform === 'android')
136
+ return path.join(project.root, 'android', 'app', 'src', 'main', 'assets', 'nodejs');
137
+ if (platform === 'ios')
138
+ return path.join(project.root, 'ios', 'nodejs');
139
+ return path.join(project.root, 'harmony', 'entry', 'src', 'main', 'resources', 'resfile', 'nodejs');
140
+ }
141
+ /**
142
+ * The libraries of the HAP of HarmonyOS (the libs/arm64-v8a of its module): libnode.so and the native addons
143
+ * @param project
144
+ */
145
+ function harmonyLibraries(project) {
146
+ return path.join(project.root, 'harmony', 'entry', 'libs', 'arm64-v8a');
147
+ }
148
+ /**
149
+ * The id of the application on a system, as its native project gives it: the applicationId of Android, the bundle
150
+ * identifier of iOS, the bundleName of HarmonyOS
151
+ * @param project
152
+ * @param platform
153
+ */
154
+ function appIdOf(project, platform) {
155
+ const read = (file, pattern, what) => {
156
+ const id = existsSync(file) ? pattern.exec(readFileSync(file, 'utf8'))?.[1] : undefined;
157
+ if (!id)
158
+ throw new Error(`The id of the application cannot be read (${what} of ${path.relative(project.root, file)})`);
159
+ return id;
160
+ };
161
+ if (platform === 'android')
162
+ return read(path.join(project.root, 'android', 'app', 'build.gradle'), /applicationId\s+["']([^"']+)["']/, 'applicationId');
163
+ if (platform === 'ios')
164
+ return read(path.join(project.root, 'ios', 'App.xcodeproj', 'project.pbxproj'), /PRODUCT_BUNDLE_IDENTIFIER = "?([^";]+)"?;/, 'PRODUCT_BUNDLE_IDENTIFIER');
165
+ return read(path.join(project.root, 'harmony', 'AppScope', 'app.json5'), /"bundleName"\s*:\s*"([^"]+)"/, 'bundleName');
166
+ }
167
+ /**
168
+ * The adb of the Android SDK (ANDROID_HOME), else the one of the PATH
169
+ */
170
+ function adbProgram() {
171
+ const home = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT;
172
+ return home ? path.join(home, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb') : 'adb';
173
+ }
174
+ /**
175
+ * The emulator or the device of Android the command runs on: the one given, else the one running (adb devices)
176
+ * @param device
177
+ */
178
+ async function androidDevice(device) {
179
+ if (device)
180
+ return device;
181
+ const serial = (await must('The devices of Android cannot be listed', adbProgram(), ['devices'])).split('\n').map((line) => line.trim().split(/\s+/)).find((parts) => parts[1] === 'device')?.[0];
182
+ if (!serial)
183
+ throw new Error('No emulator or device of Android is running (start one: emulator -avd <name>)');
184
+ return serial;
185
+ }
186
+ /**
187
+ * The architecture of an emulator or a device of Android (its ABI: arm64-v8a, x86_64…)
188
+ * @param serial
189
+ */
190
+ async function androidAbi(serial) {
191
+ return (await must('The architecture of the device of Android cannot be read', adbProgram(), ['-s', serial, 'shell', 'getprop', 'ro.product.cpu.abi'])).trim();
192
+ }
193
+ /**
194
+ * Start the application installed on an emulator or a device of Android again (its launcher activity)
195
+ * @param serial
196
+ * @param appId
197
+ */
198
+ async function restartAndroid(serial, appId) {
199
+ const adb = adbProgram();
200
+ const activity = (await must('The activity of the application cannot be found', adb, ['-s', serial, 'shell', 'cmd', 'package', 'resolve-activity', '--brief', '-c', 'android.intent.category.LAUNCHER', appId])).trim().split('\n').at(-1).trim();
201
+ await must('The application cannot be stopped', adb, ['-s', serial, 'shell', 'am', 'force-stop', appId]);
202
+ await must('The application cannot be started', adb, ['-s', serial, 'shell', 'am', 'start', '-n', activity]);
203
+ }
204
+ /**
205
+ * Put the Node.js project into the application installed on an emulator or a device of Android (a debug build:
206
+ * run-as), marked as written while developing (the native module keeps it instead of copying the assets again), and
207
+ * start the application again
208
+ * @param directory
209
+ * @param appId
210
+ * @param serial
211
+ */
212
+ async function deployAndroid(directory, appId, serial) {
213
+ const adb = adbProgram();
214
+ const installed = await run(adb, ['-s', serial, 'shell', 'pm', 'path', appId]);
215
+ if (installed.code !== 0 || !installed.stdout.includes('package:'))
216
+ throw new Error(`The application ${appId} is not installed on ${serial} (npm run android)`);
217
+ const debuggable = await run(adb, ['-s', serial, 'shell', 'run-as', appId, 'ls']);
218
+ if (debuggable.code !== 0)
219
+ throw new Error(`The files of ${appId} cannot be written (run-as): a debug build of the application is deployed to (${(debuggable.stderr || debuggable.stdout).trim()})`);
220
+ await must('The application cannot be stopped', adb, ['-s', serial, 'shell', 'am', 'force-stop', appId]);
221
+ //The files of the project replaced by the ones made, through tar (the files of macOS without their extended attributes)
222
+ const tar = spawn('tar', ['-c', '-C', directory, '.'], { stdio: ['ignore', 'pipe', 'inherit'], env: { ...process.env, COPYFILE_DISABLE: '1' } });
223
+ await must('The Node.js project cannot be written into the application', adb, ['-s', serial, 'shell', 'run-as', appId, 'sh', '-c', `'rm -rf ${ANDROID_PROJECT} && mkdir -p ${ANDROID_PROJECT} && tar -x -f - -C ${ANDROID_PROJECT} && touch ${ANDROID_DEVELOPMENT}'`], tar.stdout);
224
+ await restartAndroid(serial, appId);
225
+ }
226
+ /**
227
+ * Build the debug application of Android (Gradle, for the architecture of the device), install it, make Metro reachable
228
+ * from the device (adb reverse) and start the application: the Node.js project of its assets replaces a copy deployed
229
+ * before
230
+ * @param project
231
+ * @param device
232
+ * @return the id of the application and the device
233
+ */
234
+ async function runAndroid(project, device) {
235
+ const serial = await androidDevice(device);
236
+ const adb = adbProgram();
237
+ const appId = appIdOf(project, 'android');
238
+ await must('Metro cannot be made reachable from the device (adb reverse)', adb, ['-s', serial, 'reverse', `tcp:${METRO_PORT}`, `tcp:${METRO_PORT}`]);
239
+ const gradlew = path.join(project.root, 'android', process.platform === 'win32' ? 'gradlew.bat' : 'gradlew');
240
+ await shown(gradlew, ['installDebug', `-PreactNativeArchitectures=${await androidAbi(serial)}`], path.join(project.root, 'android'), { ...process.env, ANDROID_SERIAL: serial });
241
+ await run(adb, ['-s', serial, 'shell', 'run-as', appId, 'rm', '-f', ANDROID_DEVELOPMENT]);
242
+ await restartAndroid(serial, appId);
243
+ return { appId: appId, serial: serial };
244
+ }
245
+ /**
246
+ * The simulator of iOS the application runs on, booted (its UDID): the one given (its UDID or its name), else the one
247
+ * running, else the newest iPhone
248
+ * @param device
249
+ */
250
+ async function iosSimulator(device) {
251
+ const listed = JSON.parse(await must('The simulators cannot be listed', 'xcrun', ['simctl', 'list', 'devices', 'available', '--json']));
252
+ //The ones of the runtimes of iOS, the newest first
253
+ const simulators = Object.entries(listed.devices)
254
+ .filter(([runtime]) => runtime.includes('SimRuntime.iOS-'))
255
+ .sort(([a], [b]) => b.localeCompare(a, undefined, { numeric: true }))
256
+ .flatMap(([, devices]) => devices);
257
+ const simulator = device
258
+ ? simulators.find((candidate) => candidate.udid === device || candidate.name === device)
259
+ : simulators.find((candidate) => candidate.state === 'Booted') ?? simulators.find((candidate) => candidate.name.startsWith('iPhone'));
260
+ if (!simulator)
261
+ throw new Error(device ? `No simulator of iOS is ${device} (xcrun simctl list devices)` : 'No simulator of iOS is available (the platform of iOS of Xcode)');
262
+ if (simulator.state !== 'Booted') {
263
+ step(`The simulator ${simulator.name} booted`);
264
+ await must(`The simulator ${simulator.name} cannot be booted`, 'xcrun', ['simctl', 'bootstatus', simulator.udid, '-b']);
265
+ }
266
+ return simulator.udid;
267
+ }
268
+ /**
269
+ * Show a simulator of iOS in the app of Xcode showing them: Simulator, Device Hub since Xcode 27. Not shown without it:
270
+ * the application runs all the same
271
+ * @param udid
272
+ */
273
+ async function showSimulator(udid) {
274
+ const developer = (await run('xcode-select', ['-p'])).stdout.trim();
275
+ if (!developer)
276
+ return;
277
+ const simulator = path.join(developer, 'Applications', 'Simulator.app');
278
+ const hub = path.join(developer, '..', 'Applications', 'DeviceHub.app');
279
+ if (existsSync(simulator))
280
+ await run('open', [simulator, '--args', '-CurrentDeviceUDID', udid]);
281
+ else if (existsSync(hub))
282
+ await run('open', [hub]);
283
+ }
284
+ /**
285
+ * Whether the pods of the project of iOS are those of its Podfile and of the libnode exported: installed after the last
286
+ * change of the Podfile and the last export of the libnode (whose framework the pod of the native module vendors)
287
+ * @param ios
288
+ * @param node the export of the libnode of iOS
289
+ */
290
+ function podsInstalled(ios, node) {
291
+ const lock = path.join(ios, 'Podfile.lock');
292
+ const manifest = path.join(ios, 'Pods', 'Manifest.lock');
293
+ if (!existsSync(path.join(ios, 'App.xcworkspace')) || !existsSync(lock) || !existsSync(manifest) || !existsSync(path.join(ios, 'Podfile')))
294
+ return false;
295
+ const installed = statSync(manifest).mtimeMs;
296
+ return readFileSync(lock, 'utf8') === readFileSync(manifest, 'utf8') && installed >= statSync(path.join(ios, 'Podfile')).mtimeMs && (!existsSync(node) || installed >= statSync(node).mtimeMs);
297
+ }
298
+ /**
299
+ * Build the debug application of iOS for a simulator (the pods installed first when needed, xcodebuild), install it and
300
+ * start it; a simulator shares localhost with the computer, so Metro is reachable
301
+ * @param project
302
+ * @param udid the simulator, booted
303
+ * @return the id of the application
304
+ */
305
+ async function runIos(project, udid) {
306
+ const ios = path.join(project.root, 'ios');
307
+ if (!podsInstalled(ios, nodeOf(project, 'ios')))
308
+ await shown('pod', ['install'], ios);
309
+ const derived = path.join(ios, 'build');
310
+ await shown('xcrun', ['xcodebuild', '-workspace', 'App.xcworkspace', '-scheme', 'App', '-configuration', 'Debug', '-destination', `id=${udid}`, '-derivedDataPath', derived, 'build'], ios);
311
+ const application = path.join(derived, 'Build', 'Products', 'Debug-iphonesimulator', 'App.app');
312
+ const id = (await must('The id of the application cannot be read', 'plutil', ['-extract', 'CFBundleIdentifier', 'raw', path.join(application, 'Info.plist')])).trim();
313
+ await must('The application cannot be installed', 'xcrun', ['simctl', 'install', udid, application]);
314
+ await showSimulator(udid);
315
+ await run('xcrun', ['simctl', 'terminate', udid, id]);
316
+ await must('The application cannot be started', 'xcrun', ['simctl', 'launch', udid, id]);
317
+ return id;
318
+ }
319
+ /**
320
+ * Put the Node.js project into the application installed in a simulator of iOS (the directory of the application on
321
+ * the Mac), with the frameworks of its native addons, and start the application again
322
+ * @param directory
323
+ * @param appId
324
+ * @param udid
325
+ * @param frameworks the frameworks of the native addons for the simulators
326
+ */
327
+ async function deployIos(directory, appId, udid, frameworks) {
328
+ const container = (await must(`The application ${appId} is not installed on the simulator ${udid} (npm run ios)`, 'xcrun', ['simctl', 'get_app_container', udid, appId, 'app'])).trim();
329
+ await run('xcrun', ['simctl', 'terminate', udid, appId]);
330
+ const project = path.join(container, 'nodejs');
331
+ await rm(project, { recursive: true, force: true });
332
+ await mkdir(project, { recursive: true });
333
+ for (const entry of await readdir(directory))
334
+ await cp(path.join(directory, entry), path.join(project, entry), { recursive: true });
335
+ //The frameworks of its native addons (signed ad hoc, which the simulators take) in place of the ones of the application
336
+ for (const entry of existsSync(frameworks) ? await readdir(frameworks) : []) {
337
+ if (!entry.endsWith('.framework'))
338
+ continue;
339
+ const target = path.join(container, 'Frameworks', entry);
340
+ await rm(target, { recursive: true, force: true });
341
+ await cp(path.join(frameworks, entry), target, { recursive: true });
342
+ }
343
+ await must('The application cannot be started', 'xcrun', ['simctl', 'launch', udid, appId]);
344
+ }
345
+ /**
346
+ * The tools of DevEco Studio building and installing the applications of HarmonyOS: ohpm and hvigor (run by the Node.js
347
+ * of DevEco Studio), its SDK and hdc. DEVECO_STUDIO_HOME gives the directory of DevEco Studio (its Contents on macOS) or
348
+ * of its command line tools, DEVECO_SDK_HOME its SDK
349
+ */
350
+ function devEcoTools() {
351
+ const home = process.env.DEVECO_STUDIO_HOME ?? (process.platform === 'darwin' ? '/Applications/DevEco-Studio.app/Contents' : path.join(os.homedir(), 'command-line-tools'));
352
+ const sdk = process.env.DEVECO_SDK_HOME ?? path.join(home, 'sdk');
353
+ const hvigor = path.join(home, 'tools', 'hvigor', 'bin', 'hvigorw.js');
354
+ const node = path.join(home, 'tools', 'node', 'bin', process.platform === 'win32' ? 'node.exe' : 'node');
355
+ if (!existsSync(hvigor))
356
+ throw new Error(`DevEco Studio builds the application of HarmonyOS: install it, or give its directory (DEVECO_STUDIO_HOME; ${hvigor} missing)`);
357
+ return {
358
+ ohpm: path.join(home, 'tools', 'ohpm', 'bin', process.platform === 'win32' ? 'ohpm.bat' : 'ohpm'),
359
+ hvigor: hvigor,
360
+ node: existsSync(node) ? node : process.execPath,
361
+ sdk: sdk,
362
+ hdc: path.join(sdk, 'default', 'openharmony', 'toolchains', process.platform === 'win32' ? 'hdc.exe' : 'hdc')
363
+ };
364
+ }
365
+ /**
366
+ * Run a command of hdc, which must succeed: hdc exits with 0 when it fails too, telling the failure in its output (a
367
+ * line [Fail]…, or error: … from the device)
368
+ * @param what
369
+ * @param hdc
370
+ * @param args
371
+ */
372
+ async function mustHdc(what, hdc, args) {
373
+ const output = await must(what, hdc, args);
374
+ if (/^\[Fail\]|error:/im.test(output))
375
+ throw new Error(`${what}: ${output.trim()}`);
376
+ return output;
377
+ }
378
+ /**
379
+ * The emulator or the device of HarmonyOS the command runs on: the one given, else the one connected (hdc list targets)
380
+ * @param hdc
381
+ * @param device
382
+ */
383
+ async function harmonyTarget(hdc, device) {
384
+ if (device)
385
+ return device;
386
+ const target = (await must('The devices of HarmonyOS cannot be listed', hdc, ['list', 'targets'])).split('\n').map((line) => line.trim()).find((line) => !!line && line !== '[Empty]');
387
+ if (!target)
388
+ throw new Error('No emulator or device of HarmonyOS is connected (start one in the Device Manager of DevEco Studio)');
389
+ return target;
390
+ }
391
+ /**
392
+ * The files of the application of HarmonyOS, as it sees them (hdc file send -b writes there into a debug build), where
393
+ * the native module runs the copy of the Node.js project written while developing (nodejs-dev: the resources of the HAP
394
+ * cannot be written) while its .development says on. The files cannot be removed from the computer, only written: the
395
+ * copy is turned off instead
396
+ */
397
+ const HARMONY_FILES = '/data/storage/el2/base/files';
398
+ /**
399
+ * Write the state of the copy of the Node.js project written while developing on HarmonyOS (on: deployed, off: the
400
+ * project of the HAP runs), along with the files of a project to deploy
401
+ * @param hdc
402
+ * @param target
403
+ * @param bundle
404
+ * @param state
405
+ * @param project the Node.js project deployed with the state on
406
+ * @returns the output of hdc
407
+ */
408
+ async function writeHarmonyDevelopment(hdc, target, bundle, state, project) {
409
+ const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-harmony-'));
410
+ try {
411
+ //Sent into the files as nodejs-dev, merged into the copy there (hdc copies a directory into the one of its name)
412
+ const development = path.join(work, 'nodejs-dev');
413
+ if (project)
414
+ await cp(project, development, { recursive: true });
415
+ else
416
+ await mkdir(development);
417
+ await writeFile(path.join(development, '.development'), `${state}\n`);
418
+ return project
419
+ ? await run(hdc, ['-t', target, 'file', 'send', '-b', bundle, development, HARMONY_FILES])
420
+ : await run(hdc, ['-t', target, 'file', 'send', '-b', bundle, path.join(development, '.development'), `${HARMONY_FILES}/nodejs-dev/.development`]);
421
+ }
422
+ finally {
423
+ await rm(work, { recursive: true, force: true });
424
+ }
425
+ }
426
+ /**
427
+ * Write the state of the copy of the Node.js project written while developing on HarmonyOS (writeHarmonyDevelopment),
428
+ * with the application stopped. Once the device has started, the files of an application are there after it has run:
429
+ * before, hdc refuses its name (E005101), and the application is started once, then stopped, and the files written
430
+ * again
431
+ * @param hdc
432
+ * @param target
433
+ * @param bundle
434
+ * @param state
435
+ * @param project the Node.js project deployed with the state on
436
+ * @returns the output of hdc
437
+ */
438
+ async function sendHarmonyDevelopment(hdc, target, bundle, state, project) {
439
+ const refused = (sent) => /E005101|Invalid bundle name/i.test(sent.stdout);
440
+ await run(hdc, ['-t', target, 'shell', 'aa', 'force-stop', bundle]);
441
+ let sent = await writeHarmonyDevelopment(hdc, target, bundle, state, project);
442
+ if (refused(sent)) {
443
+ await restartHarmony(hdc, target, bundle);
444
+ for (let attempt = 0; attempt < 10 && refused(sent); attempt++) {
445
+ await new Promise((resolve) => setTimeout(resolve, attempt ? 500 : 0));
446
+ await run(hdc, ['-t', target, 'shell', 'aa', 'force-stop', bundle]);
447
+ sent = await writeHarmonyDevelopment(hdc, target, bundle, state, project);
448
+ }
449
+ }
450
+ return sent;
451
+ }
452
+ /**
453
+ * Start the application installed on an emulator or a device of HarmonyOS again
454
+ * @param hdc
455
+ * @param target
456
+ * @param bundle
457
+ */
458
+ async function restartHarmony(hdc, target, bundle) {
459
+ await run(hdc, ['-t', target, 'shell', 'aa', 'force-stop', bundle]);
460
+ const started = await must('The application cannot be started', hdc, ['-t', target, 'shell', 'aa', 'start', '-a', 'EntryAbility', '-b', bundle]);
461
+ if (/error/i.test(started))
462
+ throw new Error(`The application cannot be started: ${started.trim()}`);
463
+ }
464
+ /**
465
+ * Build the debug HAP of HarmonyOS (the user interface bundled by RNOH, hvigor), install it, make Metro reachable from
466
+ * the device (hdc rport) and start the application: the Node.js project of its resources replaces a copy deployed before
467
+ * @param project
468
+ * @param device
469
+ * @return the bundle of the application and the device
470
+ */
471
+ async function runHarmony(project, device) {
472
+ const tools = devEcoTools();
473
+ const target = await harmonyTarget(tools.hdc, device);
474
+ const bundle = appIdOf(project, 'harmony');
475
+ //The user interface in the HAP, which loads it when Metro does not run (RNOH: a bundle of JavaScript)
476
+ const reactNative = path.join(project.root, 'node_modules', '.bin', process.platform === 'win32' ? 'react-native.cmd' : 'react-native');
477
+ await shown(reactNative, ['bundle-harmony', '--dev', 'false', '--bundle-output', path.join('harmony', 'entry', 'src', 'main', 'resources', 'rawfile', 'bundle.harmony.js')], project.root);
478
+ const harmony = path.join(project.root, 'harmony');
479
+ const env = { ...process.env, DEVECO_SDK_HOME: tools.sdk, PATH: `${path.dirname(tools.node)}${path.delimiter}${process.env.PATH ?? ''}` };
480
+ //The libraries of React Native ported to HarmonyOS (RNOH autolinking): their HARs added to oh-package.json5 before
481
+ //ohpm installs them, as hvigor adds them only once ohpm ran
482
+ await shown(reactNative, ['link-harmony'], project.root);
483
+ //The packages of the HAP (RNOH and the libraries, from node_modules) in harmony/oh_modules, which its native build reads
484
+ await shown(tools.ohpm, ['install', '--all'], harmony, env);
485
+ await shown(tools.node, [tools.hvigor, '--mode', 'module', '-p', 'product=default', '-p', 'module=entry@default', 'assembleHap', '--no-daemon'], harmony, env);
486
+ const outputs = path.join(harmony, 'entry', 'build', 'default', 'outputs', 'default');
487
+ const haps = existsSync(outputs) ? readdirSync(outputs).filter((name) => name.endsWith('.hap')) : [];
488
+ const hap = haps.find((name) => name.includes('-signed')) ?? haps[0];
489
+ if (!hap)
490
+ throw new Error(`The build made no HAP (${path.relative(project.root, outputs)})`);
491
+ await mustHdc('The application cannot be installed', tools.hdc, ['-t', target, 'install', '-r', path.join(outputs, hap)]);
492
+ //The forward of a run before removed first: the server of hdc keeps it, setting it up again on an emulator started
493
+ //again, and rport fails on the port it takes (none to remove: hdc tells it, which is no failure)
494
+ await run(tools.hdc, ['-t', target, 'fport', 'rm', `tcp:${METRO_PORT}`, `tcp:${METRO_PORT}`]);
495
+ await mustHdc('Metro cannot be made reachable from the device (hdc rport)', tools.hdc, ['-t', target, 'rport', `tcp:${METRO_PORT}`, `tcp:${METRO_PORT}`]);
496
+ //A copy deployed before turned off (none deployed: nothing to write); after the device started again too, or the
497
+ //copy would run instead of the project installed
498
+ await sendHarmonyDevelopment(tools.hdc, target, bundle, 'off');
499
+ await restartHarmony(tools.hdc, target, bundle);
500
+ return { bundle: bundle, target: target };
501
+ }
502
+ /**
503
+ * Put the Node.js project into the debug application installed on an emulator or a device of HarmonyOS (files/nodejs-dev
504
+ * of the application, turned on: the native module runs it instead of the one of its resources), and start the
505
+ * application again
506
+ * @param directory
507
+ * @param bundle
508
+ * @param device
509
+ */
510
+ async function deployHarmony(directory, bundle, device) {
511
+ const tools = devEcoTools();
512
+ const target = await harmonyTarget(tools.hdc, device);
513
+ const sent = await sendHarmonyDevelopment(tools.hdc, target, bundle, 'on', directory);
514
+ if (sent.code !== 0 || /fail|error/i.test(sent.stdout))
515
+ throw new Error(`The Node.js project cannot be written into the application ${bundle} (a debug build, hdc file send -b): ${(sent.stdout || sent.stderr).trim()}`);
516
+ await restartHarmony(tools.hdc, target, bundle);
517
+ return target;
518
+ }
519
+ /**
520
+ * The commands of the mobile applications of React Native laid out as the mobile-react template: their libnode, their
521
+ * Node.js project, the application run on an emulator or a simulator, the project deployed into the application
522
+ * installed during the development
523
+ */
524
+ export class MobileController extends Controller {
525
+ async node(input) {
526
+ const project = mobileProject(process.cwd());
527
+ const abis = input.abis ? input.abis.split(',').map((abi) => abi.trim()).filter(Boolean) : undefined;
528
+ const out = await putNode(project, input.platform, abis);
529
+ done(`The libnode of ${input.platform}: ${path.relative(process.cwd(), out)}`);
530
+ return undefined;
531
+ }
532
+ async stage(input) {
533
+ const project = mobileProject(process.cwd());
534
+ const stage = await this.stageProject(project, input.platform, { minify: input.minify, abis: input.abis ? input.abis.split(',').map((abi) => abi.trim()).filter(Boolean) : undefined });
535
+ done(`The Node.js project for ${stage.platform} (Node.js ${stage.node}): ${path.relative(process.cwd(), stage.directory)}`);
536
+ return undefined;
537
+ }
538
+ async run(input) {
539
+ const project = mobileProject(process.cwd());
540
+ if (input.platform === 'android') {
541
+ const { appId, serial } = await runAndroid(project, input.device);
542
+ done(`${appId} runs on ${serial}`);
543
+ }
544
+ else if (input.platform === 'ios') {
545
+ const udid = await iosSimulator(input.device);
546
+ done(`${await runIos(project, udid)} runs on the simulator ${udid}`);
547
+ }
548
+ else {
549
+ const { bundle, target } = await runHarmony(project, input.device);
550
+ done(`${bundle} runs on ${target}`);
551
+ }
552
+ return undefined;
553
+ }
554
+ async deploy(input) {
555
+ const project = mobileProject(process.cwd());
556
+ await this.deployOnce(project, input);
557
+ if (!input.watch)
558
+ return undefined;
559
+ //The files of the application, without its dependencies and its compiled files: built and deployed again once
560
+ //they stop changing
561
+ const manifest = JSON.parse(readFileSync(path.join(project.app, 'package.json'), 'utf8'));
562
+ const ignored = ['node_modules', path.dirname(manifest.main ?? 'build/App.js')].map((directory) => directory.split(path.sep).join('/'));
563
+ const watched = (file) => {
564
+ if (!file)
565
+ return false;
566
+ const relative = file.split(path.sep).join('/');
567
+ return !relative.split('/').some((part) => part.startsWith('.')) && !ignored.some((directory) => relative === directory || relative.startsWith(`${directory}/`));
568
+ };
569
+ step('Watching the files of the application: built and deployed again when they change (Ctrl+C to stop)');
570
+ await new Promise((resolve) => {
571
+ let timer;
572
+ let deploying = Promise.resolve();
573
+ const watcher = watch(project.app, { recursive: true }, (_event, file) => {
574
+ if (!watched(file))
575
+ return;
576
+ clearTimeout(timer);
577
+ timer = setTimeout(() => {
578
+ deploying = deploying.then(async () => {
579
+ if (manifest.scripts?.build) {
580
+ step('npm run build');
581
+ const built = await runCommand('npm', ['run', 'build'], project.app);
582
+ if (built.code !== 0)
583
+ return warn(`The build failed:\n${built.output.trim().split('\n').slice(-15).join('\n')}`);
584
+ }
585
+ await this.deployOnce(project, input);
586
+ }).catch((error) => warn(error.message));
587
+ }, 300);
588
+ });
589
+ const stop = () => {
590
+ clearTimeout(timer);
591
+ watcher.close();
592
+ resolve();
593
+ };
594
+ process.once('SIGINT', stop);
595
+ process.once('SIGTERM', stop);
596
+ });
597
+ return undefined;
598
+ }
599
+ /**
600
+ * Make the Node.js project of the application for a system, where its native project takes it
601
+ * @param project
602
+ * @param platform
603
+ * @param options
604
+ * @protected
605
+ */
606
+ async stageProject(project, platform, options) {
607
+ const { stageMobileApp } = await loadBuilder(project.app);
608
+ //The libnode of the system downloaded first when the project has none yet
609
+ if (!existsSync(path.join(nodeOf(project, platform), 'include', 'node', 'node_api.h')))
610
+ await putNode(project, platform);
611
+ return stageMobileApp({
612
+ platform: platform,
613
+ project: project.app,
614
+ outdir: stageOf(project, platform),
615
+ node: nodeOf(project, platform),
616
+ frameworks: platform === 'ios' ? path.join(project.root, 'ios', 'NodeAddons') : undefined,
617
+ libraries: platform === 'harmony' ? harmonyLibraries(project) : undefined,
618
+ minify: options.minify,
619
+ abis: options.abis,
620
+ sdks: options.sdks,
621
+ progress: step
622
+ });
623
+ }
624
+ /**
625
+ * Make the Node.js project, put it into the application and start the application again
626
+ * @param project
627
+ * @param input
628
+ * @protected
629
+ */
630
+ async deployOnce(project, input) {
631
+ if (input.platform === 'android') {
632
+ const serial = await androidDevice(input.device);
633
+ //The native addons built for the architecture of the device only
634
+ const stage = await this.stageProject(project, 'android', { minify: false, abis: [await androidAbi(serial)] });
635
+ await deployAndroid(stage.directory, appIdOf(project, 'android'), serial);
636
+ done(`Deployed to ${appIdOf(project, 'android')} (${serial}) and started`);
637
+ }
638
+ else if (input.platform === 'ios') {
639
+ const udid = await iosSimulator(input.device);
640
+ const stage = await this.stageProject(project, 'ios', { minify: false, sdks: ['iphonesimulator'] });
641
+ await deployIos(stage.directory, appIdOf(project, 'ios'), udid, path.join(project.root, 'ios', 'NodeAddons', 'iphonesimulator'));
642
+ done(`Deployed to ${appIdOf(project, 'ios')} (${udid}) and started`);
643
+ }
644
+ else {
645
+ const stage = await this.stageProject(project, 'harmony', { minify: false });
646
+ //The libraries of the native addons are in the HAP: installed again with it
647
+ if (stage.addons?.libraries.length)
648
+ warn(`The native addons of HarmonyOS are libraries of the HAP: npm run harmony installs them (${stage.addons.libraries.join(', ')})`);
649
+ const target = await deployHarmony(stage.directory, appIdOf(project, 'harmony'), input.device);
650
+ done(`Deployed to ${appIdOf(project, 'harmony')} (${target}) and started`);
651
+ }
652
+ }
653
+ }
654
+ __decorate([
655
+ CLIAction('mobile node <platform>', MobileNodeDTO, { description: 'Download the libnode of a system (android, ios, harmony) from the releases of lakutata/libnode (LAKUTATA_LIBNODE_MIRROR: a mirror of them), once: kept in the cache of lakutata, put into packages/ipc/native/node/out/<platform>' }),
656
+ __metadata("design:type", Function),
657
+ __metadata("design:paramtypes", [MobileNodeDTO]),
658
+ __metadata("design:returntype", Promise)
659
+ ], MobileController.prototype, "node", null);
660
+ __decorate([
661
+ CLIAction('mobile stage <platform>', MobileStageDTO, { description: 'Make the Node.js project of the mobile application (packages/app) for a system (android, ios, harmony), where its native project takes it, with its native addons built against the libnode of the system' }),
662
+ __metadata("design:type", Function),
663
+ __metadata("design:paramtypes", [MobileStageDTO]),
664
+ __metadata("design:returntype", Promise)
665
+ ], MobileController.prototype, "stage", null);
666
+ __decorate([
667
+ CLIAction('mobile run <platform>', MobileRunDTO, { description: 'Build the debug application of a system (android, ios, harmony) from its native project, install it on an emulator, a device or a simulator and start it, Metro (npm start) giving its user interface' }),
668
+ __metadata("design:type", Function),
669
+ __metadata("design:paramtypes", [MobileRunDTO]),
670
+ __metadata("design:returntype", Promise)
671
+ ], MobileController.prototype, "run", null);
672
+ __decorate([
673
+ CLIAction('mobile deploy <platform>', MobileDeployDTO, { description: 'Make the Node.js project of the mobile application, put it into the debug application installed on an emulator, a device or a simulator of a system (android, ios, harmony), and start the application again (the development, without installing it again)' }),
674
+ __metadata("design:type", Function),
675
+ __metadata("design:paramtypes", [MobileDeployDTO]),
676
+ __metadata("design:returntype", Promise)
677
+ ], MobileController.prototype, "deploy", null);