@radhya/mach 2.0.24 → 2.0.31

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.
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "commonjs",
3
+ "main": "index.js"
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radhya/mach",
3
- "version": "2.0.24",
3
+ "version": "2.0.31",
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
- "docs/frameworks/"
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",