@blotch/angular-lynx-native 0.0.0-stage → 22.2.3
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 +44 -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 +38 -0
- package/dist/ios/frameworks.d.ts.map +1 -0
- package/dist/ios/frameworks.js +109 -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 +44 -0
- package/templates/android/angular-lynx.properties +7 -0
- package/templates/android/app/build.gradle.kts +158 -0
- package/templates/android/app/proguard-rules.pro +65 -0
- package/templates/android/app/src/main/AndroidManifest.xml +37 -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 +156 -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/app/src/main/res/xml/network_security_config.xml +15 -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 +52 -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 +108 -0
- package/templates/ios/App/XElementRegistry.swift +51 -0
- package/templates/ios/App.xcodeproj/project.pbxproj +346 -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/README.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blotch/angular-lynx-native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Builds AngularLynx apps into iOS and Android binaries. You don't call it directly: `ng build --platform ios|android|all` (the `@blotch/angular-lynx-build:application` builder) loads it after building the Lynx bundle. It's a dependency of [`@blotch/angular-lynx-build`](../build), which `ng add @blotch/angular-lynx` installs.
|
|
4
|
+
|
|
5
|
+
User documentation: [Publishing your App](../website/docs/guide/building-for-production.mdx). The design and its reasoning: [`investigations/native-app-builds.md`](../../investigations/native-app-builds.md).
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
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
|
+
2. **Copies** `dist/` (without the web bundle) into the project.
|
|
11
|
+
3. **Builds** it with `xcodebuild` or Gradle and copies the binaries to `dist/<platform>/`.
|
|
12
|
+
|
|
13
|
+
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
|
+
## The shells
|
|
16
|
+
|
|
17
|
+
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
|
+
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`, and the safe-area insets `safeAreaTop`, `safeAreaBottom`, `safeAreaLeft` and `safeAreaRight`. Lynx Explorer sends `theme` and `isNotchScreen` too, but no `appLocale`. See [`templates/android/README.md`](templates/android/README.md) for the Android shell's details.
|
|
20
|
+
|
|
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
|
+
|
|
23
|
+
| | iOS | Android |
|
|
24
|
+
| ---------------- | ---------------------------------------------------- | -------------------------------------------------------- |
|
|
25
|
+
| Language | Swift, scene lifecycle | Kotlin |
|
|
26
|
+
| Lynx | 4.1.0, prebuilt XCFrameworks (below) | 4.1.0 AARs from Maven Central |
|
|
27
|
+
| XElement | Linked and registered (`App/XElementRegistry.swift`) | Linked and registered (`XElementBehaviors`) |
|
|
28
|
+
| Toolchain | Xcode 27, iOS 15+ | AGP 9.4, Gradle 9.6, JDK 17+, SDK 36, minSdk 24 |
|
|
29
|
+
| Generated config | `Config/App.xcconfig`, `App/Info.plist` markers | `angular-lynx.properties`, `AndroidManifest.xml` markers |
|
|
30
|
+
|
|
31
|
+
## Lynx for iOS
|
|
32
|
+
|
|
33
|
+
Lynx publishes iOS only as source pods, which take minutes to compile and need CocoaPods (and so Ruby). `scripts/build-lynx-ios-frameworks.sh` compiles them once into XCFrameworks (about 26 MB zipped, with debug symbols in a separate zip): Lynx, PrimJS, XElement and their dependencies. The builder downloads the zip pinned in `src/ios/frameworks.ts` (URL and SHA-256) on first use and caches it in `~/.cache/angular-lynx/lynx-ios/<version>-<build>`.
|
|
34
|
+
|
|
35
|
+
The zip for Lynx 4.1.0 is the [`lynx-ios-4.1.0-2` release](https://github.com/Blotch-Smart-Frames/AngularLynx/releases/tag/lynx-ios-4.1.0-2). The build number counts rebuilds of one Lynx version whose frameworks changed (2 added XElement), and is part of the cache folder, so machines with an older zip download the new one. For a new Lynx version, bump the versions in the script and reset `BUILD` to 1; to change the frameworks of the same version, bump `BUILD` (in the script and in `LYNX_IOS`). Then run the **Lynx iOS frameworks** workflow with `publish` (or the script locally), and pin the new zip's URL and SHA-256. To test a locally built folder before publishing it:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm run build:lynx-ios-frameworks -w packages/native # needs Xcode, and Ruby >= 2.7 with Bundler for CocoaPods
|
|
39
|
+
export ANGULAR_LYNX_IOS_FRAMEWORKS=$PWD/packages/native/.lynx-ios-build/out/LynxFrameworks
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Changing a template
|
|
43
|
+
|
|
44
|
+
- **iOS:** `App/` is a synchronized folder, so adding or removing a Swift file needs no project change. For anything else (a build setting, a framework, a resource folder), edit `APP_PROJECT` in `scripts/generate-ios-project.ts`. `App.xcodeproj` isn't in git: the `iosTemplateProject` Vite plugin in `vite.config.ts` renders it with [`@bacons/xcode`](https://github.com/EvanBacon/xcode), which needs only Node, on every `npm run build` and `npm test`, and the npm package ships the result. Run `npm run generate:ios-project -w packages/native` to render it without the TypeScript build, e.g. to open the template in Xcode.
|
|
45
|
+
- **Both:** run `npm run e2e -- --ios --shell` (and Layer 6 for Android, see the [E2E README](../e2e/README.md)) to build and tap through a fresh app on each platform.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PreparedProject } from '../generate.ts';
|
|
2
|
+
import type { ResolvedApp } from '../options.ts';
|
|
3
|
+
export type AndroidSigning = {
|
|
4
|
+
readonly storeFile: string;
|
|
5
|
+
readonly storePassword: string;
|
|
6
|
+
readonly keyAlias: string;
|
|
7
|
+
readonly keyPassword: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Reads the upload key from the environment variables the options name.
|
|
11
|
+
* Returns undefined when signing isn't configured, in which case the release
|
|
12
|
+
* build is signed with the debug key: installable for testing, but not
|
|
13
|
+
* accepted by Google Play.
|
|
14
|
+
*/
|
|
15
|
+
export declare const resolveAndroidSigning: (app: ResolvedApp, env?: NodeJS.ProcessEnv) => AndroidSigning | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Builds the Android shell into a release `.apk` (for emulators and
|
|
18
|
+
* sideloading) and `.aab` (for Google Play). Returns the artifacts' paths.
|
|
19
|
+
*/
|
|
20
|
+
export declare const buildAndroid: ({ project, app, outputDir, logFile, log, signing, }: {
|
|
21
|
+
readonly project: PreparedProject;
|
|
22
|
+
readonly app: ResolvedApp;
|
|
23
|
+
readonly outputDir: string;
|
|
24
|
+
readonly logFile: string;
|
|
25
|
+
readonly log: (message: string) => void;
|
|
26
|
+
readonly signing: AndroidSigning | undefined;
|
|
27
|
+
}) => Promise<string[]>;
|
|
28
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/android/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAIjD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,KAAK,WAAW,EAChB,MAAK,MAAM,CAAC,UAAwB,KACnC,cAAc,GAAG,SAgCnB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAU,qDAOhC;IACD,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,CAAC;CAC9C,KAAG,OAAO,CAAC,MAAM,EAAE,CAuDnB,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { chmodSync, cpSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { capture, run } from "../process.js";
|
|
4
|
+
import { findAndroidSdk } from "./project.js";
|
|
5
|
+
/**
|
|
6
|
+
* Reads the upload key from the environment variables the options name.
|
|
7
|
+
* Returns undefined when signing isn't configured, in which case the release
|
|
8
|
+
* build is signed with the debug key: installable for testing, but not
|
|
9
|
+
* accepted by Google Play.
|
|
10
|
+
*/
|
|
11
|
+
export const resolveAndroidSigning = (app, env = process.env) => {
|
|
12
|
+
const signing = app.android.signing;
|
|
13
|
+
if (!signing) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const read = (option, name) => {
|
|
17
|
+
if (!name) {
|
|
18
|
+
throw new Error(`android.signing.${option} must name the environment variable that holds it.`);
|
|
19
|
+
}
|
|
20
|
+
const value = env[name];
|
|
21
|
+
if (!value) {
|
|
22
|
+
throw new Error(`android.signing.${option} names ${name}, which isn't set in the environment.`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
if (!signing.keyAlias) {
|
|
27
|
+
throw new Error('android.signing.keyAlias must be set.');
|
|
28
|
+
}
|
|
29
|
+
const storeFile = path.resolve(read('storeFileEnv', signing.storeFileEnv));
|
|
30
|
+
if (!existsSync(storeFile)) {
|
|
31
|
+
throw new Error(`The upload keystore ${storeFile} doesn't exist.`);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
storeFile,
|
|
35
|
+
storePassword: read('storePasswordEnv', signing.storePasswordEnv),
|
|
36
|
+
keyAlias: signing.keyAlias,
|
|
37
|
+
keyPassword: read('keyPasswordEnv', signing.keyPasswordEnv),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Builds the Android shell into a release `.apk` (for emulators and
|
|
42
|
+
* sideloading) and `.aab` (for Google Play). Returns the artifacts' paths.
|
|
43
|
+
*/
|
|
44
|
+
export const buildAndroid = async ({ project, app, outputDir, logFile, log, signing, }) => {
|
|
45
|
+
// Signing values reach Gradle as project properties through
|
|
46
|
+
// ORG_GRADLE_PROJECT_* environment variables: never on the command line,
|
|
47
|
+
// which the process list and our build log would show, and never in a file
|
|
48
|
+
// in the project.
|
|
49
|
+
// Gradle finds the SDK through ANDROID_HOME (or local.properties, which an
|
|
50
|
+
// ejected project may have). Passing it here keeps machine paths out of
|
|
51
|
+
// the project, so an ejected one can be committed and built anywhere.
|
|
52
|
+
const sdk = findAndroidSdk();
|
|
53
|
+
if (!sdk && !existsSync(path.join(project.dir, 'local.properties'))) {
|
|
54
|
+
throw new Error('The Android SDK was not found. Install Android Studio (which installs the SDK), or set ANDROID_HOME to an existing SDK.');
|
|
55
|
+
}
|
|
56
|
+
const env = {
|
|
57
|
+
...process.env,
|
|
58
|
+
...(sdk && { ANDROID_HOME: sdk }),
|
|
59
|
+
...(await javaEnv()),
|
|
60
|
+
// Plain identifiers, no dots: gradlew is a /bin/sh script, and dash
|
|
61
|
+
// (/bin/sh on Debian and Ubuntu) silently drops environment variables
|
|
62
|
+
// whose names aren't valid shell identifiers, which would leave Linux
|
|
63
|
+
// builds signed with the debug key.
|
|
64
|
+
...(signing && {
|
|
65
|
+
ORG_GRADLE_PROJECT_angularLynxStoreFile: signing.storeFile,
|
|
66
|
+
ORG_GRADLE_PROJECT_angularLynxStorePassword: signing.storePassword,
|
|
67
|
+
ORG_GRADLE_PROJECT_angularLynxKeyAlias: signing.keyAlias,
|
|
68
|
+
ORG_GRADLE_PROJECT_angularLynxKeyPassword: signing.keyPassword,
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
if (!signing) {
|
|
72
|
+
log('Signing the release build with the debug key: set android.signing in angular.json for a Google Play upload.');
|
|
73
|
+
}
|
|
74
|
+
log('Building the Android app');
|
|
75
|
+
const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
|
|
76
|
+
if (process.platform !== 'win32') {
|
|
77
|
+
// Angular schematics can't set file modes, so an ejected project's
|
|
78
|
+
// gradlew arrives without its executable bit.
|
|
79
|
+
chmodSync(path.join(project.dir, 'gradlew'), 0o755);
|
|
80
|
+
}
|
|
81
|
+
await run(gradlew, [':app:assembleRelease', ':app:bundleRelease', '--console=plain'], { cwd: project.dir, logFile, env });
|
|
82
|
+
mkdirSync(outputDir, { recursive: true });
|
|
83
|
+
const outputs = path.join(project.dir, 'app/build/outputs');
|
|
84
|
+
const apk = path.join(outputDir, `${app.artifactName}.apk`);
|
|
85
|
+
const aab = path.join(outputDir, `${app.artifactName}.aab`);
|
|
86
|
+
cpSync(path.join(outputs, 'apk/release/app-release.apk'), apk);
|
|
87
|
+
cpSync(path.join(outputs, 'bundle/release/app-release.aab'), aab);
|
|
88
|
+
return [apk, aab];
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Android Gradle Plugin 8 needs JDK 17 or newer. When JAVA_HOME isn't set on
|
|
92
|
+
* macOS, ask java_home for one rather than whatever `java` is on the PATH.
|
|
93
|
+
*/
|
|
94
|
+
const javaEnv = async () => {
|
|
95
|
+
if (process.env['JAVA_HOME'] || process.platform !== 'darwin') {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
const home = (await capture('/usr/libexec/java_home', ['-v', '17+']))?.trim();
|
|
99
|
+
return home ? { JAVA_HOME: home } : {};
|
|
100
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PlatformGenerator } from '../generate.ts';
|
|
2
|
+
import type { ResolvedApp } from '../options.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Writes the app options into the Android template: identity goes in
|
|
5
|
+
* angular-lynx.properties (which app/build.gradle.kts reads), and permissions
|
|
6
|
+
* and deep links go between the manifest's `angular-lynx:*` markers.
|
|
7
|
+
*/
|
|
8
|
+
export declare const androidGenerator: PlatformGenerator;
|
|
9
|
+
/**
|
|
10
|
+
* Escapes a value for a Java properties file. `Properties.load` reads the
|
|
11
|
+
* file as ISO-8859-1, so anything outside ASCII (an app named "Café" or
|
|
12
|
+
* "日記") must be written as `\uXXXX` or Gradle reads mojibake.
|
|
13
|
+
*/
|
|
14
|
+
export declare const propertyValue: (value: string) => string;
|
|
15
|
+
export declare const properties: (app: ResolvedApp) => string;
|
|
16
|
+
export declare const permissionEntries: (app: ResolvedApp) => string;
|
|
17
|
+
export declare const deepLinkEntries: (app: ResolvedApp) => string;
|
|
18
|
+
/** The Android SDK, from the environment or the Android Studio default. */
|
|
19
|
+
export declare const findAndroidSdk: (env?: NodeJS.ProcessEnv) => string | undefined;
|
|
20
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/android/project.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAe,WAAW,EAAE,MAAM,eAAe,CAAC;AAY9D;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,iBAc9B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,WAOtC,CAAC;AAEN,eAAO,MAAM,UAAU,GAAI,KAAK,WAAW,KAAG,MAWhC,CAAC;AAEf,eAAO,MAAM,iBAAiB,GAAI,KAAK,WAAW,KAAG,MAiBpD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,WAAW,KAAG,MAYjC,CAAC;AAEnB,2EAA2E;AAC3E,eAAO,MAAM,cAAc,GACzB,MAAK,MAAM,CAAC,UAAwB,KACnC,MAAM,GAAG,SAaX,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { PERMISSION_MAP } from "../permissions.js";
|
|
5
|
+
import { escapeXml, replaceMarker } from "../xml.js";
|
|
6
|
+
const MANIFEST = 'app/src/main/AndroidManifest.xml';
|
|
7
|
+
const SCREEN_ORIENTATIONS = {
|
|
8
|
+
portrait: 'portrait',
|
|
9
|
+
landscape: 'sensorLandscape',
|
|
10
|
+
all: 'unspecified',
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Writes the app options into the Android template: identity goes in
|
|
14
|
+
* angular-lynx.properties (which app/build.gradle.kts reads), and permissions
|
|
15
|
+
* and deep links go between the manifest's `angular-lynx:*` markers.
|
|
16
|
+
*/
|
|
17
|
+
export const androidGenerator = {
|
|
18
|
+
configure: (dir, app) => {
|
|
19
|
+
// npm keeps file modes when it packs, but a copy through another tool
|
|
20
|
+
// (or a Windows checkout) may not, and Gradle can't start without it.
|
|
21
|
+
chmodSync(path.join(dir, 'gradlew'), 0o755);
|
|
22
|
+
writeFileSync(path.join(dir, 'angular-lynx.properties'), properties(app));
|
|
23
|
+
const manifestPath = path.join(dir, MANIFEST);
|
|
24
|
+
let manifest = readFileSync(manifestPath, 'utf8');
|
|
25
|
+
manifest = replaceMarker(manifest, 'permissions', permissionEntries(app));
|
|
26
|
+
manifest = replaceMarker(manifest, 'deep-links', deepLinkEntries(app));
|
|
27
|
+
writeFileSync(manifestPath, manifest);
|
|
28
|
+
},
|
|
29
|
+
bundleDir: (dir) => path.join(dir, 'app/src/main/assets'),
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Escapes a value for a Java properties file. `Properties.load` reads the
|
|
33
|
+
* file as ISO-8859-1, so anything outside ASCII (an app named "Café" or
|
|
34
|
+
* "日記") must be written as `\uXXXX` or Gradle reads mojibake.
|
|
35
|
+
*/
|
|
36
|
+
export const propertyValue = (value) => value
|
|
37
|
+
.replace(/\\/g, '\\\\')
|
|
38
|
+
.replace(/\n/g, '\\n')
|
|
39
|
+
.replace(/[^\x20-\x7e]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
40
|
+
export const properties = (app) => [
|
|
41
|
+
'# Written by @blotch/angular-lynx-native from the builder options in',
|
|
42
|
+
'# angular.json on every build. Edits here are overwritten; change',
|
|
43
|
+
'# angular.json instead (or eject).',
|
|
44
|
+
`applicationId=${app.id}`,
|
|
45
|
+
`versionName=${app.version}`,
|
|
46
|
+
`versionCode=${app.buildNumber}`,
|
|
47
|
+
`appName=${propertyValue(app.name)}`,
|
|
48
|
+
`screenOrientation=${SCREEN_ORIENTATIONS[app.orientation]}`,
|
|
49
|
+
'',
|
|
50
|
+
].join('\n');
|
|
51
|
+
export const permissionEntries = (app) => {
|
|
52
|
+
const entries = Object.keys(app.permissions).map((permission) => PERMISSION_MAP[permission]);
|
|
53
|
+
const permissions = [...new Set(entries.flatMap((entry) => entry.android))];
|
|
54
|
+
const features = [
|
|
55
|
+
...new Set(entries.flatMap((entry) => entry.androidFeatures ?? [])),
|
|
56
|
+
];
|
|
57
|
+
return [
|
|
58
|
+
...permissions.map((name) => `<uses-permission android:name="${escapeXml(name)}" />`),
|
|
59
|
+
...features.map((name) => `<uses-feature android:name="${escapeXml(name)}" android:required="false" />`),
|
|
60
|
+
].join('\n');
|
|
61
|
+
};
|
|
62
|
+
export const deepLinkEntries = (app) => app.deepLinkSchemes.length === 0
|
|
63
|
+
? ''
|
|
64
|
+
: [
|
|
65
|
+
'<intent-filter>',
|
|
66
|
+
' <action android:name="android.intent.action.VIEW" />',
|
|
67
|
+
' <category android:name="android.intent.category.DEFAULT" />',
|
|
68
|
+
' <category android:name="android.intent.category.BROWSABLE" />',
|
|
69
|
+
...app.deepLinkSchemes.map((scheme) => ` <data android:scheme="${escapeXml(scheme)}" />`),
|
|
70
|
+
'</intent-filter>',
|
|
71
|
+
].join('\n');
|
|
72
|
+
/** The Android SDK, from the environment or the Android Studio default. */
|
|
73
|
+
export const findAndroidSdk = (env = process.env) => {
|
|
74
|
+
const candidates = [
|
|
75
|
+
env['ANDROID_HOME'],
|
|
76
|
+
env['ANDROID_SDK_ROOT'],
|
|
77
|
+
process.platform === 'darwin'
|
|
78
|
+
? path.join(os.homedir(), 'Library/Android/sdk')
|
|
79
|
+
: process.platform === 'win32'
|
|
80
|
+
? path.join(env['LOCALAPPDATA'] ?? '', 'Android/Sdk')
|
|
81
|
+
: path.join(os.homedir(), 'Android/Sdk'),
|
|
82
|
+
];
|
|
83
|
+
return candidates.find((candidate) => !!candidate && existsSync(candidate));
|
|
84
|
+
};
|
package/dist/eject.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Platform } from './generate.ts';
|
|
2
|
+
import { type NativeAppOptions } from './options.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Renders the native project a user takes ownership of with
|
|
5
|
+
* `ng generate @blotch/angular-lynx:eject`: the same project the builder
|
|
6
|
+
* generates, plus the marker that tells the builder to build it as it is.
|
|
7
|
+
* Returns its files (relative path → contents), for the schematic to write.
|
|
8
|
+
*/
|
|
9
|
+
export declare const renderEjectedProject: ({ platform, projectRoot, projectName, packageVersion, app, generatorVersion, }: {
|
|
10
|
+
readonly platform: Platform;
|
|
11
|
+
readonly projectRoot: string;
|
|
12
|
+
readonly projectName: string;
|
|
13
|
+
readonly packageVersion?: string;
|
|
14
|
+
readonly app: NativeAppOptions;
|
|
15
|
+
/** This package's version, recorded in the marker for later upgrades. */
|
|
16
|
+
readonly generatorVersion: string;
|
|
17
|
+
}) => Map<string, Buffer>;
|
|
18
|
+
//# sourceMappingURL=eject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eject.d.ts","sourceRoot":"","sources":["../src/eject.ts"],"names":[],"mappings":"AAYA,OAAO,EAAgB,KAAK,QAAQ,EAAiB,MAAM,eAAe,CAAC;AAE3E,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,cAAc,CAAC;AA+BxE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,gFAOlC;IACD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,gBAAgB,CAAC;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CA8BrB,CAAC"}
|
package/dist/eject.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { cpSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { androidGenerator } from "./android/project.js";
|
|
5
|
+
import { EJECT_MARKER, TEMPLATES_DIR } from "./generate.js";
|
|
6
|
+
import { iosGenerator } from "./ios/project.js";
|
|
7
|
+
import { resolveAppOptions } from "./options.js";
|
|
8
|
+
/**
|
|
9
|
+
* What an ejected project ignores. Written here because npm never packs
|
|
10
|
+
* `.gitignore` files, so the templates can't carry their own.
|
|
11
|
+
*/
|
|
12
|
+
const GITIGNORE = {
|
|
13
|
+
ios: [
|
|
14
|
+
'# Copied from dist/ on every `ng build --platform ios`.',
|
|
15
|
+
'LynxBundle/*',
|
|
16
|
+
'!LynxBundle/.gitkeep',
|
|
17
|
+
'# A link to the cached Lynx XCFrameworks, made on every build.',
|
|
18
|
+
'Frameworks',
|
|
19
|
+
'xcuserdata/',
|
|
20
|
+
'*.xcuserstate',
|
|
21
|
+
'',
|
|
22
|
+
].join('\n'),
|
|
23
|
+
android: [
|
|
24
|
+
'# Copied from dist/ on every `ng build --platform android`.',
|
|
25
|
+
'app/src/main/assets/*',
|
|
26
|
+
'!app/src/main/assets/.gitkeep',
|
|
27
|
+
'.gradle/',
|
|
28
|
+
'build/',
|
|
29
|
+
'app/build/',
|
|
30
|
+
'local.properties',
|
|
31
|
+
'.idea/',
|
|
32
|
+
'*.iml',
|
|
33
|
+
'',
|
|
34
|
+
].join('\n'),
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Renders the native project a user takes ownership of with
|
|
38
|
+
* `ng generate @blotch/angular-lynx:eject`: the same project the builder
|
|
39
|
+
* generates, plus the marker that tells the builder to build it as it is.
|
|
40
|
+
* Returns its files (relative path → contents), for the schematic to write.
|
|
41
|
+
*/
|
|
42
|
+
export const renderEjectedProject = ({ platform, projectRoot, projectName, packageVersion, app, generatorVersion, }) => {
|
|
43
|
+
const resolved = resolveAppOptions(app, { projectName, packageVersion });
|
|
44
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), `angular-lynx-eject-`));
|
|
45
|
+
try {
|
|
46
|
+
cpSync(path.join(TEMPLATES_DIR, platform), dir, { recursive: true });
|
|
47
|
+
(platform === 'ios' ? iosGenerator : androidGenerator).configure(dir, resolved, projectRoot);
|
|
48
|
+
writeFileSync(path.join(dir, '.gitignore'), GITIGNORE[platform]);
|
|
49
|
+
writeFileSync(path.join(dir, EJECT_MARKER), [
|
|
50
|
+
`Ejected from @blotch/angular-lynx-native ${generatorVersion}.`,
|
|
51
|
+
'',
|
|
52
|
+
`\`ng build --platform ${platform}\` builds this folder as it is and never`,
|
|
53
|
+
'regenerates it, so changes here are yours to keep. App options in',
|
|
54
|
+
'angular.json no longer apply to it, and Lynx, Xcode and Google Play',
|
|
55
|
+
'upgrades are now made here by hand. Delete this folder (and this file',
|
|
56
|
+
'with it) to go back to the generated project.',
|
|
57
|
+
'',
|
|
58
|
+
].join('\n'));
|
|
59
|
+
return new Map(listFiles(dir).map((file) => [file, readFileSync(path.join(dir, file))]));
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
rmSync(dir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const listFiles = (dir, prefix = '') => readdirSync(path.join(dir, prefix), { withFileTypes: true }).flatMap((entry) => {
|
|
66
|
+
const relative = path.posix.join(prefix, entry.name);
|
|
67
|
+
return entry.isDirectory() ? listFiles(dir, relative) : [relative];
|
|
68
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ResolvedApp } from './options.ts';
|
|
2
|
+
export type Platform = 'ios' | 'android';
|
|
3
|
+
/** The shell templates shipped in this package. */
|
|
4
|
+
export declare const TEMPLATES_DIR: string;
|
|
5
|
+
/**
|
|
6
|
+
* Written into `ios/` or `android/` by eject. Its presence tells the builder
|
|
7
|
+
* the folder belongs to the user now: build it as it is, never regenerate it.
|
|
8
|
+
* A marker rather than the folder's mere existence, so an unrelated `ios/`
|
|
9
|
+
* folder isn't mistaken for an ejected project.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EJECT_MARKER = ".angular-lynx-ejected";
|
|
12
|
+
export type PlatformGenerator = {
|
|
13
|
+
/** Writes the config into a fresh copy of the template. */
|
|
14
|
+
readonly configure: (dir: string, app: ResolvedApp, projectRoot: string) => void;
|
|
15
|
+
/** Where the Lynx bundle and its assets go inside the native project. */
|
|
16
|
+
readonly bundleDir: (dir: string) => string;
|
|
17
|
+
};
|
|
18
|
+
export type PreparedProject = {
|
|
19
|
+
/** The native project to build. */
|
|
20
|
+
readonly dir: string;
|
|
21
|
+
/** True when the project is the user's ejected `ios/` or `android/`. */
|
|
22
|
+
readonly ejected: boolean;
|
|
23
|
+
/** False when the cached project matched the inputs and was reused. */
|
|
24
|
+
readonly regenerated: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Makes the native project for a platform ready to build: the generated one in
|
|
28
|
+
* `<workDir>/<platform>`, or the user's ejected folder.
|
|
29
|
+
*
|
|
30
|
+
* Generation is skipped when a hash of every input (this package's version,
|
|
31
|
+
* the template files, the app options and the icon) matches the last run, so
|
|
32
|
+
* Xcode and Gradle keep their incremental state between builds. The bundle is
|
|
33
|
+
* copied in on every build, because it changes on every build.
|
|
34
|
+
*/
|
|
35
|
+
export declare const prepareProject: ({ platform, generator, projectRoot, workDir, app, bundleDir, packageVersion, }: {
|
|
36
|
+
readonly platform: Platform;
|
|
37
|
+
readonly generator: PlatformGenerator;
|
|
38
|
+
readonly projectRoot: string;
|
|
39
|
+
/** Where generated projects go (see `BuildNativeAppOptions.workDir`). */
|
|
40
|
+
readonly workDir: string;
|
|
41
|
+
readonly app: ResolvedApp;
|
|
42
|
+
/** The rspeedy output folder (dist/). */
|
|
43
|
+
readonly bundleDir: string;
|
|
44
|
+
readonly packageVersion: string;
|
|
45
|
+
}) => PreparedProject;
|
|
46
|
+
/**
|
|
47
|
+
* Up to 22.2.2, native builds worked in `.angular-lynx/` at the project root.
|
|
48
|
+
* They now use Angular's cache, so delete the old folder rather than leave
|
|
49
|
+
* hundreds of megabytes of stale Xcode and Gradle state in the user's project. Only a
|
|
50
|
+
* folder with the `.gitignore` those versions wrote is ours to delete.
|
|
51
|
+
*/
|
|
52
|
+
export declare const removeLegacyWorkDir: (projectRoot: string, log: (message: string) => void) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Copies the native bundle and everything it references into the project,
|
|
55
|
+
* replacing the previous build's copy so deleted lazy bundles don't linger.
|
|
56
|
+
* Skips what rspeedy and this builder put in dist/ for other targets: the web
|
|
57
|
+
* bundle and the platform output folders.
|
|
58
|
+
*/
|
|
59
|
+
export declare const copyBundle: (from: string, to: string) => void;
|
|
60
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAC;AAEzC,mDAAmD;AACnD,eAAO,MAAM,aAAa,QAEzB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,0BAA0B,CAAC;AAIpD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,EAAE,CAClB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,WAAW,EAChB,WAAW,EAAE,MAAM,KAChB,IAAI,CAAC;IACV,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,mCAAmC;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAI,gFAQ5B;IACD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC,KAAG,eA2BH,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,aAAa,MAAM,EACnB,KAAK,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,KAC7B,IAUF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,EAAE,IAAI,MAAM,KAAG,IAqBrD,CAAC"}
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/** The shell templates shipped in this package. */
|
|
6
|
+
export const TEMPLATES_DIR = fileURLToPath(new URL('../templates/', import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* Written into `ios/` or `android/` by eject. Its presence tells the builder
|
|
9
|
+
* the folder belongs to the user now: build it as it is, never regenerate it.
|
|
10
|
+
* A marker rather than the folder's mere existence, so an unrelated `ios/`
|
|
11
|
+
* folder isn't mistaken for an ejected project.
|
|
12
|
+
*/
|
|
13
|
+
export const EJECT_MARKER = '.angular-lynx-ejected';
|
|
14
|
+
const HASH_FILE = '.angular-lynx-hash';
|
|
15
|
+
/**
|
|
16
|
+
* Makes the native project for a platform ready to build: the generated one in
|
|
17
|
+
* `<workDir>/<platform>`, or the user's ejected folder.
|
|
18
|
+
*
|
|
19
|
+
* Generation is skipped when a hash of every input (this package's version,
|
|
20
|
+
* the template files, the app options and the icon) matches the last run, so
|
|
21
|
+
* Xcode and Gradle keep their incremental state between builds. The bundle is
|
|
22
|
+
* copied in on every build, because it changes on every build.
|
|
23
|
+
*/
|
|
24
|
+
export const prepareProject = ({ platform, generator, projectRoot, workDir, app, bundleDir, packageVersion, }) => {
|
|
25
|
+
const ejectedDir = path.join(projectRoot, platform);
|
|
26
|
+
if (existsSync(path.join(ejectedDir, EJECT_MARKER))) {
|
|
27
|
+
copyBundle(bundleDir, generator.bundleDir(ejectedDir));
|
|
28
|
+
return { dir: ejectedDir, ejected: true, regenerated: false };
|
|
29
|
+
}
|
|
30
|
+
const dir = path.join(workDir, platform);
|
|
31
|
+
const hash = inputHash({ platform, app, projectRoot, packageVersion });
|
|
32
|
+
const hashFile = path.join(dir, HASH_FILE);
|
|
33
|
+
const upToDate = existsSync(hashFile) && readFileSync(hashFile, 'utf8') === hash;
|
|
34
|
+
if (!upToDate) {
|
|
35
|
+
rmSync(dir, { recursive: true, force: true });
|
|
36
|
+
mkdirSync(dir, { recursive: true });
|
|
37
|
+
cpSync(path.join(TEMPLATES_DIR, platform), dir, { recursive: true });
|
|
38
|
+
generator.configure(dir, app, projectRoot);
|
|
39
|
+
writeFileSync(hashFile, hash);
|
|
40
|
+
}
|
|
41
|
+
// Generated projects are build output. `ng new` already ignores Angular's
|
|
42
|
+
// cache, but a moved cache (`cli.cache.path`) or an edited .gitignore may
|
|
43
|
+
// not be: keep them out of the user's git without asking them to edit it.
|
|
44
|
+
writeFileSync(path.join(workDir, '.gitignore'), '*\n');
|
|
45
|
+
copyBundle(bundleDir, generator.bundleDir(dir));
|
|
46
|
+
return { dir, ejected: false, regenerated: !upToDate };
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Up to 22.2.2, native builds worked in `.angular-lynx/` at the project root.
|
|
50
|
+
* They now use Angular's cache, so delete the old folder rather than leave
|
|
51
|
+
* hundreds of megabytes of stale Xcode and Gradle state in the user's project. Only a
|
|
52
|
+
* folder with the `.gitignore` those versions wrote is ours to delete.
|
|
53
|
+
*/
|
|
54
|
+
export const removeLegacyWorkDir = (projectRoot, log) => {
|
|
55
|
+
const legacy = path.join(projectRoot, '.angular-lynx');
|
|
56
|
+
const gitignore = path.join(legacy, '.gitignore');
|
|
57
|
+
if (!existsSync(gitignore) || readFileSync(gitignore, 'utf8') !== '*\n') {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
rmSync(legacy, { recursive: true, force: true });
|
|
61
|
+
log('Removed .angular-lynx/: native projects are now generated in the Angular cache');
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Copies the native bundle and everything it references into the project,
|
|
65
|
+
* replacing the previous build's copy so deleted lazy bundles don't linger.
|
|
66
|
+
* Skips what rspeedy and this builder put in dist/ for other targets: the web
|
|
67
|
+
* bundle and the platform output folders.
|
|
68
|
+
*/
|
|
69
|
+
export const copyBundle = (from, to) => {
|
|
70
|
+
if (!existsSync(path.join(from, 'main.lynx.bundle'))) {
|
|
71
|
+
throw new Error(`${from} has no main.lynx.bundle. The Lynx environment of lynx.config.ts must build to this folder.`);
|
|
72
|
+
}
|
|
73
|
+
rmSync(to, { recursive: true, force: true });
|
|
74
|
+
mkdirSync(to, { recursive: true });
|
|
75
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
76
|
+
if (entry.name === 'ios' ||
|
|
77
|
+
entry.name === 'android' ||
|
|
78
|
+
entry.name.startsWith('.') ||
|
|
79
|
+
entry.name.endsWith('.web.bundle')) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
cpSync(path.join(from, entry.name), path.join(to, entry.name), {
|
|
83
|
+
recursive: true,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const inputHash = ({ platform, app, projectRoot, packageVersion, }) => {
|
|
88
|
+
const hash = createHash('sha256');
|
|
89
|
+
hash.update(`${packageVersion}\n${platform}\n${JSON.stringify(app)}\n`);
|
|
90
|
+
for (const file of listFiles(path.join(TEMPLATES_DIR, platform))) {
|
|
91
|
+
hash.update(file.relative);
|
|
92
|
+
hash.update(readFileSync(file.absolute));
|
|
93
|
+
}
|
|
94
|
+
if (app.icon) {
|
|
95
|
+
const icon = path.resolve(projectRoot, app.icon);
|
|
96
|
+
hash.update(existsSync(icon) ? readFileSync(icon) : 'missing icon');
|
|
97
|
+
}
|
|
98
|
+
return hash.digest('hex');
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Every file under `dir`, sorted, so the hash doesn't depend on fs order.
|
|
102
|
+
*/
|
|
103
|
+
const listFiles = (dir, prefix = '') => readdirSync(dir, { withFileTypes: true })
|
|
104
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
105
|
+
.flatMap((entry) => {
|
|
106
|
+
const relative = path.posix.join(prefix, entry.name);
|
|
107
|
+
const absolute = path.join(dir, entry.name);
|
|
108
|
+
return entry.isDirectory()
|
|
109
|
+
? listFiles(absolute, relative)
|
|
110
|
+
: [{ relative, absolute }];
|
|
111
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Platform } from './generate.ts';
|
|
2
|
+
import { type NativeAppOptions } from './options.ts';
|
|
3
|
+
export type { Platform } from './generate.ts';
|
|
4
|
+
export { EJECT_MARKER, copyBundle, prepareProject } from './generate.ts';
|
|
5
|
+
export { renderEjectedProject } from './eject.ts';
|
|
6
|
+
export { LYNX_IOS, FRAMEWORKS_ENV } from './ios/frameworks.ts';
|
|
7
|
+
export type { AndroidOptions, IosOptions, NativeAppOptions, Orientation, Permission, ResolvedApp, } from './options.ts';
|
|
8
|
+
export { NativeConfigError, resolveAppOptions } from './options.ts';
|
|
9
|
+
export { NativeBuildError } from './process.ts';
|
|
10
|
+
export declare const PACKAGE_VERSION: string;
|
|
11
|
+
export type BuildNativeAppOptions = {
|
|
12
|
+
readonly platform: Platform;
|
|
13
|
+
/** The Angular project's root folder (where lynx.config.ts is). */
|
|
14
|
+
readonly projectRoot: string;
|
|
15
|
+
/**
|
|
16
|
+
* Where the generated native projects, their build state and the build
|
|
17
|
+
* logs go. The builder passes the project's folder in Angular's cache, so
|
|
18
|
+
* nothing is added to the project root and `ng cache clean` clears it.
|
|
19
|
+
*/
|
|
20
|
+
readonly workDir: string;
|
|
21
|
+
/** The project name, the default app name. */
|
|
22
|
+
readonly projectName: string;
|
|
23
|
+
/** The project's package.json version, the default app version. */
|
|
24
|
+
readonly packageVersion?: string;
|
|
25
|
+
/** The rspeedy output folder holding main.lynx.bundle. */
|
|
26
|
+
readonly bundleDir: string;
|
|
27
|
+
/** Where the binaries go, e.g. dist/ios. */
|
|
28
|
+
readonly outputDir: string;
|
|
29
|
+
readonly app: NativeAppOptions;
|
|
30
|
+
readonly log: (message: string) => void;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Turns a built Lynx bundle into native binaries: generates (or reuses) the
|
|
34
|
+
* shell project, copies the bundle in, and runs xcodebuild or Gradle.
|
|
35
|
+
* Returns the paths of the binaries it wrote to `outputDir`.
|
|
36
|
+
*/
|
|
37
|
+
export declare const buildNativeApp: (options: BuildNativeAppOptions) => Promise<{
|
|
38
|
+
readonly artifacts: readonly string[];
|
|
39
|
+
readonly projectDir: string;
|
|
40
|
+
}>;
|
|
41
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,QAAQ,EAGd,MAAM,eAAe,CAAC;AAIvB,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,cAAc,CAAC;AAExE,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;AAEhD,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,4CAA4C;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,gBAAgB,CAAC;IAC/B,QAAQ,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC,CAAC;AAEF;;;;GAIG;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,CA4DA,CAAC"}
|