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 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
- 1 error, 1 warn, 0 info
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 handling inside the app is not inspected.
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 unresolvable references.
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
  }
@@ -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
- console.log(`\n${paint('deeplink-parity', BOLD)} ${separator} ${checkedDomains.length} domain(s) checked\n`);
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', ''));
19
23
  console.log();
@@ -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({
@@ -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
- return manual ? [manual] : [];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deeplink-parity",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Checks that what your mobile app declares about deep links matches what is actually hosted — across iOS and Android.",
5
5
  "type": "module",
6
6
  "bin": {