@blotch/angular-lynx-native 22.2.4 → 22.2.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 +3 -2
- package/dist/android/run.d.ts +61 -0
- package/dist/android/run.d.ts.map +1 -0
- package/dist/android/run.js +241 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/ios/run.d.ts +61 -0
- package/dist/ios/run.d.ts.map +1 -0
- package/dist/ios/run.js +176 -0
- package/dist/process.d.ts +4 -1
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +9 -2
- package/dist/run.d.ts +21 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +1 -0
- package/package.json +1 -1
- package/templates/android/README.md +8 -0
- package/templates/android/app/src/main/java/dev/angularlynx/shell/DynamicColors.kt +165 -0
- package/templates/android/app/src/main/java/dev/angularlynx/shell/MainActivity.kt +17 -2
- package/templates/android/app/src/main/res/values/colors.xml +9 -0
- package/templates/android/app/src/main/res/values/themes.xml +1 -1
- package/templates/android/app/src/main/res/values-night/colors.xml +4 -0
- package/templates/android/app/src/main/res/values-night/themes.xml +1 -1
- package/templates/ios/App/BlurView.swift +149 -0
- package/templates/ios/App/GlassTransitions.swift +408 -0
- package/templates/ios/App/XElementRegistry.swift +3 -2
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ User documentation: [Publishing your App](../website/docs/guide/building-for-pro
|
|
|
9
9
|
1. **Generates** the native project from `templates/ios` or `templates/android` into the project's folder in Angular's build cache (`.angular/cache/<Angular version>/<project>/lynx-native/<platform>`, which `ng new` already gitignores and `ng cache clean` clears), writing the app options from `angular.json` into the few files the templates mark for it. It regenerates only when a hash of its inputs changes (this package's version, the template, the options, the icon), so Xcode and Gradle keep their incremental state.
|
|
10
10
|
2. **Copies** `dist/` (without the web bundle) into the project.
|
|
11
11
|
3. **Builds** it with `xcodebuild` or Gradle and copies the binaries to `dist/<platform>/`.
|
|
12
|
+
4. **Runs** it, with `--run` (`src/ios/run.ts`, `src/android/run.ts`): picks a simulator, emulator or phone (`--device`, else a running one, else the newest iPhone simulator or the first AVD) and starts it before step 3, so it boots while the app compiles; then installs the app and opens it. iOS uses `simctl` and shows the simulator in DeviceHub (`devices://device/open?id=<UDID>`) or, before Xcode 27.1, the Simulator app. Android uses the SDK's `adb` and `emulator` and opens the launcher activity by component.
|
|
12
13
|
|
|
13
14
|
An ejected project (`ios/` or `android/` with a `.angular-lynx-ejected` marker, written by `ng generate @blotch/angular-lynx:eject`) is built as it is and never regenerated.
|
|
14
15
|
|
|
@@ -16,9 +17,9 @@ An ejected project (`ios/` or `android/` with a `.angular-lynx-ejected` marker,
|
|
|
16
17
|
|
|
17
18
|
Both are one full-screen `LynxView` running `main.lynx.bundle`; Angular Router does all navigation inside it. Bundles and assets load from inside the app, so RSpeedy's default asset prefix (`/static/...`) works with no configuration. The `LynxView` sits inside the safe area, so CSS `env(safe-area-inset-*)` is 0 in it.
|
|
18
19
|
|
|
19
|
-
Both send the same global props, which the runtime's services read: `platform`, `theme` (again whenever the phone switches between light and dark), `appLocale` (the phone's language as a BCP 47 tag), `isNotchScreen`, the safe-area insets `safeAreaTop`, `safeAreaBottom`, `safeAreaLeft` and `safeAreaRight`, `osVersion` (which Lynx leaves out of the main thread's `SystemInfo`), and the accessibility settings `reduceMotion`, `reduceTransparency` (iOS only) and `increaseContrast`, again whenever the user changes one. Lynx Explorer sends `theme` and `isNotchScreen` too, but no `appLocale`, `osVersion` or accessibility settings. See [`templates/android/README.md`](templates/android/README.md) for the Android shell's details.
|
|
20
|
+
Both send the same global props, which the runtime's services read: `platform`, `theme` (again whenever the phone switches between light and dark), `appLocale` (the phone's language as a BCP 47 tag), `isNotchScreen`, the safe-area insets `safeAreaTop`, `safeAreaBottom`, `safeAreaLeft` and `safeAreaRight`, `osVersion` (which Lynx leaves out of the main thread's `SystemInfo`), and the accessibility settings `reduceMotion`, `reduceTransparency` (iOS only) and `increaseContrast`, again whenever the user changes one. Android 12 and later also send `dynamicColors`, the wallpaper's Material 3 color roles in light and dark (`DynamicColors.kt`), again when the user changes their wallpaper or colors. Lynx Explorer sends `theme` and `isNotchScreen` too, but no `appLocale`, `osVersion` or accessibility settings. See [`templates/android/README.md`](templates/android/README.md) for the Android shell's details.
|
|
20
21
|
|
|
21
|
-
Both link Lynx's XElement library, which holds `<input>`, `<textarea>`, `<overlay>`, `<svg>`, `<blur-view>`, `<viewpager>`, `<refresh>`, the `<scroll-coordinator>` family, `<webview>` and `<video>`. Lynx's core doesn't include these, and a host that doesn't register them renders nothing for those tags (on Android, nothing after them either). XElement's AnimaX and Markdown elements are left out: the runtime doesn't support them.
|
|
22
|
+
Both link Lynx's XElement library, which holds `<input>`, `<textarea>`, `<overlay>`, `<svg>`, `<blur-view>`, `<viewpager>`, `<refresh>`, the `<scroll-coordinator>` family, `<webview>` and `<video>`. Lynx's core doesn't include these, and a host that doesn't register them renders nothing for those tags (on Android, nothing after them either). XElement's AnimaX and Markdown elements are left out: the runtime doesn't support them. On iOS, `<blur-view>` is XElement's with more of UIKit in it (`App/BlurView.swift`): a `blur-material` attribute for UIKit's adaptive blur materials, which the glass components draw their design before iOS 26 on (Lynx's blur-view has only the styles from before iOS 13); Liquid Glass that materializes, dissolves and eases between effects inside a `glass-container`, where Lynx's sets its effect at once; and a `glass-effect-id` attribute that pairs two glass shapes across a change so one morphs into the other (`App/GlassTransitions.swift`).
|
|
22
23
|
|
|
23
24
|
| | iOS | Android |
|
|
24
25
|
| ---------------- | ---------------------------------------------------- | -------------------------------------------------------- |
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RunTarget } from '../run.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The activity the shell template declares: its manifest names `.MainActivity`
|
|
4
|
+
* in the `dev.angularlynx.shell` namespace, whatever the app's ID is.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SHELL_ACTIVITY = "dev.angularlynx.shell.MainActivity";
|
|
7
|
+
export type AdbDevice = {
|
|
8
|
+
readonly serial: string;
|
|
9
|
+
/** `device` when it's ready; also `offline`, `unauthorized`, ... */
|
|
10
|
+
readonly state: string;
|
|
11
|
+
/** The phone's model from `adb devices -l`, with spaces for its underscores. */
|
|
12
|
+
readonly model?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Parses `adb devices -l`: a header, then one line per emulator or phone,
|
|
16
|
+
* e.g. `1A2B3C device usb:1-2 product:husky model:Pixel_8 ...`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const parseAdbDevices: (output: string) => AdbDevice[];
|
|
19
|
+
/** Where `--run` opens the app: a running device, or a virtual device to start. */
|
|
20
|
+
export type AndroidChoice = {
|
|
21
|
+
readonly serial: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly avd: string;
|
|
25
|
+
readonly label: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Chooses the device `ng build --platform android --run` opens the app on.
|
|
29
|
+
*
|
|
30
|
+
* With `--device`, the running emulator or phone with that adb serial, the
|
|
31
|
+
* running emulator of that virtual device, or else that virtual device,
|
|
32
|
+
* started. Otherwise the first running emulator or connected phone (one
|
|
33
|
+
* that's still booting counts), or else the first virtual device, started.
|
|
34
|
+
*/
|
|
35
|
+
export declare const pickAndroidDevice: (devices: readonly AdbDevice[], device: string | undefined, { avdName, listVirtualDevices, }: {
|
|
36
|
+
/** The virtual device a running emulator runs, from its console. */
|
|
37
|
+
readonly avdName: (serial: string) => Promise<string | undefined>;
|
|
38
|
+
readonly listVirtualDevices: () => Promise<readonly string[]>;
|
|
39
|
+
}) => Promise<AndroidChoice>;
|
|
40
|
+
/** How long `--run` waits for an emulator to boot, and how often it checks. */
|
|
41
|
+
declare const TIMING: {
|
|
42
|
+
pollMs: number;
|
|
43
|
+
timeoutMs: number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Picks the device for `--run` and, when it's a virtual device that isn't
|
|
47
|
+
* running, starts its emulator, so it boots while Gradle builds. The returned
|
|
48
|
+
* target installs and opens the app once it's built.
|
|
49
|
+
*
|
|
50
|
+
* adb and the emulator are run from the SDK the build already found, so
|
|
51
|
+
* nobody has to put the SDK's folders on their PATH.
|
|
52
|
+
*/
|
|
53
|
+
export declare const startAndroidDevice: ({ device, logFile, log, env, timing, }: {
|
|
54
|
+
readonly device: string | undefined;
|
|
55
|
+
readonly logFile: string;
|
|
56
|
+
readonly log: (message: string) => void;
|
|
57
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
58
|
+
readonly timing?: typeof TIMING;
|
|
59
|
+
}) => Promise<RunTarget>;
|
|
60
|
+
export {};
|
|
61
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/android/run.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C;;;GAGG;AACH,eAAO,MAAM,cAAc,uCAAuC,CAAC;AAEnE,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,KAAG,SAAS,EAQtD,CAAC;AAIL,mFAAmF;AACnF,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,SAAS,SAAS,EAAE,EAC7B,QAAQ,MAAM,GAAG,SAAS,EAC1B,kCAGG;IACD,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAClE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;CAC/D,KACA,OAAO,CAAC,aAAa,CAsEvB,CAAC;AAEF,+EAA+E;AAC/E,QAAA,MAAM,MAAM;;;CAA0C,CAAC;AAEvD;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAU,wCAMtC;IACD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,MAAM,CAAC;CACjC,KAAG,OAAO,CAAC,SAAS,CAsJpB,CAAC"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { closeSync, existsSync, openSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { NativeBuildError, capture, run } from "../process.js";
|
|
5
|
+
import { findAndroidSdk } from "./project.js";
|
|
6
|
+
/**
|
|
7
|
+
* The activity the shell template declares: its manifest names `.MainActivity`
|
|
8
|
+
* in the `dev.angularlynx.shell` namespace, whatever the app's ID is.
|
|
9
|
+
*/
|
|
10
|
+
export const SHELL_ACTIVITY = 'dev.angularlynx.shell.MainActivity';
|
|
11
|
+
/**
|
|
12
|
+
* Parses `adb devices -l`: a header, then one line per emulator or phone,
|
|
13
|
+
* e.g. `1A2B3C device usb:1-2 product:husky model:Pixel_8 ...`.
|
|
14
|
+
*/
|
|
15
|
+
export const parseAdbDevices = (output) => output.split('\n').flatMap((line) => {
|
|
16
|
+
const match = /^(\S+)\s+(\S+)(.*)$/.exec(line.trim());
|
|
17
|
+
if (!match || line.startsWith('List of devices') || line.startsWith('*')) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const model = /\bmodel:(\S+)/.exec(match[3])?.[1]?.replace(/_/g, ' ');
|
|
21
|
+
return [{ serial: match[1], state: match[2], ...(model && { model }) }];
|
|
22
|
+
});
|
|
23
|
+
const isEmulator = (device) => device.serial.startsWith('emulator-');
|
|
24
|
+
/**
|
|
25
|
+
* Chooses the device `ng build --platform android --run` opens the app on.
|
|
26
|
+
*
|
|
27
|
+
* With `--device`, the running emulator or phone with that adb serial, the
|
|
28
|
+
* running emulator of that virtual device, or else that virtual device,
|
|
29
|
+
* started. Otherwise the first running emulator or connected phone (one
|
|
30
|
+
* that's still booting counts), or else the first virtual device, started.
|
|
31
|
+
*/
|
|
32
|
+
export const pickAndroidDevice = async (devices, device, { avdName, listVirtualDevices, }) => {
|
|
33
|
+
const running = async (candidate) => {
|
|
34
|
+
if (candidate.state === 'unauthorized') {
|
|
35
|
+
throw new NativeBuildError(`${candidate.model ?? candidate.serial} hasn't allowed USB debugging from this computer. Unlock it, accept the prompt, and build again.`);
|
|
36
|
+
}
|
|
37
|
+
const avd = isEmulator(candidate)
|
|
38
|
+
? await avdName(candidate.serial)
|
|
39
|
+
: undefined;
|
|
40
|
+
return {
|
|
41
|
+
serial: candidate.serial,
|
|
42
|
+
label: avd
|
|
43
|
+
? `the ${avd} emulator`
|
|
44
|
+
: isEmulator(candidate)
|
|
45
|
+
? `the emulator ${candidate.serial}`
|
|
46
|
+
: (candidate.model ?? candidate.serial),
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
const usable = devices.filter((candidate) => candidate.state === 'device' ||
|
|
50
|
+
candidate.state === 'unauthorized' ||
|
|
51
|
+
// An emulator that's still booting: launch() waits for it.
|
|
52
|
+
(isEmulator(candidate) && candidate.state === 'offline'));
|
|
53
|
+
if (device) {
|
|
54
|
+
const bySerial = usable.find((candidate) => candidate.serial === device);
|
|
55
|
+
if (bySerial) {
|
|
56
|
+
return running(bySerial);
|
|
57
|
+
}
|
|
58
|
+
for (const candidate of usable.filter(isEmulator)) {
|
|
59
|
+
if ((await avdName(candidate.serial)) === device) {
|
|
60
|
+
return running(candidate);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const virtualDevices = await listVirtualDevices();
|
|
64
|
+
if (virtualDevices.includes(device)) {
|
|
65
|
+
return { avd: device, label: `the ${device} emulator` };
|
|
66
|
+
}
|
|
67
|
+
const known = [
|
|
68
|
+
...usable.map((candidate) => candidate.serial),
|
|
69
|
+
...virtualDevices,
|
|
70
|
+
];
|
|
71
|
+
throw new NativeBuildError(`There's no running emulator, connected phone or virtual device named "${device}". ${known.length > 0
|
|
72
|
+
? `Pass one of these: ${known.join(', ')}.`
|
|
73
|
+
: "Create a virtual device in Android Studio's Device Manager."}`);
|
|
74
|
+
}
|
|
75
|
+
// A ready one before one that's booting, and before a phone that hasn't
|
|
76
|
+
// allowed debugging yet, which only gets a message saying so.
|
|
77
|
+
const first = usable.find((candidate) => candidate.state === 'device') ??
|
|
78
|
+
usable.find((candidate) => candidate.state === 'offline') ??
|
|
79
|
+
usable[0];
|
|
80
|
+
if (first) {
|
|
81
|
+
return running(first);
|
|
82
|
+
}
|
|
83
|
+
const [avd] = await listVirtualDevices();
|
|
84
|
+
if (avd) {
|
|
85
|
+
return { avd, label: `the ${avd} emulator` };
|
|
86
|
+
}
|
|
87
|
+
throw new NativeBuildError("No emulator is running, no phone is connected, and there's no virtual device to start. Create one in Android Studio's Device Manager, or connect a phone with USB debugging turned on.");
|
|
88
|
+
};
|
|
89
|
+
/** How long `--run` waits for an emulator to boot, and how often it checks. */
|
|
90
|
+
const TIMING = { pollMs: 1000, timeoutMs: 5 * 60_000 };
|
|
91
|
+
/**
|
|
92
|
+
* Picks the device for `--run` and, when it's a virtual device that isn't
|
|
93
|
+
* running, starts its emulator, so it boots while Gradle builds. The returned
|
|
94
|
+
* target installs and opens the app once it's built.
|
|
95
|
+
*
|
|
96
|
+
* adb and the emulator are run from the SDK the build already found, so
|
|
97
|
+
* nobody has to put the SDK's folders on their PATH.
|
|
98
|
+
*/
|
|
99
|
+
export const startAndroidDevice = async ({ device, logFile, log, env = process.env, timing = TIMING, }) => {
|
|
100
|
+
const sdk = findAndroidSdk(env);
|
|
101
|
+
if (!sdk) {
|
|
102
|
+
throw new NativeBuildError('The Android SDK was not found. Install Android Studio (which installs the SDK), or set ANDROID_HOME to an existing SDK.');
|
|
103
|
+
}
|
|
104
|
+
const exe = process.platform === 'win32' ? '.exe' : '';
|
|
105
|
+
const adbPath = path.join(sdk, 'platform-tools', `adb${exe}`);
|
|
106
|
+
const emulatorPath = path.join(sdk, 'emulator', `emulator${exe}`);
|
|
107
|
+
if (!existsSync(adbPath)) {
|
|
108
|
+
throw new NativeBuildError(`adb isn't in ${sdk}. Install Android SDK Platform-Tools in Android Studio's SDK Manager, under SDK Tools.`);
|
|
109
|
+
}
|
|
110
|
+
const adb = (args) => run(adbPath, args, { cwd: process.cwd(), logFile });
|
|
111
|
+
const listDevices = async () => {
|
|
112
|
+
const output = await capture(adbPath, ['devices', '-l']);
|
|
113
|
+
if (output === undefined) {
|
|
114
|
+
throw new NativeBuildError(`\`${adbPath} devices\` failed. Run it yourself to see why.`);
|
|
115
|
+
}
|
|
116
|
+
return parseAdbDevices(output);
|
|
117
|
+
};
|
|
118
|
+
const before = await listDevices();
|
|
119
|
+
const choice = await pickAndroidDevice(before, device, {
|
|
120
|
+
avdName: async (serial) => (await capture(adbPath, ['-s', serial, 'emu', 'avd', 'name']))
|
|
121
|
+
?.split('\n')[0]
|
|
122
|
+
?.trim(),
|
|
123
|
+
listVirtualDevices: async () => existsSync(emulatorPath)
|
|
124
|
+
? ((await capture(emulatorPath, ['-list-avds'])) ?? '')
|
|
125
|
+
.split('\n')
|
|
126
|
+
.map((line) => line.trim())
|
|
127
|
+
// AVD names are letters, digits, dots, dashes and underscores;
|
|
128
|
+
// this drops the INFO lines some emulator versions print.
|
|
129
|
+
.filter((line) => /^[\w.-]+$/.test(line))
|
|
130
|
+
: [],
|
|
131
|
+
});
|
|
132
|
+
let emulator;
|
|
133
|
+
if ('avd' in choice) {
|
|
134
|
+
log(`Starting ${choice.label}`);
|
|
135
|
+
emulator = startEmulator(emulatorPath, choice.avd, logFile);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
log(`Running the app on ${choice.label}`);
|
|
139
|
+
}
|
|
140
|
+
const emulatorLog = emulatorLogFile(logFile);
|
|
141
|
+
return {
|
|
142
|
+
launch: async (binary, app) => {
|
|
143
|
+
const serials = new Set(before.map((candidate) => candidate.serial));
|
|
144
|
+
let serial = 'serial' in choice ? choice.serial : undefined;
|
|
145
|
+
// An emulator shows up in `adb devices` a few seconds after it starts,
|
|
146
|
+
// and answers `sys.boot_completed` once Android has started, which
|
|
147
|
+
// takes from seconds (from a snapshot) to minutes (a cold boot).
|
|
148
|
+
const deadline = Date.now() + timing.timeoutMs;
|
|
149
|
+
for (;;) {
|
|
150
|
+
const stopped = emulator?.stopped();
|
|
151
|
+
if (stopped) {
|
|
152
|
+
throw new NativeBuildError(`The emulator stopped while starting (${stopped}). Its output is in ${emulatorLog}.`);
|
|
153
|
+
}
|
|
154
|
+
serial ??= (await listDevices()).find((candidate) => isEmulator(candidate) && !serials.has(candidate.serial))?.serial;
|
|
155
|
+
const bootCompleted = serial &&
|
|
156
|
+
(await capture(adbPath, [
|
|
157
|
+
'-s',
|
|
158
|
+
serial,
|
|
159
|
+
'shell',
|
|
160
|
+
'getprop',
|
|
161
|
+
'sys.boot_completed',
|
|
162
|
+
]));
|
|
163
|
+
if (bootCompleted && bootCompleted.trim() === '1') {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (Date.now() > deadline) {
|
|
167
|
+
throw new NativeBuildError(`${capitalize(choice.label)} didn't finish starting within ${timing.timeoutMs / 60_000} minutes.${emulator ? ` The emulator's output is in ${emulatorLog}.` : ''}`);
|
|
168
|
+
}
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, timing.pollMs));
|
|
170
|
+
}
|
|
171
|
+
log(`Installing the app on ${choice.label}`);
|
|
172
|
+
await adb(['-s', serial, 'install', '-r', binary]).catch((error) => {
|
|
173
|
+
throw error.message.includes('INSTALL_FAILED_UPDATE_INCOMPATIBLE')
|
|
174
|
+
? new NativeBuildError(`${capitalize(choice.label)} has a copy of ${app.id} signed with a different key, e.g. from before you set up signing. Uninstall it from the device, then build again.`)
|
|
175
|
+
: error;
|
|
176
|
+
});
|
|
177
|
+
// By component, because neither `monkey -p` nor an `am start` by
|
|
178
|
+
// package opened the shell on an API 34 emulator. The launcher
|
|
179
|
+
// activity is looked up rather than assumed, for an ejected project
|
|
180
|
+
// whose activity was renamed.
|
|
181
|
+
const resolved = await capture(adbPath, [
|
|
182
|
+
'-s',
|
|
183
|
+
serial,
|
|
184
|
+
'shell',
|
|
185
|
+
'cmd',
|
|
186
|
+
'package',
|
|
187
|
+
'resolve-activity',
|
|
188
|
+
'--brief',
|
|
189
|
+
'-a',
|
|
190
|
+
'android.intent.action.MAIN',
|
|
191
|
+
'-c',
|
|
192
|
+
'android.intent.category.LAUNCHER',
|
|
193
|
+
app.id,
|
|
194
|
+
]);
|
|
195
|
+
const component = resolved?.trim().split('\n').at(-1)?.trim();
|
|
196
|
+
// -S stops the copy a previous `--run` left open first, as iOS's
|
|
197
|
+
// --terminate-running-process does.
|
|
198
|
+
const output = await adb([
|
|
199
|
+
'-s',
|
|
200
|
+
serial,
|
|
201
|
+
'shell',
|
|
202
|
+
'am',
|
|
203
|
+
'start',
|
|
204
|
+
'-S',
|
|
205
|
+
'-W',
|
|
206
|
+
'-n',
|
|
207
|
+
component?.includes('/') ? component : `${app.id}/${SHELL_ACTIVITY}`,
|
|
208
|
+
]);
|
|
209
|
+
// `am start` exits with 0 even when it fails, and says why on stdout,
|
|
210
|
+
// last: `Error type 3`, then `Error: Activity class ... does not exist.`
|
|
211
|
+
const failure = output
|
|
212
|
+
.split('\n')
|
|
213
|
+
.filter((line) => line.startsWith('Error'))
|
|
214
|
+
.at(-1);
|
|
215
|
+
if (failure) {
|
|
216
|
+
throw new NativeBuildError(`Couldn't open the app: ${failure.replace(/^Error:\s*/, '').trim()} The full log is ${logFile}.`);
|
|
217
|
+
}
|
|
218
|
+
log(`Opened ${app.name} on ${choice.label}`);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
const capitalize = (text) => text[0].toUpperCase() + text.slice(1);
|
|
223
|
+
const emulatorLogFile = (logFile) => path.join(path.dirname(logFile), 'emulator.log');
|
|
224
|
+
/**
|
|
225
|
+
* Starts the emulator in the background, detached, so it keeps running after
|
|
226
|
+
* `ng build` exits and Ctrl+C in the terminal doesn't close it. Its output
|
|
227
|
+
* goes to a log file next to the build's, read if it stops while starting.
|
|
228
|
+
*/
|
|
229
|
+
const startEmulator = (emulatorPath, avd, logFile) => {
|
|
230
|
+
const output = openSync(emulatorLogFile(logFile), 'w');
|
|
231
|
+
const child = spawn(emulatorPath, ['-avd', avd], {
|
|
232
|
+
detached: true,
|
|
233
|
+
stdio: ['ignore', output, output],
|
|
234
|
+
});
|
|
235
|
+
closeSync(output);
|
|
236
|
+
let stopped;
|
|
237
|
+
child.on('error', (error) => (stopped = error.message));
|
|
238
|
+
child.on('exit', (code, signal) => (stopped = `exit code ${code ?? signal}`));
|
|
239
|
+
child.unref();
|
|
240
|
+
return { stopped: () => stopped };
|
|
241
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Platform } from './generate.ts';
|
|
2
2
|
import { type NativeAppOptions } from './options.ts';
|
|
3
|
+
import type { RunOptions } from './run.ts';
|
|
3
4
|
export type { Platform } from './generate.ts';
|
|
4
5
|
export { EJECT_MARKER, copyBundle, prepareProject } from './generate.ts';
|
|
5
6
|
export { renderEjectedProject } from './eject.ts';
|
|
@@ -7,6 +8,7 @@ export { LYNX_IOS, FRAMEWORKS_ENV } from './ios/frameworks.ts';
|
|
|
7
8
|
export type { AndroidOptions, IosOptions, NativeAppOptions, Orientation, Permission, ResolvedApp, } from './options.ts';
|
|
8
9
|
export { NativeConfigError, resolveAppOptions } from './options.ts';
|
|
9
10
|
export { NativeBuildError } from './process.ts';
|
|
11
|
+
export type { RunOptions } from './run.ts';
|
|
10
12
|
export declare const PACKAGE_VERSION: string;
|
|
11
13
|
export type BuildNativeAppOptions = {
|
|
12
14
|
readonly platform: Platform;
|
|
@@ -24,14 +26,20 @@ export type BuildNativeAppOptions = {
|
|
|
24
26
|
readonly packageVersion?: string;
|
|
25
27
|
/** The rspeedy output folder holding main.lynx.bundle. */
|
|
26
28
|
readonly bundleDir: string;
|
|
27
|
-
/** Where the binaries go, e.g. dist/ios. */
|
|
29
|
+
/** Where the binaries go, e.g. dist/ios. Emptied before they're written. */
|
|
28
30
|
readonly outputDir: string;
|
|
29
31
|
readonly app: NativeAppOptions;
|
|
32
|
+
/**
|
|
33
|
+
* Install the app on a simulator, emulator or phone and open it once it's
|
|
34
|
+
* built: `ng build --platform <platform> --run`.
|
|
35
|
+
*/
|
|
36
|
+
readonly run?: RunOptions;
|
|
30
37
|
readonly log: (message: string) => void;
|
|
31
38
|
};
|
|
32
39
|
/**
|
|
33
40
|
* Turns a built Lynx bundle into native binaries: generates (or reuses) the
|
|
34
|
-
* shell project, copies the bundle in, and runs xcodebuild or Gradle.
|
|
41
|
+
* shell project, copies the bundle in, and runs xcodebuild or Gradle. With
|
|
42
|
+
* `run`, then installs the app on a device and opens it.
|
|
35
43
|
* Returns the paths of the binaries it wrote to `outputDir`.
|
|
36
44
|
*/
|
|
37
45
|
export declare const buildNativeApp: (options: BuildNativeAppOptions) => Promise<{
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,KAAK,QAAQ,EAGd,MAAM,eAAe,CAAC;AAKvB,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,cAAc,CAAC;AACxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC/D,YAAY,EACV,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,WAAW,EACX,UAAU,EACV,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,eAAO,MAAM,eAAe,EAAE,MAErB,CAAC;AAEV,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,gBAAgB,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GACzB,SAAS,qBAAqB,KAC7B,OAAO,CAAC;IACT,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B,CA+EA,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { androidGenerator } from "./android/project.js";
|
|
4
4
|
import { buildAndroid, resolveAndroidSigning } from "./android/build.js";
|
|
5
|
+
import { startAndroidDevice } from "./android/run.js";
|
|
5
6
|
import { prepareProject, removeLegacyWorkDir, } from "./generate.js";
|
|
6
7
|
import { buildIos, resolveIosSigning } from "./ios/build.js";
|
|
7
8
|
import { resolveLynxFrameworks } from "./ios/frameworks.js";
|
|
8
9
|
import { iosGenerator } from "./ios/project.js";
|
|
10
|
+
import { startIosSimulator } from "./ios/run.js";
|
|
9
11
|
import { resolveAppOptions } from "./options.js";
|
|
10
12
|
export { EJECT_MARKER, copyBundle, prepareProject } from "./generate.js";
|
|
11
13
|
export { renderEjectedProject } from "./eject.js";
|
|
@@ -15,7 +17,8 @@ export { NativeBuildError } from "./process.js";
|
|
|
15
17
|
export const PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
16
18
|
/**
|
|
17
19
|
* Turns a built Lynx bundle into native binaries: generates (or reuses) the
|
|
18
|
-
* shell project, copies the bundle in, and runs xcodebuild or Gradle.
|
|
20
|
+
* shell project, copies the bundle in, and runs xcodebuild or Gradle. With
|
|
21
|
+
* `run`, then installs the app on a device and opens it.
|
|
19
22
|
* Returns the paths of the binaries it wrote to `outputDir`.
|
|
20
23
|
*/
|
|
21
24
|
export const buildNativeApp = async (options) => {
|
|
@@ -35,6 +38,11 @@ export const buildNativeApp = async (options) => {
|
|
|
35
38
|
const logFile = path.join(workDir, 'logs', `${platform}.log`);
|
|
36
39
|
mkdirSync(path.dirname(logFile), { recursive: true });
|
|
37
40
|
writeFileSync(logFile, '');
|
|
41
|
+
const target = !options.run
|
|
42
|
+
? undefined
|
|
43
|
+
: platform === 'ios'
|
|
44
|
+
? await startIosSimulator({ device: options.run.device, logFile, log })
|
|
45
|
+
: await startAndroidDevice({ device: options.run.device, logFile, log });
|
|
38
46
|
const project = prepareProject({
|
|
39
47
|
platform,
|
|
40
48
|
generator: platform === 'ios' ? iosGenerator : androidGenerator,
|
|
@@ -49,6 +57,11 @@ export const buildNativeApp = async (options) => {
|
|
|
49
57
|
: project.regenerated
|
|
50
58
|
? `Generated the ${platform} project`
|
|
51
59
|
: `Reusing the generated ${platform} project (no config changes)`);
|
|
60
|
+
// rspeedy's clean leaves dist/ios and dist/android alone, so building one
|
|
61
|
+
// platform doesn't delete the other's app. Emptying this platform's folder
|
|
62
|
+
// here keeps what an earlier build wrote from outliving it: an .ipa from a
|
|
63
|
+
// signed build, or the app under its old name after a rename.
|
|
64
|
+
rmSync(options.outputDir, { recursive: true, force: true });
|
|
52
65
|
const artifacts = platform === 'ios'
|
|
53
66
|
? await buildIos({
|
|
54
67
|
project,
|
|
@@ -68,5 +81,8 @@ export const buildNativeApp = async (options) => {
|
|
|
68
81
|
log,
|
|
69
82
|
signing: androidSigning,
|
|
70
83
|
});
|
|
84
|
+
await target?.launch(
|
|
85
|
+
// The simulator .app or the .apk, not the .ipa or .aab for the stores.
|
|
86
|
+
artifacts.find((artifact) => artifact.endsWith(platform === 'ios' ? '.app' : '.apk')), app);
|
|
71
87
|
return { artifacts, projectDir: project.dir };
|
|
72
88
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RunTarget } from '../run.ts';
|
|
2
|
+
/** What `xcrun simctl list --json devices runtimes` prints, as far as it's read here. */
|
|
3
|
+
export type SimctlList = {
|
|
4
|
+
readonly devices: Readonly<Record<string, readonly SimctlDevice[]>>;
|
|
5
|
+
readonly runtimes: readonly SimctlRuntime[];
|
|
6
|
+
};
|
|
7
|
+
type SimctlDevice = {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly udid: string;
|
|
10
|
+
/** `Shutdown`, `Booting`, `Booted` or `Shutting Down`. */
|
|
11
|
+
readonly state: string;
|
|
12
|
+
readonly isAvailable: boolean;
|
|
13
|
+
readonly deviceTypeIdentifier: string;
|
|
14
|
+
readonly lastUsedAt?: string;
|
|
15
|
+
};
|
|
16
|
+
type SimctlRuntime = {
|
|
17
|
+
readonly identifier: string;
|
|
18
|
+
/** e.g. `iOS 27.0`. */
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly version: string;
|
|
21
|
+
readonly platform?: string;
|
|
22
|
+
readonly isAvailable: boolean;
|
|
23
|
+
/** Newest first. */
|
|
24
|
+
readonly supportedDeviceTypes?: readonly {
|
|
25
|
+
readonly identifier: string;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly productFamily?: string;
|
|
28
|
+
}[];
|
|
29
|
+
};
|
|
30
|
+
/** The simulator to run on: an existing one, or one to create. */
|
|
31
|
+
export type Simulator = {
|
|
32
|
+
/** Undefined when the simulator has to be created first. */
|
|
33
|
+
readonly udid?: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly deviceTypeIdentifier: string;
|
|
36
|
+
readonly runtime: SimctlRuntime;
|
|
37
|
+
readonly booted: boolean;
|
|
38
|
+
readonly lastUsedAt?: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Chooses the simulator `ng build --platform ios --run` opens the app on.
|
|
42
|
+
*
|
|
43
|
+
* With `--device`, the simulator of that name or UDID. Otherwise one that's
|
|
44
|
+
* already started, so the app opens where the developer is looking, and the
|
|
45
|
+
* most recently used one if several are. Otherwise the newest iPhone on the
|
|
46
|
+
* newest iOS runtime, preferring a simulator that exists (Xcode creates one
|
|
47
|
+
* of each iPhone) over creating one.
|
|
48
|
+
*/
|
|
49
|
+
export declare const pickSimulator: (list: SimctlList, device: string | undefined) => Simulator;
|
|
50
|
+
/**
|
|
51
|
+
* Picks the simulator for `--run` and starts booting it, so it boots while
|
|
52
|
+
* xcodebuild compiles. The returned target installs and opens the app once
|
|
53
|
+
* it's built.
|
|
54
|
+
*/
|
|
55
|
+
export declare const startIosSimulator: ({ device, logFile, log, }: {
|
|
56
|
+
readonly device: string | undefined;
|
|
57
|
+
readonly logFile: string;
|
|
58
|
+
readonly log: (message: string) => void;
|
|
59
|
+
}) => Promise<RunTarget>;
|
|
60
|
+
export {};
|
|
61
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/ios/run.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,yFAAyF;AACzF,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC,CAAC,CAAC;IACpE,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CAC7C,CAAC;AAEF,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uBAAuB;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,oBAAoB;IACpB,QAAQ,CAAC,oBAAoB,CAAC,EAAE,SAAS;QACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;KACjC,EAAE,CAAC;CACL,CAAC;AAEF,kEAAkE;AAClE,MAAM,MAAM,SAAS,GAAG;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,UAAU,EAChB,QAAQ,MAAM,GAAG,SAAS,KACzB,SAkFF,CAAC;AAsBF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAU,2BAIrC;IACD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC,KAAG,OAAO,CAAC,SAAS,CA0DpB,CAAC"}
|