@reekon-tools/react-native-pdf-canvas 0.1.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.
Files changed (161) hide show
  1. package/PdfCanvas.podspec +80 -0
  2. package/README.md +1391 -0
  3. package/android/build.gradle +175 -0
  4. package/android/consumer-rules.pro +3 -0
  5. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  6. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  7. package/android/gradle.properties +2 -0
  8. package/android/gradlew +248 -0
  9. package/android/gradlew.bat +98 -0
  10. package/android/settings.gradle +34 -0
  11. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +226 -0
  12. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +239 -0
  13. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +251 -0
  14. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +221 -0
  15. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +783 -0
  16. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +1388 -0
  17. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +98 -0
  18. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +324 -0
  19. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +775 -0
  20. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +20 -0
  21. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +558 -0
  22. package/android/src/jsi/cpp/CMakeLists.txt +33 -0
  23. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +469 -0
  24. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +140 -0
  25. package/android/src/main/AndroidManifest.xml +7 -0
  26. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +45 -0
  27. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +564 -0
  28. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +33 -0
  29. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +277 -0
  30. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +214 -0
  31. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +30 -0
  32. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +234 -0
  33. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +47 -0
  34. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +143 -0
  35. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +95 -0
  36. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +626 -0
  37. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasPackage.java +33 -0
  38. package/android/tools/compile-gate.sh +289 -0
  39. package/dist/cache.d.ts +161 -0
  40. package/dist/cache.js +389 -0
  41. package/dist/clock.d.ts +21 -0
  42. package/dist/clock.js +31 -0
  43. package/dist/controller.d.ts +131 -0
  44. package/dist/controller.js +1002 -0
  45. package/dist/geometry.d.ts +96 -0
  46. package/dist/geometry.js +248 -0
  47. package/dist/index.d.ts +52 -0
  48. package/dist/index.js +50 -0
  49. package/dist/index.native.d.ts +21 -0
  50. package/dist/index.native.js +23 -0
  51. package/dist/index.web.d.ts +24 -0
  52. package/dist/index.web.js +26 -0
  53. package/dist/layout.d.ts +78 -0
  54. package/dist/layout.js +152 -0
  55. package/dist/peer-check.d.ts +83 -0
  56. package/dist/peer-check.js +183 -0
  57. package/dist/planner.d.ts +35 -0
  58. package/dist/planner.js +530 -0
  59. package/dist/policy.d.ts +14 -0
  60. package/dist/policy.js +168 -0
  61. package/dist/rasterizer/android.d.ts +121 -0
  62. package/dist/rasterizer/android.js +126 -0
  63. package/dist/rasterizer/buffer.d.ts +18 -0
  64. package/dist/rasterizer/buffer.js +18 -0
  65. package/dist/rasterizer/fake.d.ts +92 -0
  66. package/dist/rasterizer/fake.js +433 -0
  67. package/dist/rasterizer/index.d.ts +38 -0
  68. package/dist/rasterizer/index.js +69 -0
  69. package/dist/rasterizer/index.native.d.ts +19 -0
  70. package/dist/rasterizer/index.native.js +80 -0
  71. package/dist/rasterizer/index.web.d.ts +63 -0
  72. package/dist/rasterizer/index.web.js +75 -0
  73. package/dist/rasterizer/ingest.d.ts +180 -0
  74. package/dist/rasterizer/ingest.js +307 -0
  75. package/dist/rasterizer/ios.d.ts +84 -0
  76. package/dist/rasterizer/ios.js +155 -0
  77. package/dist/rasterizer/native-bridge.d.ts +261 -0
  78. package/dist/rasterizer/native-bridge.js +413 -0
  79. package/dist/rasterizer/web/client.d.ts +84 -0
  80. package/dist/rasterizer/web/client.js +359 -0
  81. package/dist/rasterizer/web/engine.d.ts +69 -0
  82. package/dist/rasterizer/web/engine.js +308 -0
  83. package/dist/rasterizer/web/index.d.ts +26 -0
  84. package/dist/rasterizer/web/index.js +21 -0
  85. package/dist/rasterizer/web/pdfium.d.ts +213 -0
  86. package/dist/rasterizer/web/pdfium.js +170 -0
  87. package/dist/rasterizer/web/protocol.d.ts +138 -0
  88. package/dist/rasterizer/web/protocol.js +37 -0
  89. package/dist/rasterizer/web/session.d.ts +106 -0
  90. package/dist/rasterizer/web/session.js +413 -0
  91. package/dist/rasterizer/web/worker.d.ts +44 -0
  92. package/dist/rasterizer/web/worker.js +76 -0
  93. package/dist/react/PdfContentView.d.ts +98 -0
  94. package/dist/react/PdfContentView.js +189 -0
  95. package/dist/react/index.d.ts +6 -0
  96. package/dist/react/index.js +3 -0
  97. package/dist/react/usePdfDocument.d.ts +134 -0
  98. package/dist/react/usePdfDocument.js +275 -0
  99. package/dist/react/usePdfLayer.d.ts +94 -0
  100. package/dist/react/usePdfLayer.js +388 -0
  101. package/dist/skia/index.d.ts +26 -0
  102. package/dist/skia/index.js +25 -0
  103. package/dist/skia/index.native.d.ts +14 -0
  104. package/dist/skia/index.native.js +16 -0
  105. package/dist/skia/index.web.d.ts +12 -0
  106. package/dist/skia/index.web.js +14 -0
  107. package/dist/skia/resolve.native.d.ts +18 -0
  108. package/dist/skia/resolve.native.js +22 -0
  109. package/dist/skia/resolve.web.d.ts +41 -0
  110. package/dist/skia/resolve.web.js +62 -0
  111. package/dist/skia/skia-api.d.ts +94 -0
  112. package/dist/skia/skia-api.js +113 -0
  113. package/dist/testing/index.d.ts +27 -0
  114. package/dist/testing/index.js +28 -0
  115. package/dist/testing/scenes.d.ts +100 -0
  116. package/dist/testing/scenes.js +267 -0
  117. package/dist/types.d.ts +611 -0
  118. package/dist/types.js +21 -0
  119. package/dist/web-init.d.ts +68 -0
  120. package/dist/web-init.js +64 -0
  121. package/ios/Package.swift +58 -0
  122. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +743 -0
  123. package/ios/Sources/PdfCanvasBridge/include/PdfCanvasModule.h +36 -0
  124. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +38 -0
  125. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +389 -0
  126. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +57 -0
  127. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +44 -0
  128. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +82 -0
  129. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +37 -0
  130. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +158 -0
  131. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +80 -0
  132. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +65 -0
  133. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +548 -0
  134. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +48 -0
  135. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +26 -0
  136. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +122 -0
  137. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +80 -0
  138. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +79 -0
  139. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +129 -0
  140. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +100 -0
  141. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +65 -0
  142. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +111 -0
  143. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +85 -0
  144. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +163 -0
  145. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +297 -0
  146. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +140 -0
  147. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +358 -0
  148. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +262 -0
  149. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +173 -0
  150. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +114 -0
  151. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +158 -0
  152. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +544 -0
  153. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +287 -0
  154. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +396 -0
  155. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +639 -0
  156. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +179 -0
  157. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +283 -0
  158. package/package.json +144 -0
  159. package/react-native.config.js +40 -0
  160. package/scripts/check-skia-peer.mjs +86 -0
  161. package/scripts/skia-peer-check.mjs +363 -0
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `bin: pdf-canvas-check-skia` — the install-time gate.
4
+ *
5
+ * Thin on purpose: every branch worth testing lives in `skia-peer-check.mjs`,
6
+ * which does no I/O and never exits. This file is the only place a process exit
7
+ * code is chosen.
8
+ *
9
+ * HOW A CONSUMER WIRES IT UP, and why there are two ways:
10
+ *
11
+ * registry / tarball dependency
12
+ * `bin` is linked into `node_modules/.bin`, so the consumer's postinstall
13
+ * can just say `pdf-canvas-check-skia`.
14
+ *
15
+ * yarn 1 `link:` dependency (how BOLDR consumes this package)
16
+ * MEASURED: a `link:` dependency gets NO `.bin` entries at all, and none of
17
+ * its own lifecycle scripts run. The bin name does not exist. Invoke the
18
+ * file by path instead — Node follows the symlink (and the Windows junction)
19
+ * even though Metro will not:
20
+ *
21
+ * node node_modules/@reekon-tools/react-native-pdf-canvas/scripts/check-skia-peer.mjs
22
+ *
23
+ * That path form works for BOTH kinds of consumer, which is why it is what
24
+ * BOLDR's own `postinstall` uses.
25
+ *
26
+ * `--strict` promotes every 'unknown'/'prerelease-below' warning to a failure.
27
+ * Off by default because an unresolvable peer is more often this check standing
28
+ * in the wrong directory than a genuinely broken app, and a false `yarn install`
29
+ * failure costs the whole team. CI that owns its own layout can turn it on.
30
+ *
31
+ * The outer try/catch is not defensive decoration. An unhandled throw here is a
32
+ * non-zero exit, i.e. a broken install for a reason that has nothing to do with
33
+ * skia's version.
34
+ */
35
+
36
+ import process from 'node:process';
37
+
38
+ import {
39
+ PEER_NAME,
40
+ evaluate,
41
+ loadSemver,
42
+ readOwnFloor,
43
+ report,
44
+ resolveConsumerSkia,
45
+ } from './skia-peer-check.mjs';
46
+
47
+ const strict = process.argv.includes('--strict');
48
+
49
+ function main() {
50
+ const {raw: floorRange, name} = readOwnFloor();
51
+ const selfName = name ?? '@reekon-tools/react-native-pdf-canvas';
52
+ const found = resolveConsumerSkia();
53
+
54
+ const verdict =
55
+ found.version === null
56
+ ? {status: 'unknown', message: found.reason, floor: floorRange}
57
+ : evaluate({
58
+ found: found.version,
59
+ floorRange,
60
+ semver: loadSemver(),
61
+ });
62
+
63
+ const {code, lines} = report(verdict, {selfName, floorRange});
64
+ for (const line of lines) {
65
+ (code === 0 ? console.warn : console.error)(line);
66
+ }
67
+
68
+ if (code !== 0) return code;
69
+ if (strict && verdict.status !== 'ok') {
70
+ console.error(` --strict: refusing to pass on "${verdict.status}".`);
71
+ return 1;
72
+ }
73
+ return 0;
74
+ }
75
+
76
+ let code = 0;
77
+ try {
78
+ code = main();
79
+ } catch (error) {
80
+ console.warn(
81
+ ` [pdf-canvas] could not check the ${PEER_NAME} peer floor: ` +
82
+ `${error && error.message ? error.message : error}`,
83
+ );
84
+ code = strict ? 1 : 0;
85
+ }
86
+ process.exit(code);
@@ -0,0 +1,363 @@
1
+ /**
2
+ * The skia peer-floor check — pure half.
3
+ *
4
+ * Plain `.mjs`, deliberately: this runs from a CONSUMER'S `postinstall`, at
5
+ * which point `dist/` may not exist. Under yarn 1's `link:` protocol the linked
6
+ * package's own lifecycle scripts never run at all (measured — see README,
7
+ * "Enforcing the floor"), so `prepare` has NOT built anything and anything
8
+ * importing from `dist/` would fail as a missing module. `scripts/` is shipped
9
+ * verbatim in `files` and needs no build step.
10
+ *
11
+ * ---------------------------------------------------------------------------
12
+ * THE FLOOR IS WRITTEN ONCE, IN `peerDependencies`.
13
+ * ---------------------------------------------------------------------------
14
+ *
15
+ * This module does not restate `2.8.0`. It reads the range out of the package's
16
+ * own `peerDependencies["@shopify/react-native-skia"]`, so the gate and the
17
+ * published contract cannot drift apart — a floor bump is a one-line edit to
18
+ * package.json. `parseFloor` accepts ONLY a bare `>=x.y.z`, and returns null for
19
+ * anything else, which downgrades the whole check to a warning rather than
20
+ * letting a compound range be silently misread as its first clause.
21
+ *
22
+ * ---------------------------------------------------------------------------
23
+ * WHICH COPY OF SKIA GETS CHECKED — the trap this module exists to avoid.
24
+ * ---------------------------------------------------------------------------
25
+ *
26
+ * Resolution must start from the CONSUMER, never from this file. Measured in
27
+ * the BOLDR tree, where the app pins skia 2.6.2 and this package's own dev tree
28
+ * pins 2.11.0:
29
+ *
30
+ * createRequire(<package dir>) -> 2.11.0 <- the dev copy. A FALSE PASS.
31
+ * createRequire(<consumer cwd>) -> 2.6.2 <- the version the app ships.
32
+ *
33
+ * Under `link:` the package directory is a symlink to a sibling checkout that
34
+ * has its own `node_modules`, so the wrong-direction resolution does not fail —
35
+ * it succeeds, with the answer the author wants to hear. `resolveConsumerSkia`
36
+ * therefore tries `INIT_CWD` then `process.cwd()`, and refuses to believe a
37
+ * resolution that landed inside this package while the consumer is somewhere
38
+ * else. Node's resolver walks parent directories, so a monorepo hoist above the
39
+ * consumer root is found for free.
40
+ *
41
+ * ---------------------------------------------------------------------------
42
+ * A CRASH HERE IS A BROKEN `yarn install` FOR THE WHOLE APP.
43
+ * ---------------------------------------------------------------------------
44
+ *
45
+ * So the verdict has three outcomes, not two, and only ONE of them is fatal:
46
+ *
47
+ * 'ok' -> exit 0, silent
48
+ * 'too-low' -> exit 1. A release version provably below the floor.
49
+ * 'prerelease-below' -> warn, exit 0.
50
+ * 'unknown' -> warn, exit 0. Skia absent, unparseable version,
51
+ * unparseable floor, self-resolution, thrown error.
52
+ *
53
+ * `prerelease-below` follows reanimated's own precedent verbatim
54
+ * (`scripts/validate-worklets-version.js`: "Don't perform any checks for
55
+ * pre-release versions, like nightlies or feature previews. The user knows what
56
+ * they're doing."). It also defends against a real semver footgun: strict
57
+ * `satisfies('2.12.0-next.1', '>=2.8.0')` is FALSE, because a prerelease only
58
+ * satisfies a range that names its own [major,minor,patch] tuple. Failing an
59
+ * install for being too NEW is exactly the wrong reason to fail, so a
60
+ * prerelease can only ever warn.
61
+ */
62
+
63
+ import {createRequire} from 'node:module';
64
+ import path from 'node:path';
65
+ import {fileURLToPath} from 'node:url';
66
+
67
+ export const PEER_NAME = '@shopify/react-native-skia';
68
+
69
+ /** This package's root, i.e. the parent of `scripts/`. */
70
+ export const PACKAGE_ROOT = path.resolve(
71
+ path.dirname(fileURLToPath(import.meta.url)),
72
+ '..',
73
+ );
74
+
75
+ /**
76
+ * A bare `>=x.y.z` and nothing else. Anything more expressive (a caret, an ` || `
77
+ * union, a prerelease floor) returns null and the check degrades to a warning —
78
+ * see the header. This is a deliberate refusal to guess.
79
+ */
80
+ const FLOOR_SHAPE = /^>=\s*(\d+)\.(\d+)\.(\d+)$/;
81
+
82
+ const VERSION_SHAPE =
83
+ /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
84
+
85
+ /** @typedef {{range: string, tuple: [number, number, number]}} Floor */
86
+ /** @typedef {{tuple: [number, number, number], prerelease: string | null}} Parsed */
87
+
88
+ /** @returns {Floor | null} */
89
+ export function parseFloor(range) {
90
+ if (typeof range !== 'string') return null;
91
+ const m = FLOOR_SHAPE.exec(range.trim());
92
+ if (m === null) return null;
93
+ return {range: range.trim(), tuple: [+m[1], +m[2], +m[3]]};
94
+ }
95
+
96
+ /** @returns {Parsed | null} */
97
+ export function parseVersion(version) {
98
+ if (typeof version !== 'string') return null;
99
+ const m = VERSION_SHAPE.exec(version.trim());
100
+ if (m === null) return null;
101
+ return {tuple: [+m[1], +m[2], +m[3]], prerelease: m[4] ?? null};
102
+ }
103
+
104
+ function compareTuple(a, b) {
105
+ for (let i = 0; i < 3; i++) {
106
+ if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
107
+ }
108
+ return 0;
109
+ }
110
+
111
+ /**
112
+ * `semver.satisfies(version, '>=x.y.z', {includePrerelease: true})`,
113
+ * reimplemented in six lines because the package ships no runtime dependencies
114
+ * and a `link:` consumer would not get one installed even if it did.
115
+ *
116
+ * The one non-obvious rule, and the reason this is not a bare tuple compare:
117
+ * for an EQUAL tuple a prerelease is LOWER, so `2.8.0-next.1` does not meet a
118
+ * `>=2.8.0` floor while `2.12.0-next.1` does. `peer-check.test.ts` asserts this
119
+ * function agrees with the real `semver` over a table that includes every
120
+ * prerelease in skia's published history.
121
+ *
122
+ * @param {Parsed} v
123
+ * @param {Floor} floor
124
+ */
125
+ export function meetsFloor(v, floor) {
126
+ const c = compareTuple(v.tuple, floor.tuple);
127
+ if (c !== 0) return c > 0;
128
+ return v.prerelease === null;
129
+ }
130
+
131
+ /**
132
+ * Use the real `semver` when the tree happens to have one, and fall back
133
+ * otherwise. Both arms are exercised in the wild — a `link:` consumer resolves
134
+ * this package's dev copy, a tarball consumer with no semver anywhere gets the
135
+ * fallback — so `peer-check.test.ts` pins them to the same answers rather than
136
+ * trusting that they agree.
137
+ */
138
+ export function loadSemver(requireFrom = createRequire(import.meta.url)) {
139
+ try {
140
+ const satisfies = requireFrom('semver/functions/satisfies');
141
+ return typeof satisfies === 'function' ? {satisfies} : null;
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * @param {Parsed} v
149
+ * @param {Floor} floor
150
+ */
151
+ export function meetsFloorVia(semver, rawVersion, v, floor) {
152
+ if (semver !== null) {
153
+ try {
154
+ return semver.satisfies(rawVersion, floor.range, {
155
+ includePrerelease: true,
156
+ });
157
+ } catch {
158
+ // fall through to the built-in comparison
159
+ }
160
+ }
161
+ return meetsFloor(v, floor);
162
+ }
163
+
164
+ /**
165
+ * Roots to resolve the peer from, most authoritative first. `INIT_CWD` is set by
166
+ * yarn 1 and npm to the directory the install was invoked in; `process.cwd()` is
167
+ * what a `postinstall` actually runs in (measured: the consumer root, for both a
168
+ * `link:` and a tarball dependency).
169
+ */
170
+ export function consumerRoots(env = process.env, cwd = process.cwd()) {
171
+ const roots = [];
172
+ for (const candidate of [env['INIT_CWD'], cwd]) {
173
+ if (typeof candidate !== 'string' || candidate.length === 0) continue;
174
+ const resolved = path.resolve(candidate);
175
+ if (!roots.includes(resolved)) roots.push(resolved);
176
+ }
177
+ return roots;
178
+ }
179
+
180
+ const isInside = (child, parent) => {
181
+ const rel = path.relative(parent, child);
182
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
183
+ };
184
+
185
+ /**
186
+ * `isInside` OR equal, and the difference is a real bug that the suite caught:
187
+ * the consumer root can legitimately BE this package's root (`yarn check:peers`
188
+ * in the dev tree). Treating that as "not our tree" made the false-pass guard
189
+ * fire on the one case where reading our own copy is exactly right.
190
+ */
191
+ const isAtOrInside = (child, parent) =>
192
+ path.resolve(child) === path.resolve(parent) || isInside(child, parent);
193
+
194
+ /**
195
+ * Find the consumer's installed peer.
196
+ *
197
+ * @returns {{version: string, from: string, dir: string}
198
+ * | {version: null, reason: string}}
199
+ */
200
+ export function resolveConsumerSkia({
201
+ roots = consumerRoots(),
202
+ packageRoot = PACKAGE_ROOT,
203
+ makeRequire = root => createRequire(path.join(root, 'noop.js')),
204
+ } = {}) {
205
+ const tried = [];
206
+ for (const root of roots) {
207
+ let manifestPath;
208
+ try {
209
+ manifestPath = makeRequire(root).resolve(`${PEER_NAME}/package.json`);
210
+ } catch {
211
+ tried.push(root);
212
+ continue;
213
+ }
214
+ const dir = path.dirname(manifestPath);
215
+ // The measured false-pass guard: a resolution that landed in our own dev
216
+ // tree tells us about US, not about the consumer.
217
+ if (isInside(dir, packageRoot) && !isAtOrInside(root, packageRoot)) {
218
+ return {
219
+ version: null,
220
+ reason:
221
+ `${PEER_NAME} resolved to ${dir}, which is inside this package's ` +
222
+ `own dev tree rather than the consumer's (${root}). That copy is ` +
223
+ 'not what the app ships, so no verdict is possible.',
224
+ };
225
+ }
226
+ let version;
227
+ try {
228
+ version = makeRequire(root)(`${PEER_NAME}/package.json`).version;
229
+ } catch (error) {
230
+ return {
231
+ version: null,
232
+ reason: `${PEER_NAME}/package.json at ${dir} could not be read (${
233
+ error && error.message ? error.message : error
234
+ }).`,
235
+ };
236
+ }
237
+ if (typeof version !== 'string') {
238
+ return {
239
+ version: null,
240
+ reason: `${PEER_NAME}/package.json at ${dir} has no "version" string.`,
241
+ };
242
+ }
243
+ return {version, from: root, dir};
244
+ }
245
+ return {
246
+ version: null,
247
+ reason:
248
+ `${PEER_NAME} is not resolvable from ${
249
+ tried.length > 0 ? tried.join(' or ') : 'anywhere'
250
+ }. It is a REQUIRED peer, not an optional one — but "not installed yet" ` +
251
+ 'and "installed somewhere this check cannot see" are indistinguishable ' +
252
+ 'from here, so this is a warning.',
253
+ };
254
+ }
255
+
256
+ /** Read our own declared floor. Never throws. */
257
+ export function readOwnFloor(
258
+ manifestPath = path.join(PACKAGE_ROOT, 'package.json'),
259
+ ) {
260
+ try {
261
+ const manifest = createRequire(import.meta.url)(manifestPath);
262
+ const peers = manifest && manifest.peerDependencies;
263
+ return {
264
+ raw: peers ? peers[PEER_NAME] : undefined,
265
+ name: manifest && manifest.name,
266
+ };
267
+ } catch (error) {
268
+ return {raw: undefined, name: undefined, error};
269
+ }
270
+ }
271
+
272
+ /**
273
+ * The whole decision, as data. No I/O, no `process.exit` — that is the CLI's
274
+ * job, and keeping them apart is what makes every branch below testable.
275
+ *
276
+ * @returns {{status: 'ok' | 'too-low' | 'prerelease-below' | 'unknown',
277
+ * message: string, found?: string, floor?: string}}
278
+ */
279
+ export function evaluate({found, floorRange, packageName = PEER_NAME, semver}) {
280
+ const floor = parseFloor(floorRange);
281
+ if (floor === null) {
282
+ return {
283
+ status: 'unknown',
284
+ message:
285
+ `Cannot check ${PEER_NAME}: this package declares the peer range ` +
286
+ `${JSON.stringify(floorRange)}, and this check only understands a ` +
287
+ 'bare ">=x.y.z". Refusing to guess.',
288
+ };
289
+ }
290
+ if (found === null || found === undefined) {
291
+ return {status: 'unknown', message: 'No installed version was resolved.'};
292
+ }
293
+ const parsed = parseVersion(found);
294
+ if (parsed === null) {
295
+ return {
296
+ status: 'unknown',
297
+ message:
298
+ `Cannot check ${PEER_NAME}: the installed version ` +
299
+ `${JSON.stringify(found)} is not a semver release or prerelease.`,
300
+ floor: floor.range,
301
+ };
302
+ }
303
+ if (meetsFloorVia(semver ?? null, found, parsed, floor)) {
304
+ return {
305
+ status: 'ok',
306
+ message: `${PEER_NAME} ${found} satisfies ${floor.range}.`,
307
+ found,
308
+ floor: floor.range,
309
+ };
310
+ }
311
+ if (parsed.prerelease !== null) {
312
+ return {
313
+ status: 'prerelease-below',
314
+ message:
315
+ `${PEER_NAME} ${found} is a PRERELEASE below ${floor.range}. Not ` +
316
+ 'failing the install — a prerelease is an explicit opt-in, and ' +
317
+ 'strict semver would also reject a prerelease of a NEWER minor. ' +
318
+ 'Verify it yourself.',
319
+ found,
320
+ floor: floor.range,
321
+ };
322
+ }
323
+ return {
324
+ status: 'too-low',
325
+ message: `${PEER_NAME} ${found} is below the required ${floor.range}.`,
326
+ found,
327
+ floor: floor.range,
328
+ packageName,
329
+ };
330
+ }
331
+
332
+ /**
333
+ * The exit code and the text the developer actually reads. Actionable means all
334
+ * three of: what was found, what is required, and the command that fixes it.
335
+ *
336
+ * @returns {{code: 0 | 1, lines: string[]}}
337
+ */
338
+ export function report(verdict, {selfName = PEER_NAME, floorRange} = {}) {
339
+ const floor = verdict.floor ?? floorRange ?? '';
340
+ if (verdict.status === 'ok') return {code: 0, lines: []};
341
+ if (verdict.status === 'too-low') {
342
+ return {
343
+ code: 1,
344
+ lines: [
345
+ '',
346
+ ` ${selfName} requires ${PEER_NAME} ${floor}.`,
347
+ ` Found: ${verdict.found}`,
348
+ '',
349
+ ` Below ${floor} the RN Web reconciler leaks one CanvasKit Paint per`,
350
+ ' drawn tile per frame into the Emscripten heap, which never shrinks.',
351
+ " This package's tiling makes that leak proportional to the number of",
352
+ ' pieces on screen, so its memory policy cannot hold there.',
353
+ '',
354
+ ` Fix: yarn add ${PEER_NAME}@^${floor.replace(/^>=\s*/, '')}`,
355
+ '',
356
+ ],
357
+ };
358
+ }
359
+ return {
360
+ code: 0,
361
+ lines: ['', ` [${selfName}] ${verdict.message}`, ''],
362
+ };
363
+ }