@produtype/core 0.74.0 → 0.75.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.
@@ -651,6 +651,25 @@ async function analyzeProject(projectPath) {
651
651
  swiftDeps.push(match[1].toLowerCase());
652
652
  }
653
653
  }
654
+ /**
655
+ * Package.resolved, which is where a Swift project's dependencies are actually
656
+ * legible.
657
+ *
658
+ * `Package.swift` is read above, and on a real application it usually declares the
659
+ * modules of that package rather than the whole tree: WordPress-iOS resolves 40-odd
660
+ * packages and `sentry-cocoa` — its crash reporter — appears only here. The lockfile
661
+ * is data rather than source, it names every transitive dependency, and it is
662
+ * committed by convention, which makes it the better of the two to read.
663
+ *
664
+ * `identity` rather than `location`, because that is the name SPM itself uses and
665
+ * the one that survives a repository moving host.
666
+ */
667
+ for (const file of ownManifests.filter((f) => /(^|\/)Package\.resolved$/.test(f))) {
668
+ const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
669
+ for (const match of raw.matchAll(/"identity"\s*:\s*"([^"]+)"/g)) {
670
+ swiftDeps.push(match[1].toLowerCase());
671
+ }
672
+ }
654
673
  /**
655
674
  * .csproj, which is XML rather than JSON or one-entry-per-line.
656
675
  *
@@ -29,9 +29,74 @@ const REPORTER_DEPS = [
29
29
  'trackjs',
30
30
  ];
31
31
  const REPORTER_PY_DEPS = ['sentry-sdk', 'rollbar', 'bugsnag'];
32
+ /**
33
+ * The same capability, in the ecosystems where the browser does not exist.
34
+ *
35
+ * This detector was written for code running in a tab and then made `required` for the
36
+ * mobile-app profile, where none of what it reads can occur: an iOS application has no
37
+ * `window.onerror` and no npm dependency. Three real repositories measured — DuckDuckGo
38
+ * iOS, thunderbird-android, WordPress-iOS — and all three were told at `high` that a
39
+ * crash on someone's phone reaches nobody. A check whose answer cannot vary is not a
40
+ * measure.
41
+ *
42
+ * Coordinates rather than words, matched as substrings by the helpers: `io.sentry` also
43
+ * covers `io.sentry:sentry-android`, and `sentry-cocoa` is how SPM names the package
44
+ * whatever the product is called.
45
+ */
46
+ const REPORTER_GRADLE_DEPS = [
47
+ 'io.sentry',
48
+ 'com.google.firebase:firebase-crashlytics',
49
+ 'com.bugsnag',
50
+ 'com.microsoft.appcenter:appcenter-crashes',
51
+ 'ch.acra:acra',
52
+ 'io.embrace',
53
+ 'com.datadoghq:dd-sdk-android',
54
+ 'com.instabug',
55
+ ];
56
+ const REPORTER_SWIFT_DEPS = [
57
+ 'sentry-cocoa',
58
+ 'sentry-cocoa-spm',
59
+ 'firebase-ios-sdk',
60
+ 'firebasecrashlytics',
61
+ 'bugsnag-cocoa',
62
+ 'appcenter-sdk-apple',
63
+ 'instabug',
64
+ 'embrace-apple-sdk',
65
+ ];
66
+ const REPORTER_DART_DEPS = ['sentry_flutter', 'firebase_crashlytics', 'bugsnag_flutter'];
67
+ /**
68
+ * Handlers the platform defines, which no author names.
69
+ *
70
+ * `NSSetUncaughtExceptionHandler` is Foundation, `Thread.setDefaultUncaughtExceptionHandler`
71
+ * is the JVM, `FlutterError.onError` is Flutter. Each is the one place its platform
72
+ * lets a program learn that it is about to die, so a call to it is the capability
73
+ * itself rather than a word that tends to accompany it — thunderbird-android installs
74
+ * one and was reported as having nothing.
75
+ *
76
+ * `SentrySDK.start` and `FirebaseCrashlytics` are here as well as in the dependency
77
+ * lists: a repository can use a reporter whose manifest is in another repository, and
78
+ * the call is still a call.
79
+ */
80
+ const PLATFORM_CRASH_HANDLERS = [
81
+ /NSSetUncaughtExceptionHandler\s*\(/,
82
+ // `(` or `{`: Kotlin writes the handler as a trailing lambda, which is how both real
83
+ // Android applications measured install theirs.
84
+ /setDefaultUncaughtExceptionHandler\s*[({]/,
85
+ /FlutterError\s*\.\s*onError\s*=/,
86
+ /PlatformDispatcher\s*\.\s*instance\s*\.\s*onError\s*=/,
87
+ /SentrySDK\s*\.\s*start\s*\(/,
88
+ /Sentry\s*\.\s*init\s*\(/,
89
+ /FirebaseCrashlytics|Crashlytics\s*\.\s*crashlytics\s*\(/,
90
+ ];
32
91
  async function detectErrorReporting(ctx) {
33
92
  const evidence = [];
34
- const deps = [...(0, detectContext_1.hasAnyDep)(ctx, REPORTER_DEPS), ...(0, detectContext_1.hasAnyPyDep)(ctx, REPORTER_PY_DEPS)];
93
+ const deps = [
94
+ ...(0, detectContext_1.hasAnyDep)(ctx, REPORTER_DEPS),
95
+ ...(0, detectContext_1.hasAnyPyDep)(ctx, REPORTER_PY_DEPS),
96
+ ...(0, detectContext_1.hasAnyGradleDep)(ctx, REPORTER_GRADLE_DEPS),
97
+ ...(0, detectContext_1.hasAnySwiftDep)(ctx, REPORTER_SWIFT_DEPS),
98
+ ...(0, detectContext_1.hasAnyDartDep)(ctx, REPORTER_DART_DEPS),
99
+ ];
35
100
  for (const dep of deps)
36
101
  evidence.push({ type: 'dependency', value: dep });
37
102
  /**
@@ -44,6 +109,7 @@ async function detectErrorReporting(ctx) {
44
109
  /window\.addEventListener\s*\(\s*['"]unhandledrejection['"]/,
45
110
  /window\.onerror\s*=/,
46
111
  /componentDidCatch\s*\([^)]*\)\s*\{[^}]*(fetch|report|log)/s,
112
+ ...PLATFORM_CRASH_HANDLERS,
47
113
  ], 10);
48
114
  for (const hit of handlers) {
49
115
  evidence.push({ type: 'snippet', value: hit.snippet, file: hit.file, line: hit.line });
@@ -54,7 +120,7 @@ async function detectErrorReporting(ctx) {
54
120
  // A reporting service is wired up once and covers everything; a hand-rolled handler
55
121
  // usually covers what its author remembered.
56
122
  complete: deps.length > 0,
57
- evidence: (0, absenceEvidence_1.evidenceOrSearch)(evidence, 'somewhere a crash in the browser is sent', ['@sentry/browser', '@sentry/react', '@bugsnag/js', 'rollbar', 'logrocket', '@datadog/browser-rum', 'sentry-sdk', 'window.onerror', 'addEventListener("error")', 'addEventListener("unhandledrejection")']),
123
+ evidence: (0, absenceEvidence_1.evidenceOrSearch)(evidence, 'somewhere a crash in the browser is sent', ['@sentry/browser', '@sentry/react', '@bugsnag/js', 'rollbar', 'logrocket', '@datadog/browser-rum', 'sentry-sdk', 'window.onerror', 'addEventListener("error")', 'addEventListener("unhandledrejection")', 'NSSetUncaughtExceptionHandler', 'Thread.setDefaultUncaughtExceptionHandler', 'FlutterError.onError', 'sentry-cocoa', 'io.sentry', 'firebase-crashlytics']),
58
124
  details: { services: deps, handlers: handlers.length },
59
125
  };
60
126
  }
@@ -82,6 +82,26 @@ const OFFLINE_SWIFT = [
82
82
  'realm/realm-cocoa',
83
83
  'realmswift',
84
84
  ];
85
+ /**
86
+ * Local storage the platform itself provides, which no dependency list will hold.
87
+ *
88
+ * Every entry in the lists above is a third-party database, and the two most widely
89
+ * used stores on these platforms ship with the operating system: Core Data on Apple's,
90
+ * `SQLiteDatabase` on Android's. Measured on two real applications whose whole purpose
91
+ * is working without a network — WordPress-iOS keeps its posts in Core Data,
92
+ * thunderbird-android keeps its mail in SQLite — and both were told at `high` that they
93
+ * store nothing locally.
94
+ *
95
+ * These are types the platform defines, not names an author picked: `NSManagedObjectContext`
96
+ * belongs to Core Data and appears nowhere else, and `getWritableDatabase()` is the one
97
+ * way an Android application opens its own database.
98
+ */
99
+ const PLATFORM_LOCAL_STORES = [
100
+ /NSPersistentContainer|NSManagedObjectContext|NSPersistentStoreCoordinator/,
101
+ /ModelContainer\s*\(|@Model\b/,
102
+ /SQLiteOpenHelper|getWritableDatabase\s*\(|getReadableDatabase\s*\(/,
103
+ /android\.database\.sqlite\.SQLiteDatabase/,
104
+ ];
85
105
  /** Dependencies that put a secret somewhere the operating system protects. */
86
106
  const SECURE_STORAGE_DEPS = [
87
107
  'flutter_secure_storage',
@@ -315,8 +335,15 @@ async function detectMobile(ctx) {
315
335
  * connection is gone is a different thing from one that shows a spinner forever.
316
336
  */
317
337
  const connectivityChecks = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [/Connectivity\(\)/, /connectivity_plus/, /NetInfo\./, /navigator\.onLine/, /NWPathMonitor/, /isReachable/], 3);
338
+ const platformStores = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, PLATFORM_LOCAL_STORES, 3);
318
339
  const offlineEvidence = [
319
340
  ...offlineDeps.map((dep) => ({ type: 'dependency', value: dep })),
341
+ ...platformStores.map((match) => ({
342
+ type: 'snippet',
343
+ value: match.snippet,
344
+ file: match.file,
345
+ line: match.line,
346
+ })),
320
347
  ...connectivityChecks.map((match) => ({
321
348
  type: 'snippet',
322
349
  value: match.snippet,
@@ -361,8 +388,8 @@ async function detectMobile(ctx) {
361
388
  },
362
389
  {
363
390
  key: 'mobile.offline',
364
- present: offlineDeps.length > 0,
365
- evidence: (0, absenceEvidence_1.evidenceOrSearch)(offlineEvidence, 'a local database the app can read with no network', ['sqflite', 'drift', 'hive', 'isar', 'objectbox', 'realm', 'androidx.room', 'sqldelight', 'grdb.swift', 'sqlite.swift']),
391
+ present: offlineDeps.length > 0 || platformStores.length > 0,
392
+ evidence: (0, absenceEvidence_1.evidenceOrSearch)(offlineEvidence, 'a local database the app can read with no network', ['sqflite', 'drift', 'hive', 'isar', 'objectbox', 'realm', 'androidx.room', 'sqldelight', 'grdb.swift', 'sqlite.swift', 'NSManagedObjectContext', 'SQLiteOpenHelper', 'getWritableDatabase(']),
366
393
  },
367
394
  {
368
395
  key: 'mobile.forcedUpdate',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.74.0",
3
+ "version": "0.75.0",
4
4
  "description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
5
5
  "license": "MIT",
6
6
  "bin": {