@blotch/angular-lynx-native 0.0.0-stage → 22.2.2
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 +39 -2
- package/dist/android/build.d.ts +28 -0
- package/dist/android/build.d.ts.map +1 -0
- package/dist/android/build.js +100 -0
- package/dist/android/project.d.ts +20 -0
- package/dist/android/project.d.ts.map +1 -0
- package/dist/android/project.js +84 -0
- package/dist/eject.d.ts +18 -0
- package/dist/eject.d.ts.map +1 -0
- package/dist/eject.js +68 -0
- package/dist/generate.d.ts +60 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +111 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/ios/build.d.ts +32 -0
- package/dist/ios/build.d.ts.map +1 -0
- package/dist/ios/build.js +149 -0
- package/dist/ios/frameworks.d.ts +31 -0
- package/dist/ios/frameworks.d.ts.map +1 -0
- package/dist/ios/frameworks.js +99 -0
- package/dist/ios/project.d.ts +11 -0
- package/dist/ios/project.d.ts.map +1 -0
- package/dist/ios/project.js +73 -0
- package/dist/options.d.ts +95 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +112 -0
- package/dist/permissions.d.ts +17 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +38 -0
- package/dist/png.d.ts +10 -0
- package/dist/png.d.ts.map +1 -0
- package/dist/png.js +24 -0
- package/dist/process.d.ts +17 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +67 -0
- package/dist/xml.d.ts +10 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +32 -0
- package/package.json +59 -4
- package/templates/android/README.md +42 -0
- package/templates/android/angular-lynx.properties +7 -0
- package/templates/android/app/build.gradle.kts +140 -0
- package/templates/android/app/proguard-rules.pro +65 -0
- package/templates/android/app/src/main/AndroidManifest.xml +36 -0
- package/templates/android/app/src/main/assets/.gitkeep +0 -0
- package/templates/android/app/src/main/java/dev/angularlynx/shell/BundleAssets.kt +45 -0
- package/templates/android/app/src/main/java/dev/angularlynx/shell/MainActivity.kt +96 -0
- package/templates/android/app/src/main/java/dev/angularlynx/shell/ResourceFetchers.kt +115 -0
- package/templates/android/app/src/main/java/dev/angularlynx/shell/ShellApplication.kt +39 -0
- package/templates/android/app/src/main/java/dev/angularlynx/shell/ShellModule.kt +40 -0
- package/templates/android/app/src/main/res/drawable/ic_launcher_background.xml +4 -0
- package/templates/android/app/src/main/res/drawable/ic_launcher_foreground.xml +10 -0
- package/templates/android/app/src/main/res/mipmap/ic_launcher.xml +6 -0
- package/templates/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- package/templates/android/app/src/main/res/values/themes.xml +13 -0
- package/templates/android/app/src/main/res/values-night/themes.xml +8 -0
- package/templates/android/build.gradle.kts +5 -0
- package/templates/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/android/gradle/wrapper/gradle-wrapper.properties +10 -0
- package/templates/android/gradle.properties +7 -0
- package/templates/android/gradlew +248 -0
- package/templates/android/gradlew.bat +82 -0
- package/templates/android/settings.gradle.kts +28 -0
- package/templates/ios/App/AppDelegate.swift +51 -0
- package/templates/ios/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
- package/templates/ios/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/templates/ios/App/Assets.xcassets/Contents.json +6 -0
- package/templates/ios/App/Assets.xcassets/LaunchBackground.colorset/Contents.json +20 -0
- package/templates/ios/App/BundleResourceFetcher.swift +136 -0
- package/templates/ios/App/Info.plist +48 -0
- package/templates/ios/App/LynxViewController.swift +68 -0
- package/templates/ios/App.xcodeproj/project.pbxproj +328 -0
- package/templates/ios/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/templates/ios/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +76 -0
- package/templates/ios/Config/App.xcconfig +9 -0
- package/templates/ios/LynxBundle/.gitkeep +0 -0
package/dist/options.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export class NativeConfigError extends Error {
|
|
2
|
+
}
|
|
3
|
+
/**
|
|
4
|
+
* Letters and digits in dot-separated segments that each start with a letter:
|
|
5
|
+
* the intersection of what iOS (no underscores) and Android (no hyphens)
|
|
6
|
+
* accept, so one ID works on both.
|
|
7
|
+
*/
|
|
8
|
+
const APP_ID = /^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*)+$/;
|
|
9
|
+
/** Up to three period-separated integers, as App Store Connect requires. */
|
|
10
|
+
const VERSION = /^\d+(\.\d+){0,2}$/;
|
|
11
|
+
const SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/;
|
|
12
|
+
/** Google Play's maximum versionCode. */
|
|
13
|
+
const MAX_BUILD_NUMBER = 2_100_000_000;
|
|
14
|
+
const PERMISSIONS = [
|
|
15
|
+
'camera',
|
|
16
|
+
'microphone',
|
|
17
|
+
'location',
|
|
18
|
+
'photos',
|
|
19
|
+
'contacts',
|
|
20
|
+
'notifications',
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Applies defaults and validates the options, so the generators never see a
|
|
24
|
+
* value that would produce a project Xcode or Gradle rejects later with a
|
|
25
|
+
* less helpful message.
|
|
26
|
+
*/
|
|
27
|
+
export const resolveAppOptions = (rawOptions, defaults) => {
|
|
28
|
+
const options = (compact(rawOptions) ?? {});
|
|
29
|
+
const name = (options.appName ?? defaults.projectName).trim();
|
|
30
|
+
if (!name) {
|
|
31
|
+
throw new NativeConfigError('appName must not be empty.');
|
|
32
|
+
}
|
|
33
|
+
const id = options.appId ?? `com.example.${toIdSegment(defaults.projectName)}`;
|
|
34
|
+
if (!APP_ID.test(id)) {
|
|
35
|
+
throw new NativeConfigError(`appId "${id}" is not valid. Use letters and digits in at least two dot-separated parts, each starting with a letter (e.g. com.example.myapp). iOS rejects underscores and Android rejects hyphens.`);
|
|
36
|
+
}
|
|
37
|
+
const packageVersion = defaults.packageVersion && VERSION.test(defaults.packageVersion)
|
|
38
|
+
? defaults.packageVersion
|
|
39
|
+
: undefined;
|
|
40
|
+
// `ng new` sets 0.0.0, which App Store Connect rejects.
|
|
41
|
+
const version = options.version ??
|
|
42
|
+
(packageVersion && packageVersion !== '0.0.0' ? packageVersion : '1.0.0');
|
|
43
|
+
if (!VERSION.test(version)) {
|
|
44
|
+
throw new NativeConfigError(`version "${version}" is not valid. Use up to three period-separated numbers, e.g. 1.2.0.`);
|
|
45
|
+
}
|
|
46
|
+
const buildNumber = options.buildNumber ?? 1;
|
|
47
|
+
if (!Number.isInteger(buildNumber) ||
|
|
48
|
+
buildNumber < 1 ||
|
|
49
|
+
buildNumber > MAX_BUILD_NUMBER) {
|
|
50
|
+
throw new NativeConfigError(`buildNumber ${buildNumber} is not valid. Use a whole number from 1 to ${MAX_BUILD_NUMBER}.`);
|
|
51
|
+
}
|
|
52
|
+
const permissions = options.permissions ?? {};
|
|
53
|
+
for (const [permission, reason] of Object.entries(permissions)) {
|
|
54
|
+
if (!PERMISSIONS.includes(permission)) {
|
|
55
|
+
throw new NativeConfigError(`Unknown permission "${permission}". Supported: ${PERMISSIONS.join(', ')}.`);
|
|
56
|
+
}
|
|
57
|
+
if (typeof reason !== 'string' || !reason.trim()) {
|
|
58
|
+
throw new NativeConfigError(`permissions.${permission} needs the reason shown to the user, e.g. "Scan receipts".`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const deepLinkSchemes = options.deepLinkSchemes ?? [];
|
|
62
|
+
for (const scheme of deepLinkSchemes) {
|
|
63
|
+
if (!SCHEME.test(scheme)) {
|
|
64
|
+
throw new NativeConfigError(`deepLinkSchemes entry "${scheme}" is not a valid URL scheme.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const productName = name.replace(/[^A-Za-z0-9]/g, '') || 'App';
|
|
68
|
+
const artifactName = name
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
71
|
+
.replace(/^-|-$/g, '') || 'app';
|
|
72
|
+
return {
|
|
73
|
+
name,
|
|
74
|
+
id,
|
|
75
|
+
version,
|
|
76
|
+
buildNumber,
|
|
77
|
+
orientation: options.orientation ?? 'portrait',
|
|
78
|
+
permissions,
|
|
79
|
+
deepLinkSchemes,
|
|
80
|
+
icon: options.icon,
|
|
81
|
+
productName,
|
|
82
|
+
artifactName,
|
|
83
|
+
ios: options.ios ?? {},
|
|
84
|
+
android: options.android ?? {},
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* `my-app` → `myapp`: a project name as one app ID segment.
|
|
89
|
+
*/
|
|
90
|
+
const toIdSegment = (projectName) => {
|
|
91
|
+
const segment = projectName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
92
|
+
return /^[a-z]/.test(segment) ? segment : `app${segment}`;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Drops `undefined` values and the objects left empty by that. Angular CLI
|
|
96
|
+
* fills in every property its schema declares for a nested option object,
|
|
97
|
+
* as `undefined` when the user didn't set it, so `ios: {}` arrives as
|
|
98
|
+
* `{ teamId: undefined, signing: { keyIdEnv: undefined, ... } }`, which
|
|
99
|
+
* would read as "signing configured, but incomplete".
|
|
100
|
+
*/
|
|
101
|
+
export const compact = (value) => {
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
return value.map(compact);
|
|
104
|
+
}
|
|
105
|
+
if (!value || typeof value !== 'object') {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
const entries = Object.entries(value)
|
|
109
|
+
.map(([key, entry]) => [key, compact(entry)])
|
|
110
|
+
.filter(([, entry]) => entry !== undefined);
|
|
111
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
112
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Permission } from './options.ts';
|
|
2
|
+
/**
|
|
3
|
+
* What each cross-platform permission becomes on each OS. iOS asks the user
|
|
4
|
+
* with the app's own reason string (an Info.plist usage description); Android
|
|
5
|
+
* declares manifest permissions and shows a system message instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PERMISSION_MAP: Record<Permission, {
|
|
8
|
+
readonly ios: readonly string[];
|
|
9
|
+
readonly android: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* Hardware the Android permission implies. Declared as not required, or
|
|
12
|
+
* Google Play hides the app from devices without it (a tablet without a
|
|
13
|
+
* camera, say), which a permission alone silently causes.
|
|
14
|
+
*/
|
|
15
|
+
readonly androidFeatures?: readonly string[];
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CACjC,UAAU,EACV;IACE,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C,CAiCF,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each cross-platform permission becomes on each OS. iOS asks the user
|
|
3
|
+
* with the app's own reason string (an Info.plist usage description); Android
|
|
4
|
+
* declares manifest permissions and shows a system message instead.
|
|
5
|
+
*/
|
|
6
|
+
export const PERMISSION_MAP = {
|
|
7
|
+
camera: {
|
|
8
|
+
ios: ['NSCameraUsageDescription'],
|
|
9
|
+
android: ['android.permission.CAMERA'],
|
|
10
|
+
androidFeatures: ['android.hardware.camera'],
|
|
11
|
+
},
|
|
12
|
+
microphone: {
|
|
13
|
+
ios: ['NSMicrophoneUsageDescription'],
|
|
14
|
+
android: ['android.permission.RECORD_AUDIO'],
|
|
15
|
+
androidFeatures: ['android.hardware.microphone'],
|
|
16
|
+
},
|
|
17
|
+
location: {
|
|
18
|
+
ios: ['NSLocationWhenInUseUsageDescription'],
|
|
19
|
+
android: [
|
|
20
|
+
'android.permission.ACCESS_COARSE_LOCATION',
|
|
21
|
+
'android.permission.ACCESS_FINE_LOCATION',
|
|
22
|
+
],
|
|
23
|
+
androidFeatures: ['android.hardware.location'],
|
|
24
|
+
},
|
|
25
|
+
photos: {
|
|
26
|
+
ios: ['NSPhotoLibraryUsageDescription'],
|
|
27
|
+
android: ['android.permission.READ_MEDIA_IMAGES'],
|
|
28
|
+
},
|
|
29
|
+
contacts: {
|
|
30
|
+
ios: ['NSContactsUsageDescription'],
|
|
31
|
+
android: ['android.permission.READ_CONTACTS'],
|
|
32
|
+
},
|
|
33
|
+
// iOS asks for notification permission at runtime without a usage string.
|
|
34
|
+
notifications: {
|
|
35
|
+
ios: [],
|
|
36
|
+
android: ['android.permission.POST_NOTIFICATIONS'],
|
|
37
|
+
},
|
|
38
|
+
};
|
package/dist/png.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a PNG's dimensions from its header, without decoding it. Enough to
|
|
3
|
+
* reject an icon of the wrong size before Xcode's asset compiler does, with a
|
|
4
|
+
* clearer message.
|
|
5
|
+
*/
|
|
6
|
+
export declare const readPngSize: (file: string) => {
|
|
7
|
+
readonly width: number;
|
|
8
|
+
readonly height: number;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=png.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"png.d.ts","sourceRoot":"","sources":["../src/png.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,WAAW,GACtB,MAAM,MAAM,KACX;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAiBnD,CAAC"}
|
package/dist/png.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync } from 'node:fs';
|
|
2
|
+
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
3
|
+
/**
|
|
4
|
+
* Reads a PNG's dimensions from its header, without decoding it. Enough to
|
|
5
|
+
* reject an icon of the wrong size before Xcode's asset compiler does, with a
|
|
6
|
+
* clearer message.
|
|
7
|
+
*/
|
|
8
|
+
export const readPngSize = (file) => {
|
|
9
|
+
const header = Buffer.alloc(24);
|
|
10
|
+
const fd = openSync(file, 'r');
|
|
11
|
+
try {
|
|
12
|
+
readSync(fd, header, 0, header.length, 0);
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
closeSync(fd);
|
|
16
|
+
}
|
|
17
|
+
// The IHDR chunk always comes first: 8-byte signature, 4-byte length,
|
|
18
|
+
// "IHDR", then width and height.
|
|
19
|
+
if (!header.subarray(0, 8).equals(SIGNATURE) ||
|
|
20
|
+
header.toString('ascii', 12, 16) !== 'IHDR') {
|
|
21
|
+
throw new Error(`${file} is not a PNG file.`);
|
|
22
|
+
}
|
|
23
|
+
return { width: header.readUInt32BE(16), height: header.readUInt32BE(20) };
|
|
24
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A native build step failed. The message ends with the log's last lines. */
|
|
2
|
+
export declare class NativeBuildError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Runs a native tool with its output going to `logFile` rather than the
|
|
6
|
+
* terminal. xcodebuild and Gradle print thousands of lines on success, which
|
|
7
|
+
* would bury the builder's own progress; on failure, the end of the log is
|
|
8
|
+
* almost always the error, so that's what the thrown error quotes.
|
|
9
|
+
*/
|
|
10
|
+
export declare const run: (command: string, args: readonly string[], options: {
|
|
11
|
+
readonly cwd: string;
|
|
12
|
+
readonly logFile: string;
|
|
13
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
/** Runs a command and returns its stdout, or undefined if it fails. */
|
|
16
|
+
export declare const capture: (command: string, args: readonly string[]) => Promise<string | undefined>;
|
|
17
|
+
//# sourceMappingURL=process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"AAIA,8EAA8E;AAC9E,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAQ9C;;;;;GAKG;AACH,eAAO,MAAM,GAAG,GACd,SAAS,MAAM,EACf,MAAM,SAAS,MAAM,EAAE,EACvB,SAAS;IACP,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,KACA,OAAO,CAAC,IAAI,CAqDd,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,OAAO,GAClB,SAAS,MAAM,EACf,MAAM,SAAS,MAAM,EAAE,KACtB,OAAO,CAAC,MAAM,GAAG,SAAS,CAOzB,CAAC"}
|
package/dist/process.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/** A native build step failed. The message ends with the log's last lines. */
|
|
5
|
+
export class NativeBuildError extends Error {
|
|
6
|
+
}
|
|
7
|
+
/** How many log lines a failure message quotes. */
|
|
8
|
+
const TAIL_LINES = 40;
|
|
9
|
+
const MAX_ERROR_LINES = 20;
|
|
10
|
+
/** clang/swiftc/javac/kotlinc diagnostics and xcodebuild's own errors. */
|
|
11
|
+
const ERROR_LINE = /(^|:\d+(:\d+)?:\s|^\s*e:\s)error:|^error:|^e: /;
|
|
12
|
+
/**
|
|
13
|
+
* Runs a native tool with its output going to `logFile` rather than the
|
|
14
|
+
* terminal. xcodebuild and Gradle print thousands of lines on success, which
|
|
15
|
+
* would bury the builder's own progress; on failure, the end of the log is
|
|
16
|
+
* almost always the error, so that's what the thrown error quotes.
|
|
17
|
+
*/
|
|
18
|
+
export const run = (command, args, options) => {
|
|
19
|
+
mkdirSync(path.dirname(options.logFile), { recursive: true });
|
|
20
|
+
appendFileSync(options.logFile, `\n$ ${[command, ...args].join(' ')}\n (in ${options.cwd})\n`);
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn(command, args, {
|
|
23
|
+
cwd: options.cwd,
|
|
24
|
+
env: options.env ?? process.env,
|
|
25
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
26
|
+
});
|
|
27
|
+
const recent = [];
|
|
28
|
+
const errors = new Set();
|
|
29
|
+
let partial = '';
|
|
30
|
+
const record = (chunk) => {
|
|
31
|
+
appendFileSync(options.logFile, chunk);
|
|
32
|
+
const lines = (partial + chunk.toString()).split('\n');
|
|
33
|
+
partial = lines.pop();
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
// Compiler diagnostics ("file.swift:12:8: error: ...") say what went
|
|
36
|
+
// wrong far better than the end of the log, which for xcodebuild is
|
|
37
|
+
// usually a multi-kilobyte command line.
|
|
38
|
+
if (ERROR_LINE.test(line) && errors.size < MAX_ERROR_LINES) {
|
|
39
|
+
errors.add(line.trim());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
recent.push(...lines);
|
|
43
|
+
recent.splice(0, Math.max(0, recent.length - TAIL_LINES));
|
|
44
|
+
};
|
|
45
|
+
child.stdout.on('data', record);
|
|
46
|
+
child.stderr.on('data', record);
|
|
47
|
+
child.on('error', (error) => reject(new NativeBuildError(`Could not run ${command}: ${error.message}`)));
|
|
48
|
+
child.on('close', (code) => {
|
|
49
|
+
if (code === 0) {
|
|
50
|
+
resolve();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const detail = errors.size > 0
|
|
54
|
+
? `The errors:\n\n${[...errors].join('\n')}`
|
|
55
|
+
: `Its last lines:\n\n${[...recent, partial].join('\n').trim()}`;
|
|
56
|
+
reject(new NativeBuildError(`${command} ${args[0] ?? ''} failed (exit code ${code}). The full log is ${options.logFile}. ${detail}`));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
/** Runs a command and returns its stdout, or undefined if it fails. */
|
|
61
|
+
export const capture = (command, args) => new Promise((resolve) => {
|
|
62
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
63
|
+
let output = '';
|
|
64
|
+
child.stdout.on('data', (chunk) => (output += chunk.toString()));
|
|
65
|
+
child.on('error', () => resolve(undefined));
|
|
66
|
+
child.on('close', (code) => resolve(code === 0 ? output : undefined));
|
|
67
|
+
});
|
package/dist/xml.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Escapes text for an XML element or attribute (plists, manifests). */
|
|
2
|
+
export declare const escapeXml: (text: string) => string;
|
|
3
|
+
/**
|
|
4
|
+
* Replaces the text between `<!-- angular-lynx:<name> -->` and
|
|
5
|
+
* `<!-- /angular-lynx:<name> -->` (or, when the template has a single marker
|
|
6
|
+
* with no closing one, the marker itself). Templates mark the few places the
|
|
7
|
+
* generator writes to, so everything else in them stays plain native code.
|
|
8
|
+
*/
|
|
9
|
+
export declare const replaceMarker: (source: string, name: string, content: string) => string;
|
|
10
|
+
//# sourceMappingURL=xml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../src/xml.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,MAMb,CAAC;AAE7B;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,SAAS,MAAM,KACd,MAkBF,CAAC"}
|
package/dist/xml.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Escapes text for an XML element or attribute (plists, manifests). */
|
|
2
|
+
export const escapeXml = (text) => text
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''');
|
|
8
|
+
/**
|
|
9
|
+
* Replaces the text between `<!-- angular-lynx:<name> -->` and
|
|
10
|
+
* `<!-- /angular-lynx:<name> -->` (or, when the template has a single marker
|
|
11
|
+
* with no closing one, the marker itself). Templates mark the few places the
|
|
12
|
+
* generator writes to, so everything else in them stays plain native code.
|
|
13
|
+
*/
|
|
14
|
+
export const replaceMarker = (source, name, content) => {
|
|
15
|
+
const open = `<!-- angular-lynx:${name} -->`;
|
|
16
|
+
const close = `<!-- /angular-lynx:${name} -->`;
|
|
17
|
+
const start = source.indexOf(open);
|
|
18
|
+
if (start === -1) {
|
|
19
|
+
throw new Error(`The template has no ${open} marker.`);
|
|
20
|
+
}
|
|
21
|
+
const closeAt = source.indexOf(close, start);
|
|
22
|
+
const end = closeAt === -1 ? start + open.length : closeAt + close.length;
|
|
23
|
+
// Indent inserted lines (and the closing marker) like the opening marker.
|
|
24
|
+
const indent = /[ \t]*$/.exec(source.slice(0, start))[0];
|
|
25
|
+
const lines = content
|
|
26
|
+
.split('\n')
|
|
27
|
+
.filter((line) => line.trim())
|
|
28
|
+
.map((line) => `\n${indent}${line}`)
|
|
29
|
+
.join('');
|
|
30
|
+
const closing = closeAt === -1 ? '' : `\n${indent}${close}`;
|
|
31
|
+
return `${source.slice(0, start)}${open}${lines}${closing}${source.slice(end)}`;
|
|
32
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blotch/angular-lynx-native",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "22.2.2",
|
|
4
|
+
"description": "Builds AngularLynx apps into iOS and Android binaries",
|
|
5
|
+
"homepage": "https://github.com/Blotch-Smart-Frames/AngularLynx#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/Blotch-Smart-Frames/AngularLynx/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/Blotch-Smart-Frames/AngularLynx.git",
|
|
13
|
+
"directory": "packages/native"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"templates"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.build.json && vite build",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"test:coverage": "vitest run --coverage",
|
|
34
|
+
"test:ci:coverage": "vitest run --coverage",
|
|
35
|
+
"lint": "oxlint .",
|
|
36
|
+
"lint:fix": "oxlint --fix .",
|
|
37
|
+
"format": "oxfmt --write .",
|
|
38
|
+
"format:check": "oxfmt --check .",
|
|
39
|
+
"cspell": "cspell .",
|
|
40
|
+
"generate:ios-project": "vite build",
|
|
41
|
+
"build:lynx-ios-frameworks": "scripts/build-lynx-ios-frameworks.sh",
|
|
42
|
+
"publish:local": "yalc publish --push"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@bacons/xcode": "1.0.0-alpha.33",
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"typescript": "~6.0.0"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22.12"
|
|
51
|
+
},
|
|
52
|
+
"ng-update": {
|
|
53
|
+
"packageGroup": [
|
|
54
|
+
"@blotch/angular-lynx",
|
|
55
|
+
"@blotch/angular-lynx-build",
|
|
56
|
+
"@blotch/angular-lynx-testing-library",
|
|
57
|
+
"@blotch/rsbuild-plugin-angular-lynx",
|
|
58
|
+
"@blotch/angular-lynx-native"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Android shell template
|
|
2
|
+
|
|
3
|
+
The Android app that `ng build --platform android` builds. The generator copies this folder into `.angular/cache/<Angular version>/<project>/lynx-native/android`, fills in the few files below, and runs Gradle. Everything else is static: it's code we own and test, not something users edit (they can `eject` to get a copy they own).
|
|
4
|
+
|
|
5
|
+
The app is one activity holding one full-screen `LynxView` that runs `main.lynx.bundle`. Angular Router does all navigation inside it.
|
|
6
|
+
|
|
7
|
+
## What the generator changes
|
|
8
|
+
|
|
9
|
+
| What | Where | Notes |
|
|
10
|
+
| --------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| The bundle | `app/src/main/assets/` | The whole `dist/` folder (`main.lynx.bundle`, `static/…`), copied after emptying the folder. Bundled files are read from here. |
|
|
12
|
+
| App identity | `angular-lynx.properties` | Keys: `applicationId`, `versionName`, `versionCode`, `appName`, `screenOrientation` (`portrait`, `sensorLandscape` or `unspecified`). Read by `app/build.gradle.kts`. The committed file holds defaults. |
|
|
13
|
+
| Permissions | `AndroidManifest.xml`, at `<!-- angular-lynx:permissions -->` | `<uses-permission>` lines. `INTERNET` is always declared. |
|
|
14
|
+
| Deep links | `AndroidManifest.xml`, at `<!-- angular-lynx:deep-links -->` | `<intent-filter>`s inside the main activity. |
|
|
15
|
+
| SDK location | `local.properties` | `sdk.dir=…`, unless `ANDROID_HOME` is set. |
|
|
16
|
+
| Release signing | Gradle properties, from the environment | `ORG_GRADLE_PROJECT_angularLynxStoreFile`, `…StorePassword`, `…KeyAlias`, `…KeyPassword` (no dots: dash, `/bin/sh` on Debian and Ubuntu, drops variables whose names aren't shell identifiers before `gradlew` starts Gradle). Never written to disk. Without all four, release builds are signed with the debug key (installable, not uploadable). |
|
|
17
|
+
|
|
18
|
+
Build commands and outputs (no flavors, so the names are fixed):
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
./gradlew :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apk
|
|
22
|
+
./gradlew :app:assembleRelease :app:bundleRelease # app/build/outputs/apk/release/app-release.apk
|
|
23
|
+
# app/build/outputs/bundle/release/app-release.aab
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How the shell loads the app
|
|
27
|
+
|
|
28
|
+
- **Bundles** come from `assets/` (`ResourceFetchers.kt`, `BundleAssets.kt`). A URL without a scheme (`/static/…`, `static/…`), `asset:///…` and `file:///android_asset/…` all mean "the file at that path in `assets/`", so the bundle works with Rspeedy's default asset prefix (`/`) and doesn't need a platform-specific one. Absolute `http(s)` URLs are fetched with OkHttp.
|
|
29
|
+
- **Images** are loaded by Fresco. The media fetcher rewrites bundled image URLs to `asset:///…`, which Fresco reads from the APK.
|
|
30
|
+
- **Lazy routes** need nothing extra: the build plugin keeps each lazy chunk inside `main.lynx.bundle` (see `split-chunks.ts` in the build plugin). Bundles requested by URL at runtime still go through the template resource fetcher.
|
|
31
|
+
|
|
32
|
+
## Back button
|
|
33
|
+
|
|
34
|
+
Back is off by default, so it leaves the app like any root screen. The runtime turns it on while the Router has history by calling `NativeModules.AngularLynxShell.setBackHandlerEnabled(true)` (from the background thread, where `NativeModules` lives). While it's on, a back press sends the global event `angularLynxBack` instead, which the runtime turns into a Router back navigation. The app has to say beforehand because Android decides synchronously, at the start of the gesture, whether an app handles back (predictive back animates differently).
|
|
35
|
+
|
|
36
|
+
## Versions
|
|
37
|
+
|
|
38
|
+
Pinned in `app/build.gradle.kts`, `build.gradle.kts` and `gradle/wrapper/gradle-wrapper.properties`:
|
|
39
|
+
|
|
40
|
+
- Lynx 4.1.0 and PrimJS 4.1.1 (Maven Central), Fresco 2.3.0 (the version Lynx's image service is built against), OkHttp 4.12.
|
|
41
|
+
- AGP 9.4.1 with its built-in Kotlin, Gradle 9.6.1 (AGP 9.4's minimum), JDK 17 or newer.
|
|
42
|
+
- `compileSdk` and `targetSdk` 36, `minSdk` 24. Google Play raises its target API requirement every August; bump `targetSdk` with it.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import java.util.Properties
|
|
2
|
+
|
|
3
|
+
plugins {
|
|
4
|
+
id("com.android.application")
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// App identity comes from angular.json, which the generator writes into
|
|
8
|
+
// angular-lynx.properties. The committed file has defaults so the template
|
|
9
|
+
// also builds on its own.
|
|
10
|
+
val appConfig = Properties().apply {
|
|
11
|
+
rootProject.file("angular-lynx.properties").inputStream().use { load(it) }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
fun appConfig(key: String): String =
|
|
15
|
+
appConfig.getProperty(key) ?: error("angular-lynx.properties has no $key")
|
|
16
|
+
|
|
17
|
+
// Release signing arrives as Gradle properties (the builder sets
|
|
18
|
+
// ORG_GRADLE_PROJECT_angularLynxStoreFile etc. in the environment, which
|
|
19
|
+
// keeps secrets out of command lines and logs) and is never written to disk,
|
|
20
|
+
// so the generated project never holds secrets. The names have no dots:
|
|
21
|
+
// gradlew is a /bin/sh script, and dash (Debian and Ubuntu's /bin/sh) drops
|
|
22
|
+
// environment variables whose names aren't shell identifiers before Gradle
|
|
23
|
+
// starts. Without them, release builds fall back to the debug key: still
|
|
24
|
+
// installable on an emulator, but Google Play rejects them.
|
|
25
|
+
val releaseSigning = listOf("StoreFile", "StorePassword", "KeyAlias", "KeyPassword")
|
|
26
|
+
.associateWith { providers.gradleProperty("angularLynx$it").orNull }
|
|
27
|
+
val hasReleaseSigning = releaseSigning.values.all { !it.isNullOrEmpty() }
|
|
28
|
+
|
|
29
|
+
// One place for the Lynx version, so a Lynx upgrade is a one-line change.
|
|
30
|
+
val lynxVersion = "4.1.0"
|
|
31
|
+
val primjsVersion = "4.1.1"
|
|
32
|
+
// Lynx's image service is written against Fresco 2.3, and Lynx Explorer ships
|
|
33
|
+
// the same version, so stay on it until Lynx moves.
|
|
34
|
+
val frescoVersion = "2.3.0"
|
|
35
|
+
|
|
36
|
+
android {
|
|
37
|
+
// The namespace is the package of the shell's own Kotlin code and R class.
|
|
38
|
+
// It's fixed; only the applicationId (the store identity) is configurable.
|
|
39
|
+
namespace = "dev.angularlynx.shell"
|
|
40
|
+
compileSdk = 36
|
|
41
|
+
|
|
42
|
+
defaultConfig {
|
|
43
|
+
applicationId = appConfig("applicationId")
|
|
44
|
+
// Lynx 4.1's AARs declare minSdk 16, but Lynx Explorer and Lynx's
|
|
45
|
+
// integration guide use 24, the oldest version Lynx tests on.
|
|
46
|
+
minSdk = 24
|
|
47
|
+
// Google Play requires new apps and updates to target a recent API
|
|
48
|
+
// level, raised every August. Bump this with each requirement.
|
|
49
|
+
targetSdk = 36
|
|
50
|
+
versionCode = appConfig("versionCode").toInt()
|
|
51
|
+
versionName = appConfig("versionName")
|
|
52
|
+
resValue("string", "app_name", appConfig("appName"))
|
|
53
|
+
// portrait, sensorLandscape or unspecified (follows the device).
|
|
54
|
+
manifestPlaceholders["screenOrientation"] =
|
|
55
|
+
appConfig.getProperty("screenOrientation") ?: "portrait"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
signingConfigs {
|
|
59
|
+
if (hasReleaseSigning) {
|
|
60
|
+
create("release") {
|
|
61
|
+
storeFile = file(releaseSigning.getValue("StoreFile")!!)
|
|
62
|
+
storePassword = releaseSigning.getValue("StorePassword")
|
|
63
|
+
keyAlias = releaseSigning.getValue("KeyAlias")
|
|
64
|
+
keyPassword = releaseSigning.getValue("KeyPassword")
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
buildTypes {
|
|
70
|
+
release {
|
|
71
|
+
// R8 shrinking is off until the Lynx keep rules in
|
|
72
|
+
// proguard-rules.pro are verified against every AngularLynx
|
|
73
|
+
// feature; Lynx reaches much of its Java code through JNI and
|
|
74
|
+
// reflection, which R8 can't see.
|
|
75
|
+
isMinifyEnabled = false
|
|
76
|
+
proguardFiles(
|
|
77
|
+
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
78
|
+
"proguard-rules.pro",
|
|
79
|
+
)
|
|
80
|
+
signingConfig = if (hasReleaseSigning) {
|
|
81
|
+
signingConfigs.getByName("release")
|
|
82
|
+
} else {
|
|
83
|
+
signingConfigs.getByName("debug")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
compileOptions {
|
|
89
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
90
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
buildFeatures {
|
|
94
|
+
resValues = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
androidResources {
|
|
98
|
+
// The bundle and lazy bundles are read with AssetManager.open(), which
|
|
99
|
+
// works on compressed assets too, but leaving them uncompressed lets
|
|
100
|
+
// the OS map them straight from the APK.
|
|
101
|
+
noCompress += listOf("bundle")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
packaging {
|
|
105
|
+
jniLibs {
|
|
106
|
+
// Store native libraries uncompressed and page-aligned in the APK,
|
|
107
|
+
// which Android loads without extracting them first.
|
|
108
|
+
useLegacyPackaging = false
|
|
109
|
+
// Lynx's and Fresco's libraries ship already stripped. Without
|
|
110
|
+
// this, AGP tries to strip them again, which needs the NDK (and
|
|
111
|
+
// only warns when it's missing), so users would need the NDK for
|
|
112
|
+
// nothing.
|
|
113
|
+
keepDebugSymbols += "**/*.so"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
dependencies {
|
|
119
|
+
implementation("androidx.activity:activity-ktx:1.10.1")
|
|
120
|
+
implementation("androidx.core:core-ktx:1.16.0")
|
|
121
|
+
|
|
122
|
+
// The Lynx engine, its JavaScript runtime (PrimJS) and its JS SDK.
|
|
123
|
+
implementation("org.lynxsdk.lynx:lynx:$lynxVersion")
|
|
124
|
+
implementation("org.lynxsdk.lynx:lynx-jssdk:$lynxVersion")
|
|
125
|
+
implementation("org.lynxsdk.lynx:lynx-trace:$lynxVersion")
|
|
126
|
+
implementation("org.lynxsdk.lynx:primjs:$primjsVersion")
|
|
127
|
+
|
|
128
|
+
// Lynx services: <image> (backed by Fresco), console logging, and
|
|
129
|
+
// fetch()/XMLHttpRequest (backed by OkHttp). The service AARs only
|
|
130
|
+
// compile against Fresco and OkHttp, so the app has to provide them.
|
|
131
|
+
implementation("org.lynxsdk.lynx:lynx-service-image:$lynxVersion")
|
|
132
|
+
implementation("org.lynxsdk.lynx:lynx-service-log:$lynxVersion")
|
|
133
|
+
implementation("org.lynxsdk.lynx:lynx-service-http:$lynxVersion")
|
|
134
|
+
implementation("com.facebook.fresco:fresco:$frescoVersion")
|
|
135
|
+
implementation("com.facebook.fresco:animated-gif:$frescoVersion")
|
|
136
|
+
implementation("com.facebook.fresco:animated-webp:$frescoVersion")
|
|
137
|
+
implementation("com.facebook.fresco:webpsupport:$frescoVersion")
|
|
138
|
+
implementation("com.facebook.fresco:animated-base:$frescoVersion")
|
|
139
|
+
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
|
140
|
+
}
|