deeplink-parity 0.6.2 → 0.6.4
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 +18 -4
- package/dist/cli.js +20 -2
- package/dist/report/console.js +6 -2
- package/dist/routes/extract.js +6 -0
- package/dist/routes/init.js +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ npx deeplink-parity .
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
deeplink-parity · 3 domain(s) checked
|
|
16
|
+
deeplink-parity v0.6.4 · 3 domain(s) checked
|
|
17
|
+
routes: deeplink-parity.yml (iOS 74 · Android 73)
|
|
17
18
|
|
|
18
19
|
ERROR links.example.com
|
|
19
20
|
AASA responded with 404
|
|
@@ -24,7 +25,11 @@ WARN promo.example.com
|
|
|
24
25
|
Declared on iOS but not on Android
|
|
25
26
|
The same link opens the app on iOS and the browser on Android
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
WARN route-gap
|
|
29
|
+
/events is in the Android route table but not iOS's
|
|
30
|
+
A /events link navigates on Android and goes nowhere on iOS.
|
|
31
|
+
|
|
32
|
+
1 error, 2 warn, 0 info
|
|
28
33
|
```
|
|
29
34
|
|
|
30
35
|
<br>
|
|
@@ -57,6 +62,9 @@ Findings show up as annotations on the run. Add `sha256` to include the Android
|
|
|
57
62
|
key in the check, and `fail-on: never` to report without failing while an existing backlog
|
|
58
63
|
is cleared.
|
|
59
64
|
|
|
65
|
+
Domains are checked with zero setup. To also compare the **screens behind the links**,
|
|
66
|
+
run `npx deeplink-parity init` once — see [Route parity](#route-parity).
|
|
67
|
+
|
|
60
68
|
<br>
|
|
61
69
|
|
|
62
70
|
## Contents
|
|
@@ -111,6 +119,7 @@ No configuration file. Domains are discovered from your app, then the matching w
|
|
|
111
119
|
**Warnings** — one platform works and the other does not
|
|
112
120
|
|
|
113
121
|
- **A domain is declared on iOS but not Android, or the reverse**
|
|
122
|
+
- **A route is in one platform's route table but not the other's** (with [route parity](#route-parity) set up)
|
|
114
123
|
- An `https` intent-filter has no `autoVerify`
|
|
115
124
|
- A manifest host reference could not be resolved
|
|
116
125
|
|
|
@@ -135,6 +144,7 @@ npx deeplink-parity init [path...] # interactive setup for route com
|
|
|
135
144
|
--output <file> Also write the JSON result to a file
|
|
136
145
|
--format github GitHub Actions annotations (auto-detected on Actions)
|
|
137
146
|
--yes init only: accept the top suggestion without prompting
|
|
147
|
+
-v, --version Print the version
|
|
138
148
|
-h, --help Show this message
|
|
139
149
|
```
|
|
140
150
|
|
|
@@ -274,6 +284,8 @@ The **check never writes to the repositories it scans** — it reads files and G
|
|
|
274
284
|
public well-known files per domain, nothing else. `init` writes exactly one file,
|
|
275
285
|
`deeplink-parity.yml`, in your working directory, after printing its content and asking.
|
|
276
286
|
|
|
287
|
+
<br>
|
|
288
|
+
|
|
277
289
|
## Native, Flutter and React Native
|
|
278
290
|
|
|
279
291
|
Whatever the app is written in, deep links are declared in the same two native files and
|
|
@@ -316,7 +328,9 @@ read as browser registration rather than deep links.
|
|
|
316
328
|
|
|
317
329
|
- **Deferred deep links are out of scope.** Install-then-open attribution is decided at runtime by your attribution SDK's servers. No static check can confirm it, and reporting a pass would be worse than saying nothing.
|
|
318
330
|
- **It does not prove path equivalence.** iOS and Android use different path-matching engines. Differing path sets are reported as `info` with both sides shown, for a human to judge — not asserted as a bug.
|
|
319
|
-
- **It does not run your app.** Route
|
|
331
|
+
- **It does not run your app.** Route parity compares the declared tables; whether a
|
|
332
|
+
handler behind a route actually works, or a dynamically-matched route resolves, only
|
|
333
|
+
shows up on a device.
|
|
320
334
|
|
|
321
335
|
<br>
|
|
322
336
|
|
|
@@ -330,7 +344,7 @@ npm run typecheck
|
|
|
330
344
|
|
|
331
345
|
`fixtures/` holds synthetic iOS and Android projects covering literal hosts, chained
|
|
332
346
|
`@string` references resolved through gradle `resValue` and `.properties`, flavor-specific
|
|
333
|
-
values, and
|
|
347
|
+
values, unresolvable references, and route tables in both platforms' shapes.
|
|
334
348
|
|
|
335
349
|
<br>
|
|
336
350
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { localSource, networkSource } from './fetch/wellKnown.js';
|
|
5
5
|
import { exitCodeFor, printReport } from './report/console.js';
|
|
@@ -28,6 +28,7 @@ Options
|
|
|
28
28
|
--output <file> Also write the JSON result to a file
|
|
29
29
|
--format github GitHub Actions annotations (auto-detected on Actions)
|
|
30
30
|
--yes init only: accept the top suggestion without prompting
|
|
31
|
+
-v, --version Print the version
|
|
31
32
|
-h, --help Show this message
|
|
32
33
|
|
|
33
34
|
The check reads the repositories and GETs two public well-known files per domain —
|
|
@@ -39,6 +40,7 @@ function parseArgs(argv) {
|
|
|
39
40
|
const positional = [];
|
|
40
41
|
let json = false;
|
|
41
42
|
let help = false;
|
|
43
|
+
let version = false;
|
|
42
44
|
let printRoutes = false;
|
|
43
45
|
let yes = false;
|
|
44
46
|
let sha256;
|
|
@@ -53,6 +55,8 @@ function parseArgs(argv) {
|
|
|
53
55
|
json = true;
|
|
54
56
|
else if (arg === '-h' || arg === '--help')
|
|
55
57
|
help = true;
|
|
58
|
+
else if (arg === '-v' || arg === '--version')
|
|
59
|
+
version = true;
|
|
56
60
|
else if (arg === '--print-routes')
|
|
57
61
|
printRoutes = true;
|
|
58
62
|
else if (arg === '--yes')
|
|
@@ -77,6 +81,7 @@ function parseArgs(argv) {
|
|
|
77
81
|
roots: roots.length ? roots : [resolve('.')],
|
|
78
82
|
json,
|
|
79
83
|
help,
|
|
84
|
+
version,
|
|
80
85
|
printRoutes,
|
|
81
86
|
yes,
|
|
82
87
|
sha256,
|
|
@@ -110,6 +115,11 @@ async function assertRootsExist(roots) {
|
|
|
110
115
|
}
|
|
111
116
|
async function main() {
|
|
112
117
|
const opts = parseArgs(process.argv);
|
|
118
|
+
const pkg = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
119
|
+
if (opts.version) {
|
|
120
|
+
console.log(pkg.version);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
113
123
|
if (opts.help) {
|
|
114
124
|
console.log(USAGE);
|
|
115
125
|
return;
|
|
@@ -150,6 +160,8 @@ async function main() {
|
|
|
150
160
|
console.log();
|
|
151
161
|
}
|
|
152
162
|
const payload = {
|
|
163
|
+
version: pkg.version,
|
|
164
|
+
routeConfig: configPath,
|
|
153
165
|
ios: result.iosApps.map((a) => ({
|
|
154
166
|
entitlements: a.entitlementsPath,
|
|
155
167
|
teamId: a.teamId,
|
|
@@ -181,13 +193,19 @@ async function main() {
|
|
|
181
193
|
// machine-readable output in the same run.
|
|
182
194
|
if (opts.output)
|
|
183
195
|
await writeFile(opts.output, `${JSON.stringify(payload, null, 2)}\n`);
|
|
196
|
+
const routesLine = configPath
|
|
197
|
+
? `routes: ${configPath}` +
|
|
198
|
+
(result.routes
|
|
199
|
+
? ` (iOS ${result.routes.ios?.paths.length ?? 0} · Android ${result.routes.android?.paths.length ?? 0})`
|
|
200
|
+
: '')
|
|
201
|
+
: 'routes: no config found — run "deeplink-parity init" to compare route tables';
|
|
184
202
|
if (opts.json) {
|
|
185
203
|
console.log(JSON.stringify(payload, null, 2));
|
|
186
204
|
}
|
|
187
205
|
else {
|
|
188
206
|
if (opts.format === 'github')
|
|
189
207
|
printGithubAnnotations(result.findings);
|
|
190
|
-
printReport(result.findings, result.domains);
|
|
208
|
+
printReport(result.findings, result.domains, { version: pkg.version, routesLine });
|
|
191
209
|
}
|
|
192
210
|
process.exit(exitCodeFor(result.findings));
|
|
193
211
|
}
|
package/dist/report/console.js
CHANGED
|
@@ -10,10 +10,14 @@ const LABEL = { error: 'ERROR', warn: 'WARN ', info: 'INFO ' };
|
|
|
10
10
|
const ORDER = ['error', 'warn', 'info'];
|
|
11
11
|
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
12
12
|
const paint = (s, c) => (useColor ? `${c}${s}${RESET}` : s);
|
|
13
|
-
export function printReport(findings, checkedDomains) {
|
|
13
|
+
export function printReport(findings, checkedDomains, meta) {
|
|
14
14
|
// every escape goes through paint(), which is a no-op when stdout is not a terminal
|
|
15
15
|
const separator = paint('·', DIM);
|
|
16
|
-
|
|
16
|
+
const version = meta ? ` ${paint(`v${meta.version}`, DIM)}` : '';
|
|
17
|
+
console.log(`\n${paint('deeplink-parity', BOLD)}${version} ${separator} ${checkedDomains.length} domain(s) checked`);
|
|
18
|
+
if (meta)
|
|
19
|
+
console.log(paint(meta.routesLine, DIM));
|
|
20
|
+
console.log();
|
|
17
21
|
if (findings.length === 0) {
|
|
18
22
|
console.log(paint('No problems found', '[32m'));
|
|
19
23
|
console.log();
|
package/dist/routes/extract.js
CHANGED
|
@@ -33,9 +33,11 @@ export async function extractRoutes(platform, config, roots) {
|
|
|
33
33
|
const findings = [];
|
|
34
34
|
const files = [];
|
|
35
35
|
const paths = new Set();
|
|
36
|
+
let unresolved = false;
|
|
36
37
|
for (const file of config.files) {
|
|
37
38
|
const hits = await resolveInRoots(file, roots);
|
|
38
39
|
if (hits.length === 0) {
|
|
40
|
+
unresolved = true;
|
|
39
41
|
findings.push({
|
|
40
42
|
severity: 'error',
|
|
41
43
|
rule: 'routes-file-missing',
|
|
@@ -45,6 +47,7 @@ export async function extractRoutes(platform, config, roots) {
|
|
|
45
47
|
continue;
|
|
46
48
|
}
|
|
47
49
|
if (hits.length > 1) {
|
|
50
|
+
unresolved = true;
|
|
48
51
|
findings.push({
|
|
49
52
|
severity: 'error',
|
|
50
53
|
rule: 'routes-file-ambiguous',
|
|
@@ -59,6 +62,9 @@ export async function extractRoutes(platform, config, roots) {
|
|
|
59
62
|
paths.add(m[1] ?? m[0]);
|
|
60
63
|
}
|
|
61
64
|
}
|
|
65
|
+
// a table missing any of its files is incomplete; diffing it would manufacture gaps
|
|
66
|
+
if (unresolved)
|
|
67
|
+
return { findings };
|
|
62
68
|
if (files.length > 0 && paths.size === 0) {
|
|
63
69
|
// matching nothing is a broken config, never a clean pass
|
|
64
70
|
findings.push({
|
package/dist/routes/init.js
CHANGED
|
@@ -35,7 +35,17 @@ async function pickFiles(rl, platform, candidates, yes) {
|
|
|
35
35
|
return [];
|
|
36
36
|
if (answer === 'm') {
|
|
37
37
|
const manual = (await rl.question('path: ')).trim();
|
|
38
|
-
|
|
38
|
+
if (!manual)
|
|
39
|
+
return [];
|
|
40
|
+
// a mistyped path written into the config would only surface at check time
|
|
41
|
+
try {
|
|
42
|
+
await access(manual);
|
|
43
|
+
return [manual];
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.log(` Not found: ${manual}`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
39
49
|
}
|
|
40
50
|
const tokens = answer.split(/[,\s]+/);
|
|
41
51
|
const indexes = tokens.map((t) => Number.parseInt(t, 10));
|
package/package.json
CHANGED