@radhya/mach 2.0.24 → 2.0.30
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 -0
- package/dist/{chunk-VX4DWB47.js → chunk-6LOQKG2J.js} +18 -18
- package/dist/{credentials-PS7URNCB.js → credentials-P3555DXZ.js} +1 -1
- package/dist/index.js +291 -162
- package/docs/deep-links.md +173 -0
- package/expo-plugin/index.js +119 -0
- package/expo-plugin/package.json +4 -0
- package/package.json +4 -2
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Deep Links
|
|
2
|
+
|
|
3
|
+
Mach can configure the project-side pieces for:
|
|
4
|
+
|
|
5
|
+
- iOS Universal Links
|
|
6
|
+
- Android App Links
|
|
7
|
+
- App URI schemes
|
|
8
|
+
- Smart App Banner metadata when a web `index.html` exists
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
Run the interactive flow:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
mach deeplink setup
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
When run from the project root, Mach reads these values from `mach.config.json`:
|
|
19
|
+
|
|
20
|
+
- `framework`
|
|
21
|
+
- `scheme`
|
|
22
|
+
- `ios.bundleIdentifier`
|
|
23
|
+
- `android.package`
|
|
24
|
+
- `ios.appleTeamId`
|
|
25
|
+
- `ios.ascAppId`
|
|
26
|
+
- `submit.<profile>.ios.appleTeamId`
|
|
27
|
+
- `submit.<profile>.ios.ascAppId`
|
|
28
|
+
|
|
29
|
+
Precedence is:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
command flags > existing deepLinks config > profile-specific config > root mach.config.json > Expo config inference > prompts/defaults
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For most linked projects, this is enough:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
mach deeplink setup --base-url https://app.example.com
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use `--profile` when the team/app metadata lives under a non-production submit profile:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
mach deeplink setup --profile staging --base-url https://staging.example.com
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Pass values directly only when you want to override config:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
mach deeplink setup \
|
|
51
|
+
--yes \
|
|
52
|
+
--profile production \
|
|
53
|
+
--framework expo \
|
|
54
|
+
--platform all \
|
|
55
|
+
--base-url https://app.example.com \
|
|
56
|
+
--scheme example \
|
|
57
|
+
--ios-bundle-id com.example.app \
|
|
58
|
+
--apple-team-id ABCDE12345 \
|
|
59
|
+
--app-store-id 1234567890 \
|
|
60
|
+
--android-package com.example.app \
|
|
61
|
+
--android-fingerprint AA:BB:CC:DD \
|
|
62
|
+
--install-plugin \
|
|
63
|
+
--output public
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`setup` writes:
|
|
67
|
+
|
|
68
|
+
- compact `deepLinks` in `mach.config.json`
|
|
69
|
+
- `public/.well-known/apple-app-site-association`
|
|
70
|
+
- `public/.well-known/apple-app-site-association.json`
|
|
71
|
+
- `public/apple-app-site-association`
|
|
72
|
+
- `public/apple-app-site-association.json`
|
|
73
|
+
- `public/.well-known/assetlinks.json`
|
|
74
|
+
|
|
75
|
+
When `--output` is not provided, Mach uses `public` if that folder exists; otherwise it writes to `deep-links` and prints a hosting note.
|
|
76
|
+
|
|
77
|
+
`mach.config.json` is the only Mach deep-link source of truth. Values already available from the selected profile or root config are not duplicated inside `deepLinks`. A typical generated block is intentionally small:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"scheme": "example",
|
|
82
|
+
"ios": {
|
|
83
|
+
"bundleIdentifier": "com.example.app",
|
|
84
|
+
"appleTeamId": "ABCDE12345"
|
|
85
|
+
},
|
|
86
|
+
"android": {
|
|
87
|
+
"package": "com.example.app"
|
|
88
|
+
},
|
|
89
|
+
"deepLinks": {
|
|
90
|
+
"baseUrl": "https://app.example.com",
|
|
91
|
+
"web": {
|
|
92
|
+
"outputDir": "public"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Expo Projects
|
|
99
|
+
|
|
100
|
+
For Expo projects, Mach wires a config plugin that applies:
|
|
101
|
+
|
|
102
|
+
- `scheme`
|
|
103
|
+
- `extra.deepLinkBaseUrl`
|
|
104
|
+
- `ios.associatedDomains`
|
|
105
|
+
- `android.intentFilters`
|
|
106
|
+
|
|
107
|
+
The plugin reads from `mach.config.json`; it does not require or create a second deep-link JSON file.
|
|
108
|
+
|
|
109
|
+
When `@radhya/mach` is installed in the app project, Mach uses the packaged plugin:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
plugins: ['@radhya/mach/expo-plugin']
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If the package is not installed, interactive `mach deeplink setup` asks to install it. `mach deeplink setup --yes` and `mach deeplink setup --install-plugin` install it automatically using the detected package manager.
|
|
116
|
+
|
|
117
|
+
Use `mach deeplink setup --no-install-plugin` when you want to avoid changing `package.json`; if `@radhya/mach` is missing, Mach uses the local fallback instead.
|
|
118
|
+
|
|
119
|
+
When the app only uses a global or `npx` Mach CLI and the package is not installed, Mach generates a local fallback:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
plugins/withMachDeepLinks.js
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
and wires:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
plugins: ['./plugins/withMachDeepLinks']
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This fallback keeps Expo and EAS builds working because the app project can resolve the plugin without depending on a global CLI install.
|
|
132
|
+
|
|
133
|
+
If the project uses `app.json` or `app.config.json`, Mach inserts the plugin automatically.
|
|
134
|
+
|
|
135
|
+
If the project uses `app.config.ts` or `app.config.js`, Mach inserts the plugin automatically when it can find a normal `plugins: [` array. If the dynamic config is too custom to patch safely, Mach prints the exact plugin entry to add:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
export default {
|
|
139
|
+
expo: {
|
|
140
|
+
plugins: ['@radhya/mach/expo-plugin'],
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Verify
|
|
146
|
+
|
|
147
|
+
Verify local files:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
mach deeplink verify
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Check hosted files too:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
mach deeplink verify --live
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Print Android device verification commands:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
mach deeplink verify --android-device
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Live verification checks:
|
|
166
|
+
|
|
167
|
+
- `https://<domain>/.well-known/apple-app-site-association`
|
|
168
|
+
- `https://<domain>/apple-app-site-association`
|
|
169
|
+
- `https://<domain>/.well-known/assetlinks.json`
|
|
170
|
+
|
|
171
|
+
## Current Scope
|
|
172
|
+
|
|
173
|
+
The first implementation generates hosted files and Expo integration. Vanilla React Native native file patching is intentionally conservative in this phase: verification reports missing Android manifest or iOS entitlement entries, but setup does not mutate native XML or Xcode project files yet.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
const unique = values => Array.from(new Set(values.filter(Boolean)))
|
|
5
|
+
const nonEmpty = value => typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined
|
|
6
|
+
const first = (...values) => values.map(nonEmpty).find(Boolean)
|
|
7
|
+
|
|
8
|
+
const sanitizeScheme = value => {
|
|
9
|
+
const normalized = nonEmpty(value)
|
|
10
|
+
if (!normalized) return 'app'
|
|
11
|
+
return normalized
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9+.-]/g, '-')
|
|
14
|
+
.replace(/^-+|-+$/g, '') || 'app'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const normalizeBaseUrl = value => {
|
|
18
|
+
const normalized = nonEmpty(value)
|
|
19
|
+
if (!normalized) return undefined
|
|
20
|
+
const parsed = new URL(normalized.includes('://') ? normalized : `https://${normalized}`)
|
|
21
|
+
const pathname = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/+$/, '')
|
|
22
|
+
return `${parsed.protocol}//${parsed.host}${pathname}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const domainFromBaseUrl = value => {
|
|
26
|
+
try {
|
|
27
|
+
const normalized = normalizeBaseUrl(value)
|
|
28
|
+
return normalized ? new URL(normalized).hostname : undefined
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const readMachConfig = config => {
|
|
35
|
+
const projectRoot = config && config._internal && config._internal.projectRoot
|
|
36
|
+
? config._internal.projectRoot
|
|
37
|
+
: process.cwd()
|
|
38
|
+
const configPath = path.join(projectRoot, 'mach.config.json')
|
|
39
|
+
if (!fs.existsSync(configPath)) return {}
|
|
40
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resolveBuildProfile = (machConfig, name, seen = new Set()) => {
|
|
44
|
+
const profiles = machConfig.build || {}
|
|
45
|
+
const profile = profiles[name]
|
|
46
|
+
if (!profile || typeof profile !== 'object') return {}
|
|
47
|
+
if (seen.has(name)) throw new Error(`Circular Mach build profile inheritance: ${name}`)
|
|
48
|
+
seen.add(name)
|
|
49
|
+
|
|
50
|
+
if (!profile.extends) return { ...profile }
|
|
51
|
+
const parent = resolveBuildProfile(machConfig, profile.extends, seen)
|
|
52
|
+
return {
|
|
53
|
+
...parent,
|
|
54
|
+
...profile,
|
|
55
|
+
env: { ...(parent.env || {}), ...(profile.env || {}) },
|
|
56
|
+
ios: { ...(parent.ios || {}), ...(profile.ios || {}) },
|
|
57
|
+
android: { ...(parent.android || {}), ...(profile.android || {}) },
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const hasDomainFilter = (filter, domain) => {
|
|
62
|
+
const data = Array.isArray(filter && filter.data) ? filter.data : []
|
|
63
|
+
return data.some(item => item && item.scheme === 'https' && item.host === domain)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const resolveDeepLinks = config => {
|
|
67
|
+
const machConfig = readMachConfig(config)
|
|
68
|
+
const deepLinks = machConfig.deepLinks || {}
|
|
69
|
+
const profileName = deepLinks.profile || process.env.MACH_PROFILE || process.env.EAS_BUILD_PROFILE || 'production'
|
|
70
|
+
const profile = resolveBuildProfile(machConfig, profileName)
|
|
71
|
+
const baseUrl = normalizeBaseUrl(deepLinks.baseUrl)
|
|
72
|
+
const domain = first(deepLinks.domain, domainFromBaseUrl(baseUrl))
|
|
73
|
+
const scheme = sanitizeScheme(first(
|
|
74
|
+
deepLinks.scheme,
|
|
75
|
+
profile.ios && profile.ios.scheme,
|
|
76
|
+
machConfig.scheme,
|
|
77
|
+
config.scheme,
|
|
78
|
+
machConfig.slug,
|
|
79
|
+
machConfig.name,
|
|
80
|
+
config.slug,
|
|
81
|
+
config.name,
|
|
82
|
+
'app'
|
|
83
|
+
))
|
|
84
|
+
|
|
85
|
+
return { ...deepLinks, baseUrl, domain, scheme }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = function withMachDeepLinks(config) {
|
|
89
|
+
const deepLinks = resolveDeepLinks(config)
|
|
90
|
+
if (!deepLinks.baseUrl || !deepLinks.domain) return config
|
|
91
|
+
|
|
92
|
+
config.scheme = deepLinks.scheme
|
|
93
|
+
config.extra = { ...(config.extra || {}), deepLinkBaseUrl: deepLinks.baseUrl }
|
|
94
|
+
|
|
95
|
+
config.ios = config.ios || {}
|
|
96
|
+
config.ios.associatedDomains = unique([
|
|
97
|
+
...(config.ios.associatedDomains || []),
|
|
98
|
+
deepLinks.domain ? `applinks:${deepLinks.domain}` : null,
|
|
99
|
+
deepLinks.ios && deepLinks.ios.webCredentials ? `webcredentials:${deepLinks.domain}` : null,
|
|
100
|
+
deepLinks.ios && deepLinks.ios.activityContinuation ? `activitycontinuation:${deepLinks.domain}` : null,
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
config.android = config.android || {}
|
|
104
|
+
const intentFilters = Array.isArray(config.android.intentFilters)
|
|
105
|
+
? config.android.intentFilters
|
|
106
|
+
: []
|
|
107
|
+
|
|
108
|
+
if (!intentFilters.some(filter => hasDomainFilter(filter, deepLinks.domain))) {
|
|
109
|
+
intentFilters.push({
|
|
110
|
+
action: 'VIEW',
|
|
111
|
+
autoVerify: true,
|
|
112
|
+
data: [{ scheme: 'https', host: deepLinks.domain, pathPattern: '.*' }],
|
|
113
|
+
category: ['BROWSABLE', 'DEFAULT'],
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
config.android.intentFilters = intentFilters
|
|
118
|
+
return config
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radhya/mach",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.30",
|
|
4
4
|
"description": "Mach CLI: Cloud Build Orchestrator for React Native & Expo",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/",
|
|
12
|
-
"
|
|
12
|
+
"expo-plugin/",
|
|
13
|
+
"docs/frameworks/",
|
|
14
|
+
"docs/deep-links.md"
|
|
13
15
|
],
|
|
14
16
|
"scripts": {
|
|
15
17
|
"build": "tsup src/index.ts src/commands/isolated-upload.ts --format esm --clean --minify --dts",
|