@inglorious/capacitor 0.0.0-stage → 0.1.0
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/LICENSE +9 -0
- package/README.md +94 -2
- package/bin/inglorious-capacitor.js +32 -0
- package/package.json +59 -3
- package/src/assets/index.js +127 -0
- package/src/config.js +62 -0
- package/src/configure/android.js +96 -0
- package/src/configure/configure.test.js +182 -0
- package/src/configure/fixtures/android/app/build.gradle +54 -0
- package/src/configure/fixtures/android/app/src/main/AndroidManifest.xml +41 -0
- package/src/configure/fixtures/android/build.gradle +29 -0
- package/src/configure/fixtures/android/settings.gradle +5 -0
- package/src/configure/fixtures/android/variables.gradle +16 -0
- package/src/configure/fixtures/ios/App/App/AppDelegate.swift +44 -0
- package/src/configure/fixtures/ios/App/App/Base.lproj/Main.storyboard +19 -0
- package/src/configure/fixtures/ios/App/App/Info.plist +70 -0
- package/src/configure/fixtures/ios/App/App.xcodeproj/project.pbxproj +380 -0
- package/src/configure/index.js +88 -0
- package/src/configure/ios.js +185 -0
- package/src/configure/well-known.js +77 -0
- package/src/configure/well-known.test.js +65 -0
- package/src/index.js +3 -0
- package/types/index.d.ts +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2025 Inglorious Coderz Srl.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @inglorious/capacitor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
Native project setup for [Capacitor](https://capacitorjs.com) apps, written once in `package.json` instead of clicked through Xcode and Android Studio. `@inglorious/create-app --mobile` sets it up for you.
|
|
6
|
+
|
|
7
|
+
- `inglorious-capacitor configure` writes the app's native settings into `android/` and `ios/` after every `cap sync`: App Links and Universal Links, the signing team, push, Analytics off until consent, permissions and Info.plist entries, and the Firebase files.
|
|
8
|
+
- `inglorious-capacitor assets` makes every icon and splash screen, light and dark, from one logo.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add -D @inglorious/capacitor
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run `configure` as Capacitor's `sync` hook, so `cap add` and `cap sync` both apply the settings:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"scripts": {
|
|
21
|
+
"capacitor:sync:after": "inglorious-capacitor configure",
|
|
22
|
+
"mobile:init:ios": "cap add ios && inglorious-capacitor assets --ios",
|
|
23
|
+
"mobile:init:android": "cap add android && inglorious-capacitor assets --android",
|
|
24
|
+
"mobile:assets": "inglorious-capacitor assets"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Settings
|
|
30
|
+
|
|
31
|
+
The `native` key of `package.json` holds them all. Every key is optional; the app's id comes from `capacitor.config.ts`.
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"native": {
|
|
36
|
+
"appLinks": {
|
|
37
|
+
"domain": "app.example.com",
|
|
38
|
+
"paths": ["/", "/jobs*"],
|
|
39
|
+
"exclude": ["#*access_token*", "/onboarding*"]
|
|
40
|
+
},
|
|
41
|
+
"ios": {
|
|
42
|
+
"teamId": "ABCDE12345",
|
|
43
|
+
"swipeBack": true,
|
|
44
|
+
"infoPlist": {
|
|
45
|
+
"ITSAppUsesNonExemptEncryption": false,
|
|
46
|
+
"NSMicrophoneUsageDescription": "Records the voice message for your job."
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"android": {
|
|
50
|
+
"sha256": ["C1:0B:…:07"],
|
|
51
|
+
"permissions": ["android.permission.RECORD_AUDIO"]
|
|
52
|
+
},
|
|
53
|
+
"assets": {
|
|
54
|
+
"logo": "public/logo.svg",
|
|
55
|
+
"background": "#f6f3ea",
|
|
56
|
+
"backgroundDark": "#2b2b26",
|
|
57
|
+
"iconScale": 0.6,
|
|
58
|
+
"splashScale": 0.3
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### What `configure` does
|
|
65
|
+
|
|
66
|
+
| Setting | iOS | Android |
|
|
67
|
+
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
|
68
|
+
| `appLinks` | Associated Domains entitlement | Verified intent filter for `paths` |
|
|
69
|
+
| `appLinks` + `teamId` | `public/.well-known/apple-app-site-association` | |
|
|
70
|
+
| `appLinks` + `sha256` | | `public/.well-known/assetlinks.json` |
|
|
71
|
+
| `ios.teamId` | `DEVELOPMENT_TEAM`, so Xcode signs without asking | |
|
|
72
|
+
| `ios.swipeBack` | A swipe from the left edge goes back: WebKit's back gesture, turned on in a generated `MainViewController` | |
|
|
73
|
+
| `ios.infoPlist` | Info.plist entries | |
|
|
74
|
+
| `android.permissions` | | `<uses-permission>` entries |
|
|
75
|
+
| `@capacitor-firebase/messaging` installed | Push entitlement, background mode, AppDelegate methods | |
|
|
76
|
+
| `@capacitor-firebase/analytics` installed | Collection off until consent | Collection off until consent |
|
|
77
|
+
| `GoogleService-Info.plist` in the root | Copied into the app target | |
|
|
78
|
+
| `google-services.json` in the root | | Copied to `android/app/` |
|
|
79
|
+
| always | | Keystores kept out of git |
|
|
80
|
+
|
|
81
|
+
Path patterns end in `*` to match a prefix: `/jobs*` is every path that starts with `/jobs`. `exclude` takes paths and, starting with `#`, fragments; only iOS can exclude, so on Android list in `paths` only what opens the app. Serve both `.well-known` files as `application/json`.
|
|
82
|
+
|
|
83
|
+
It runs for the platform Capacitor is syncing, and changes nothing when the settings haven't changed. Commit `android/` and `ios/` as usual.
|
|
84
|
+
|
|
85
|
+
### What `assets` does
|
|
86
|
+
|
|
87
|
+
It centres the logo (`public/logo.svg` or `public/logo.png` by default) on `background` for the icons and the splash screen, and on `backgroundDark` for the dark splash screen, then hands the result to [`@capacitor/assets`](https://github.com/ionic-team/capacitor-assets) for every size and density. `iconScale` and `splashScale` are the logo's width as a share of the icon and of the splash screen. Pass `--ios` or `--android` for one platform.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
**MIT License - Free and open source**
|
|
92
|
+
|
|
93
|
+
Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
94
|
+
|
|
95
|
+
You're free to use, modify, and distribute this software. See [LICENSE](./LICENSE) for details.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process"
|
|
3
|
+
|
|
4
|
+
const USAGE = `Usage: inglorious-capacitor <command>
|
|
5
|
+
|
|
6
|
+
configure Apply the "native" settings in package.json to the
|
|
7
|
+
native projects (runs after every cap sync)
|
|
8
|
+
assets [--ios] [--android]
|
|
9
|
+
Make the app icons and splash screens from the logo`
|
|
10
|
+
|
|
11
|
+
const FIRST_ARGUMENT = 2
|
|
12
|
+
const SUCCESS = 0
|
|
13
|
+
const FAILURE = 1
|
|
14
|
+
|
|
15
|
+
const [command, ...flags] = process.argv.slice(FIRST_ARGUMENT)
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
if (command === "configure") {
|
|
19
|
+
const { configure } = await import("../src/configure/index.js")
|
|
20
|
+
const platforms = await configure()
|
|
21
|
+
if (platforms.length) console.log(`Configured ${platforms.join(" and ")}`)
|
|
22
|
+
} else if (command === "assets") {
|
|
23
|
+
const { assets } = await import("../src/assets/index.js")
|
|
24
|
+
await assets({ platforms: flags.map((flag) => flag.replace(/^--/, "")) })
|
|
25
|
+
} else {
|
|
26
|
+
console.log(USAGE)
|
|
27
|
+
process.exitCode = command ? FAILURE : SUCCESS
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(error.message)
|
|
31
|
+
process.exitCode = FAILURE
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/capacitor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Configures Capacitor's native projects from package.json, and makes their icons and splash screens from one logo.",
|
|
5
|
+
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/IngloriousCoderz/inglorious-forge.git",
|
|
10
|
+
"directory": "packages/capacitor"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/IngloriousCoderz/inglorious-forge/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"capacitor",
|
|
17
|
+
"ios",
|
|
18
|
+
"android",
|
|
19
|
+
"app-links",
|
|
20
|
+
"icons",
|
|
21
|
+
"splash-screen",
|
|
22
|
+
"inglorious-web"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"inglorious-capacitor": "./bin/inglorious-capacitor.js"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./types/index.d.ts",
|
|
31
|
+
"import": "./src/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin",
|
|
36
|
+
"src",
|
|
37
|
+
"types"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@capacitor/assets": "^4.0.0",
|
|
44
|
+
"@trapezedev/project": "^7.1.11",
|
|
45
|
+
"plist": "^3.1.1",
|
|
46
|
+
"sharp": "^0.35.5"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@inglorious/eslint-config": "1.2.0",
|
|
50
|
+
"prettier": "^3.9.10",
|
|
51
|
+
"vitest": "^5.1.0"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">= 26"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"format": "prettier --write .",
|
|
58
|
+
"lint": "eslint .",
|
|
59
|
+
"test:watch": "vitest",
|
|
60
|
+
"test": "vitest run"
|
|
61
|
+
}
|
|
6
62
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process"
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"
|
|
3
|
+
import { createRequire } from "node:module"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import process from "node:process"
|
|
6
|
+
|
|
7
|
+
import sharp from "sharp"
|
|
8
|
+
|
|
9
|
+
import { readSettings } from "../config.js"
|
|
10
|
+
|
|
11
|
+
const LOGOS = ["public/logo.svg", "public/logo.png"]
|
|
12
|
+
const SOURCES = path.join(
|
|
13
|
+
"node_modules",
|
|
14
|
+
".cache",
|
|
15
|
+
"inglorious-capacitor",
|
|
16
|
+
"assets",
|
|
17
|
+
)
|
|
18
|
+
const PLATFORMS = ["ios", "android"]
|
|
19
|
+
const DEFAULTS = {
|
|
20
|
+
background: "#ffffff",
|
|
21
|
+
backgroundDark: "#121212",
|
|
22
|
+
iconScale: 0.6,
|
|
23
|
+
splashScale: 0.3,
|
|
24
|
+
}
|
|
25
|
+
const ICON_SIZE = 1024
|
|
26
|
+
const SPLASH_SIZE = 2732
|
|
27
|
+
const TRANSPARENT = "#00000000"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Makes every app icon and splash screen of the native projects from one
|
|
31
|
+
* logo, centred on the background colours of `native.assets` in
|
|
32
|
+
* `package.json`, in light and dark. The mark is sized from the logo itself,
|
|
33
|
+
* so a new logo needs no new settings.
|
|
34
|
+
*
|
|
35
|
+
* @param {{ root?: string, platforms?: string[] }} [options] Both platforms
|
|
36
|
+
* that exist by default.
|
|
37
|
+
*/
|
|
38
|
+
export async function assets({ root = process.cwd(), platforms = [] } = {}) {
|
|
39
|
+
const settings = await readSettings(root, {})
|
|
40
|
+
const options = { ...DEFAULTS, ...settings.assets }
|
|
41
|
+
const logo =
|
|
42
|
+
options.logo ?? LOGOS.find((file) => existsSync(path.join(root, file)))
|
|
43
|
+
if (!logo || !existsSync(path.join(root, logo))) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`No logo found: add ${LOGOS.join(" or ")}, or set native.assets.logo`,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const targets = (platforms.length ? platforms : PLATFORMS).filter(
|
|
50
|
+
(platform) =>
|
|
51
|
+
PLATFORMS.includes(platform) && existsSync(path.join(root, platform)),
|
|
52
|
+
)
|
|
53
|
+
if (!targets.length) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"No native project found: run mobile:init:ios or mobile:init:android",
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const sources = path.join(root, SOURCES)
|
|
60
|
+
rmSync(sources, { recursive: true, force: true })
|
|
61
|
+
mkdirSync(sources, { recursive: true })
|
|
62
|
+
await composeSources(readFileSync(path.join(root, logo)), sources, options)
|
|
63
|
+
|
|
64
|
+
execFileSync(
|
|
65
|
+
process.execPath,
|
|
66
|
+
[
|
|
67
|
+
capacitorAssetsBin(),
|
|
68
|
+
"generate",
|
|
69
|
+
...targets.map((platform) => `--${platform}`),
|
|
70
|
+
"--assetPath",
|
|
71
|
+
SOURCES,
|
|
72
|
+
],
|
|
73
|
+
{ cwd: root, stdio: "inherit" },
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The sources `@capacitor/assets` expects, so it never scales the logo itself:
|
|
79
|
+
* it would size the splash from the image's width, not the mark's.
|
|
80
|
+
*/
|
|
81
|
+
async function composeSources(
|
|
82
|
+
logo,
|
|
83
|
+
dir,
|
|
84
|
+
{ background, backgroundDark, iconScale, splashScale },
|
|
85
|
+
) {
|
|
86
|
+
const mark = (canvas, scale) =>
|
|
87
|
+
sharp(logo)
|
|
88
|
+
.resize(Math.round(canvas * scale))
|
|
89
|
+
.png()
|
|
90
|
+
.toBuffer()
|
|
91
|
+
|
|
92
|
+
const canvas = (size, color) =>
|
|
93
|
+
sharp({
|
|
94
|
+
create: { width: size, height: size, channels: 4, background: color },
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const icon = await mark(ICON_SIZE, iconScale)
|
|
98
|
+
await canvas(ICON_SIZE, background)
|
|
99
|
+
.composite([{ input: icon, gravity: "centre" }])
|
|
100
|
+
.png()
|
|
101
|
+
.toFile(path.join(dir, "icon-only.png"))
|
|
102
|
+
await canvas(ICON_SIZE, TRANSPARENT)
|
|
103
|
+
.composite([{ input: icon, gravity: "centre" }])
|
|
104
|
+
.png()
|
|
105
|
+
.toFile(path.join(dir, "icon-foreground.png"))
|
|
106
|
+
await canvas(ICON_SIZE, background)
|
|
107
|
+
.png()
|
|
108
|
+
.toFile(path.join(dir, "icon-background.png"))
|
|
109
|
+
|
|
110
|
+
const splash = await mark(SPLASH_SIZE, splashScale)
|
|
111
|
+
for (const [name, color] of [
|
|
112
|
+
["splash.png", background],
|
|
113
|
+
["splash-dark.png", backgroundDark],
|
|
114
|
+
]) {
|
|
115
|
+
await canvas(SPLASH_SIZE, color)
|
|
116
|
+
.composite([{ input: splash, gravity: "centre" }])
|
|
117
|
+
.png()
|
|
118
|
+
.toFile(path.join(dir, name))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function capacitorAssetsBin() {
|
|
123
|
+
const require = createRequire(import.meta.url)
|
|
124
|
+
const manifest = require.resolve("@capacitor/assets/package.json")
|
|
125
|
+
const { bin } = JSON.parse(readFileSync(manifest, "utf8"))
|
|
126
|
+
return path.join(path.dirname(manifest), bin["capacitor-assets"])
|
|
127
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import process from "node:process"
|
|
4
|
+
import { pathToFileURL } from "node:url"
|
|
5
|
+
|
|
6
|
+
const CAPACITOR_CONFIG_FILES = [
|
|
7
|
+
"capacitor.config.ts",
|
|
8
|
+
"capacitor.config.js",
|
|
9
|
+
"capacitor.config.json",
|
|
10
|
+
]
|
|
11
|
+
const PUSH_PLUGIN = "@capacitor-firebase/messaging"
|
|
12
|
+
const ANALYTICS_PLUGIN = "@capacitor-firebase/analytics"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Everything `configure` and `assets` act on: the app's id and name from the
|
|
16
|
+
* Capacitor config, the `native` settings from `package.json`, and the Firebase
|
|
17
|
+
* plugins the app depends on.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} root The project's folder.
|
|
20
|
+
* @param {NodeJS.ProcessEnv} [env] Capacitor hands its config to hooks here.
|
|
21
|
+
* @returns {Promise<import("../types/index.d.ts").ProjectSettings>}
|
|
22
|
+
*/
|
|
23
|
+
export async function readSettings(root, env = process.env) {
|
|
24
|
+
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"))
|
|
25
|
+
const capacitor = env.CAPACITOR_CONFIG
|
|
26
|
+
? JSON.parse(env.CAPACITOR_CONFIG)
|
|
27
|
+
: await importCapacitorConfig(root)
|
|
28
|
+
const dependencies = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
29
|
+
const native = pkg.native ?? {}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
root,
|
|
33
|
+
appId: capacitor.appId,
|
|
34
|
+
appName: capacitor.appName,
|
|
35
|
+
appLinks: native.appLinks?.domain
|
|
36
|
+
? normalizeAppLinks(native.appLinks)
|
|
37
|
+
: null,
|
|
38
|
+
ios: { teamId: null, infoPlist: {}, ...native.ios },
|
|
39
|
+
android: { sha256: [], permissions: [], ...native.android },
|
|
40
|
+
assets: native.assets ?? {},
|
|
41
|
+
hasPush: PUSH_PLUGIN in dependencies,
|
|
42
|
+
hasAnalytics: ANALYTICS_PLUGIN in dependencies,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function importCapacitorConfig(root) {
|
|
47
|
+
const file = CAPACITOR_CONFIG_FILES.map((name) => path.join(root, name)).find(
|
|
48
|
+
(candidate) => existsSync(candidate),
|
|
49
|
+
)
|
|
50
|
+
if (!file) {
|
|
51
|
+
throw new Error(`No Capacitor config found in ${root}`)
|
|
52
|
+
}
|
|
53
|
+
if (file.endsWith(".json")) {
|
|
54
|
+
return JSON.parse(readFileSync(file, "utf8"))
|
|
55
|
+
}
|
|
56
|
+
const { default: config } = await import(pathToFileURL(file).href)
|
|
57
|
+
return config
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeAppLinks({ domain, paths = ["/*"], exclude = [] }) {
|
|
61
|
+
return { domain, paths, exclude }
|
|
62
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
import { intentFilterData } from "./well-known.js"
|
|
5
|
+
|
|
6
|
+
const ACTIVITY = "manifest/application/activity"
|
|
7
|
+
const APPLICATION = "manifest/application"
|
|
8
|
+
const MANIFEST = "manifest"
|
|
9
|
+
const APP_LINKS_FILTER = `${ACTIVITY}/intent-filter[@android:autoVerify='true']`
|
|
10
|
+
const ANALYTICS_OFF = [
|
|
11
|
+
"firebase_analytics_collection_enabled",
|
|
12
|
+
"google_analytics_adid_collection_enabled",
|
|
13
|
+
]
|
|
14
|
+
const GOOGLE_SERVICES = "google-services.json"
|
|
15
|
+
const KEYSTORES = ["*.jks", "*.keystore"]
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Applies the settings to the Android project: App Links, Analytics off until
|
|
19
|
+
* consent, extra permissions, the Firebase file, and keystores kept out of git.
|
|
20
|
+
*
|
|
21
|
+
* @param {import("@trapezedev/project").MobileProject} project
|
|
22
|
+
* @param {import("../../types/index.d.ts").ProjectSettings} settings
|
|
23
|
+
*/
|
|
24
|
+
export function configureAndroid(project, settings) {
|
|
25
|
+
const manifest = project.android.getAndroidManifest()
|
|
26
|
+
const androidRoot = path.join(settings.root, "android")
|
|
27
|
+
|
|
28
|
+
if (manifest.find(APP_LINKS_FILTER)?.length) {
|
|
29
|
+
manifest.deleteNodes(APP_LINKS_FILTER)
|
|
30
|
+
}
|
|
31
|
+
if (settings.appLinks) {
|
|
32
|
+
manifest.injectFragment(ACTIVITY, appLinksFilter(settings.appLinks))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (settings.hasAnalytics) {
|
|
36
|
+
for (const name of ANALYTICS_OFF) {
|
|
37
|
+
if (
|
|
38
|
+
manifest.find(`${APPLICATION}/meta-data[@android:name='${name}']`)
|
|
39
|
+
?.length
|
|
40
|
+
) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
manifest.injectFragment(
|
|
44
|
+
APPLICATION,
|
|
45
|
+
`<meta-data android:name="${name}" android:value="false" />`,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const permission of settings.android.permissions) {
|
|
51
|
+
if (
|
|
52
|
+
manifest.find(
|
|
53
|
+
`${MANIFEST}/uses-permission[@android:name='${permission}']`,
|
|
54
|
+
)?.length
|
|
55
|
+
) {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
manifest.injectFragment(
|
|
59
|
+
MANIFEST,
|
|
60
|
+
`<uses-permission android:name="${permission}" />`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const googleServices = path.join(settings.root, GOOGLE_SERVICES)
|
|
65
|
+
if (existsSync(googleServices)) {
|
|
66
|
+
copyFileSync(googleServices, path.join(androidRoot, "app", GOOGLE_SERVICES))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ignoreKeystores(path.join(androidRoot, ".gitignore"))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function appLinksFilter(appLinks) {
|
|
73
|
+
const data = intentFilterData(appLinks)
|
|
74
|
+
.map(
|
|
75
|
+
(attributes) =>
|
|
76
|
+
`<data ${Object.entries(attributes)
|
|
77
|
+
.map(([name, value]) => `${name}="${value}"`)
|
|
78
|
+
.join(" ")} />`,
|
|
79
|
+
)
|
|
80
|
+
.join("")
|
|
81
|
+
|
|
82
|
+
return `<intent-filter android:autoVerify="true"><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" />${data}</intent-filter>`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ignoreKeystores(file) {
|
|
86
|
+
if (!existsSync(file)) return
|
|
87
|
+
|
|
88
|
+
const source = readFileSync(file, "utf8")
|
|
89
|
+
const lines = source.split("\n").map((line) => {
|
|
90
|
+
const uncommented = line.replace(/^#/, "")
|
|
91
|
+
return KEYSTORES.includes(uncommented) ? uncommented : line
|
|
92
|
+
})
|
|
93
|
+
const missing = KEYSTORES.filter((pattern) => !lines.includes(pattern))
|
|
94
|
+
const next = [...lines, ...missing].join("\n")
|
|
95
|
+
if (next !== source) writeFileSync(file, next)
|
|
96
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs"
|
|
9
|
+
import { tmpdir } from "node:os"
|
|
10
|
+
import path from "node:path"
|
|
11
|
+
import { fileURLToPath } from "node:url"
|
|
12
|
+
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
14
|
+
|
|
15
|
+
import { configure } from "./index.js"
|
|
16
|
+
|
|
17
|
+
const FIXTURES = fileURLToPath(new URL("./fixtures", import.meta.url))
|
|
18
|
+
const CAPACITOR_CONFIG = JSON.stringify({
|
|
19
|
+
appId: "com.example.app",
|
|
20
|
+
appName: "Example",
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe("configure", () => {
|
|
24
|
+
let root
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
root = mkdtempSync(path.join(tmpdir(), "inglorious-capacitor-"))
|
|
28
|
+
cpSync(FIXTURES, root, { recursive: true })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
rmSync(root, { recursive: true, force: true })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("leaves a project without settings as it was, but for keystores", async () => {
|
|
36
|
+
writePackage(root, {})
|
|
37
|
+
const before = snapshot(root)
|
|
38
|
+
|
|
39
|
+
await configure({ root, env: { CAPACITOR_CONFIG } })
|
|
40
|
+
|
|
41
|
+
const after = snapshot(root)
|
|
42
|
+
expect(after.entitlements).toBeNull()
|
|
43
|
+
expect(after.manifest).not.toContain("autoVerify")
|
|
44
|
+
expect(after.plist).not.toContain("UIBackgroundModes")
|
|
45
|
+
expect(after.appDelegate).toBe(before.appDelegate)
|
|
46
|
+
expect(after.gitignore).toMatch(/^\*\.jks$/m)
|
|
47
|
+
expect(existsSync(path.join(root, "public"))).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("applies every setting, and changes nothing when run again", async () => {
|
|
51
|
+
writePackage(root, {
|
|
52
|
+
dependencies: {
|
|
53
|
+
"@capacitor-firebase/messaging": "^8.0.0",
|
|
54
|
+
"@capacitor-firebase/analytics": "^8.0.0",
|
|
55
|
+
},
|
|
56
|
+
native: {
|
|
57
|
+
appLinks: {
|
|
58
|
+
domain: "app.example.com",
|
|
59
|
+
paths: ["/", "/jobs*"],
|
|
60
|
+
exclude: ["#*access_token*"],
|
|
61
|
+
},
|
|
62
|
+
ios: {
|
|
63
|
+
teamId: "ABCDE12345",
|
|
64
|
+
infoPlist: { NSMicrophoneUsageDescription: "Records your voice" },
|
|
65
|
+
},
|
|
66
|
+
android: {
|
|
67
|
+
sha256: ["AA:BB"],
|
|
68
|
+
permissions: ["android.permission.RECORD_AUDIO"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
writeFileSync(path.join(root, "google-services.json"), "{}")
|
|
73
|
+
writeFileSync(path.join(root, "GoogleService-Info.plist"), "<plist/>")
|
|
74
|
+
|
|
75
|
+
await configure({ root, env: { CAPACITOR_CONFIG } })
|
|
76
|
+
const first = snapshot(root)
|
|
77
|
+
await configure({ root, env: { CAPACITOR_CONFIG } })
|
|
78
|
+
|
|
79
|
+
expect(snapshot(root)).toEqual(first)
|
|
80
|
+
|
|
81
|
+
expect(first.manifest).toContain(
|
|
82
|
+
'<data android:scheme="https" android:host="app.example.com" />',
|
|
83
|
+
)
|
|
84
|
+
expect(first.manifest).toContain('<data android:pathPrefix="/jobs" />')
|
|
85
|
+
expect(first.manifest).toContain(
|
|
86
|
+
'<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />',
|
|
87
|
+
)
|
|
88
|
+
expect(first.manifest).toContain(
|
|
89
|
+
'<uses-permission android:name="android.permission.RECORD_AUDIO" />',
|
|
90
|
+
)
|
|
91
|
+
expect(first.googleServices).toBe("{}")
|
|
92
|
+
|
|
93
|
+
expect(first.pbxproj).toContain("DEVELOPMENT_TEAM = ABCDE12345;")
|
|
94
|
+
expect(first.pbxproj).toContain(
|
|
95
|
+
"CODE_SIGN_ENTITLEMENTS = App/App.entitlements;",
|
|
96
|
+
)
|
|
97
|
+
expect(first.pbxproj).toContain("GoogleService-Info.plist in Resources")
|
|
98
|
+
expect(first.entitlements).toContain("applinks:app.example.com")
|
|
99
|
+
expect(first.entitlements).toContain("<key>aps-environment</key>")
|
|
100
|
+
expect(first.plist).toContain("<string>remote-notification</string>")
|
|
101
|
+
expect(first.plist).toContain("<string>Records your voice</string>")
|
|
102
|
+
expect(first.plist).toContain(
|
|
103
|
+
"<key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key>",
|
|
104
|
+
)
|
|
105
|
+
expect(first.appDelegate).toContain(
|
|
106
|
+
"didRegisterForRemoteNotificationsWithDeviceToken",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
expect(JSON.parse(first.association).applinks.details[0].appIDs).toEqual([
|
|
110
|
+
"ABCDE12345.com.example.app",
|
|
111
|
+
])
|
|
112
|
+
expect(JSON.parse(first.assetLinks)[0].target.package_name).toBe(
|
|
113
|
+
"com.example.app",
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("moves App Links to a new domain", async () => {
|
|
118
|
+
writePackage(root, { native: { appLinks: { domain: "old.example.com" } } })
|
|
119
|
+
await configure({ root, env: { CAPACITOR_CONFIG } })
|
|
120
|
+
|
|
121
|
+
writePackage(root, { native: { appLinks: { domain: "new.example.com" } } })
|
|
122
|
+
await configure({ root, env: { CAPACITOR_CONFIG } })
|
|
123
|
+
|
|
124
|
+
const { manifest, entitlements } = snapshot(root)
|
|
125
|
+
expect(manifest).not.toContain("old.example.com")
|
|
126
|
+
expect(manifest.match(/autoVerify/g)).toHaveLength(1)
|
|
127
|
+
expect(entitlements).not.toContain("old.example.com")
|
|
128
|
+
expect(entitlements).toContain("applinks:new.example.com")
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("turns on iOS swipe back with a view controller of its own", async () => {
|
|
132
|
+
writePackage(root, { native: { ios: { swipeBack: true } } })
|
|
133
|
+
|
|
134
|
+
await configure({ root, env: { CAPACITOR_CONFIG } })
|
|
135
|
+
const first = snapshot(root)
|
|
136
|
+
await configure({ root, env: { CAPACITOR_CONFIG } })
|
|
137
|
+
|
|
138
|
+
expect(snapshot(root)).toEqual(first)
|
|
139
|
+
expect(first.viewController).toContain(
|
|
140
|
+
"webView?.allowsBackForwardNavigationGestures = true",
|
|
141
|
+
)
|
|
142
|
+
expect(first.storyboard).toContain('customClass="MainViewController"')
|
|
143
|
+
expect(first.pbxproj).toContain("MainViewController.swift in Sources")
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("configures only the platform Capacitor is syncing", async () => {
|
|
147
|
+
writePackage(root, { native: { ios: { teamId: "ABCDE12345" } } })
|
|
148
|
+
|
|
149
|
+
const platforms = await configure({
|
|
150
|
+
root,
|
|
151
|
+
env: { CAPACITOR_CONFIG, CAPACITOR_PLATFORM_NAME: "web" },
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
expect(platforms).toEqual([])
|
|
155
|
+
expect(snapshot(root).pbxproj).not.toContain("DEVELOPMENT_TEAM")
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
function writePackage(root, pkg) {
|
|
160
|
+
writeFileSync(path.join(root, "package.json"), JSON.stringify(pkg))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function snapshot(root) {
|
|
164
|
+
const read = (...segments) => {
|
|
165
|
+
const file = path.join(root, ...segments)
|
|
166
|
+
return existsSync(file) ? readFileSync(file, "utf8") : null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
manifest: read("android", "app", "src", "main", "AndroidManifest.xml"),
|
|
171
|
+
gitignore: read("android", ".gitignore"),
|
|
172
|
+
googleServices: read("android", "app", "google-services.json"),
|
|
173
|
+
pbxproj: read("ios", "App", "App.xcodeproj", "project.pbxproj"),
|
|
174
|
+
plist: read("ios", "App", "App", "Info.plist"),
|
|
175
|
+
entitlements: read("ios", "App", "App", "App.entitlements"),
|
|
176
|
+
appDelegate: read("ios", "App", "App", "AppDelegate.swift"),
|
|
177
|
+
viewController: read("ios", "App", "App", "MainViewController.swift"),
|
|
178
|
+
storyboard: read("ios", "App", "App", "Base.lproj", "Main.storyboard"),
|
|
179
|
+
association: read("public", ".well-known", "apple-app-site-association"),
|
|
180
|
+
assetLinks: read("public", ".well-known", "assetlinks.json"),
|
|
181
|
+
}
|
|
182
|
+
}
|