@qvac/sdk 0.20.1 → 0.21.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 (104) hide show
  1. package/NOTICE +76 -13
  2. package/README.md +39 -5
  3. package/dist/examples/advisory-model-fit.d.ts +17 -16
  4. package/dist/examples/advisory-model-fit.js +18 -18
  5. package/dist/examples/asr/parakeet-indic-conformer-filesystem.js +3 -3
  6. package/dist/examples/asr/parakeet-unified-coreml.d.ts +2 -0
  7. package/dist/examples/asr/parakeet-unified-coreml.js +113 -0
  8. package/dist/examples/assess-model-fit.js +9 -4
  9. package/dist/examples/model-fit-ui/server.js +5 -2
  10. package/dist/examples/rpc-inference.d.ts +2 -0
  11. package/dist/examples/rpc-inference.js +51 -0
  12. package/dist/examples/rpc-server.d.ts +2 -0
  13. package/dist/examples/rpc-server.js +32 -0
  14. package/dist/examples/tools/llamacpp-deferred-tools.d.ts +2 -0
  15. package/dist/examples/tools/llamacpp-deferred-tools.js +77 -0
  16. package/dist/examples/tts/audio8.js +4 -0
  17. package/dist/examples/tts/moss-dialogue.d.ts +2 -0
  18. package/dist/examples/tts/moss-dialogue.js +63 -0
  19. package/dist/examples/tts/moss.d.ts +2 -0
  20. package/dist/examples/tts/moss.js +71 -0
  21. package/dist/scripts/contract/build-contract.d.ts +2 -1
  22. package/dist/scripts/contract/build-contract.js +4 -1
  23. package/dist/scripts/contract/build-numeric-constants.d.ts +15 -0
  24. package/dist/scripts/contract/build-numeric-constants.js +13 -0
  25. package/dist/scripts/contract/method-shapes.d.ts +3 -0
  26. package/dist/scripts/contract/method-shapes.js +3 -0
  27. package/dist/scripts/enforce-inference-versions.d.ts +18 -0
  28. package/dist/scripts/enforce-inference-versions.js +158 -0
  29. package/dist/scripts/generate-bare-kit-runtimes.d.ts +2 -0
  30. package/dist/scripts/generate-bare-kit-runtimes.js +106 -0
  31. package/dist/src/client/api/assess-model-fit.d.ts +5 -4
  32. package/dist/src/client/api/assess-model-fit.js +24 -7
  33. package/dist/src/client/api/bci-transcribe.d.ts +4 -1
  34. package/dist/src/client/api/get-system-resources.d.ts +8 -0
  35. package/dist/src/client/api/get-system-resources.js +8 -0
  36. package/dist/src/client/api/index.d.ts +1 -0
  37. package/dist/src/client/api/index.js +1 -0
  38. package/dist/src/client/api/rpc-server.d.ts +20 -0
  39. package/dist/src/client/api/rpc-server.js +45 -0
  40. package/dist/src/client/api/text-to-speech.d.ts +9 -0
  41. package/dist/src/client/api/text-to-speech.js +9 -0
  42. package/dist/src/client/api/transcribe.d.ts +21 -15
  43. package/dist/src/client/api/transcribe.js +65 -20
  44. package/dist/src/client/rpc/rpc-error.js +11 -7
  45. package/dist/src/commands/bundle/entry-gen.d.ts +2 -2
  46. package/dist/src/commands/bundle/entry-gen.js +8 -4
  47. package/dist/src/commands/bundle/index.d.ts +20 -0
  48. package/dist/src/commands/bundle/index.js +88 -12
  49. package/dist/src/commands/bundle/plugins.js +1 -1
  50. package/dist/src/commands/command-logger.d.ts +6 -0
  51. package/dist/src/commands/command-logger.js +12 -0
  52. package/dist/src/commands/host-prebuilds/index.d.ts +71 -0
  53. package/dist/src/commands/host-prebuilds/index.js +227 -0
  54. package/dist/src/commands/host-prebuilds/package-manager.d.ts +54 -0
  55. package/dist/src/commands/host-prebuilds/package-manager.js +278 -0
  56. package/dist/src/commands/index.d.ts +10 -1
  57. package/dist/src/commands/index.js +6 -0
  58. package/dist/src/commands/verify/abi.d.ts +22 -2
  59. package/dist/src/commands/verify/abi.js +49 -27
  60. package/dist/src/commands/verify/addon-source.d.ts +22 -0
  61. package/dist/src/commands/verify/addon-source.js +69 -3
  62. package/dist/src/commands/verify/bare-kit-runtime.d.ts +38 -0
  63. package/dist/src/commands/verify/bare-kit-runtime.js +201 -0
  64. package/dist/src/commands/verify/bare-kit-runtimes.d.ts +12 -0
  65. package/dist/src/commands/verify/bare-kit-runtimes.js +108 -0
  66. package/dist/src/commands/verify/bundle-source.js +16 -15
  67. package/dist/src/commands/verify/engines-advice.d.ts +73 -0
  68. package/dist/src/commands/verify/engines-advice.js +271 -0
  69. package/dist/src/commands/verify/index.d.ts +22 -0
  70. package/dist/src/commands/verify/index.js +151 -13
  71. package/dist/src/commands/verify/node-modules-source.js +43 -39
  72. package/dist/src/commands/verify/prebuilds.d.ts +19 -0
  73. package/dist/src/commands/verify/prebuilds.js +52 -13
  74. package/dist/src/expo/plugins/index.d.ts +2 -0
  75. package/dist/src/expo/plugins/withMobileBundle.d.ts +15 -5
  76. package/dist/src/expo/plugins/withMobileBundle.js +68 -42
  77. package/dist/src/expo/plugins/withQvacSDK.d.ts +7 -1
  78. package/dist/src/expo/plugins/withQvacSDK.js +6 -3
  79. package/dist/src/index.d.ts +5 -2
  80. package/dist/src/index.js +4 -2
  81. package/dist/src/schemas/sdk-errors-client.d.ts +2 -0
  82. package/dist/src/schemas/sdk-errors-client.js +16 -0
  83. package/dist/src/schemas/sdk-errors-server.d.ts +1 -0
  84. package/dist/src/schemas/sdk-errors-server.js +5 -0
  85. package/dist/src/utils/errors-client.d.ts +15 -0
  86. package/dist/src/utils/errors-client.js +26 -0
  87. package/dist/src/utils/errors-server.d.ts +1 -0
  88. package/dist/src/utils/errors-server.js +1 -0
  89. package/dist/src/utils/mcp-adapter.js +7 -1
  90. package/dist/src/utils/tool-helpers.d.ts +4 -0
  91. package/dist/src/utils/tool-helpers.js +2 -0
  92. package/dist/src/worker/ggml-rpc-server-provider.d.ts +2 -0
  93. package/dist/src/worker/ggml-rpc-server-provider.js +1 -0
  94. package/dist/src/worker/rpc-server-provider.d.ts +3 -0
  95. package/dist/src/worker/rpc-server-provider.js +1 -0
  96. package/package.json +30 -25
  97. package/dist/scripts/enforce-inference-peer-dependencies.d.ts +0 -2
  98. package/dist/scripts/enforce-inference-peer-dependencies.js +0 -39
  99. package/dist/src/deprecate.d.ts +0 -2
  100. package/dist/src/deprecate.js +0 -9
  101. package/dist/src/worker/legacy-worker-entry.d.ts +0 -2
  102. package/dist/src/worker/legacy-worker-entry.js +0 -3
  103. package/dist/src/worker/worker-core.d.ts +0 -7
  104. package/dist/src/worker/worker-core.js +0 -8
@@ -0,0 +1,271 @@
1
+ import semver from 'semver';
2
+ import { detectPackageManager } from '../host-prebuilds/package-manager.js';
3
+ import { formatRuntimeSource } from './abi.js';
4
+ import { findReactNativeBareKitUpgrade, NETWORK_TIMEOUT_MS, REACT_NATIVE_BARE_KIT } from './bare-kit-runtime.js';
5
+ import { REACT_NATIVE_BARE_KIT_RUNTIMES } from './bare-kit-runtimes.js';
6
+ const REGISTRY_URL = 'https://registry.npmjs.org';
7
+ /** The registry's abbreviated metadata lists `engines` per version at a fraction of the full size. */
8
+ const ABBREVIATED_METADATA_TYPE = 'application/vnd.npm.install-v1+json';
9
+ export async function fetchPackageMetadata(name) {
10
+ const url = `${REGISTRY_URL}/${encodeURIComponent(name)}`;
11
+ const response = await fetch(url, {
12
+ headers: { accept: ABBREVIATED_METADATA_TYPE },
13
+ signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS)
14
+ });
15
+ if (!response.ok)
16
+ throw new Error(`GET ${url} returned HTTP ${response.status}`);
17
+ return (await response.json());
18
+ }
19
+ export function formatOverrideSnippet(manager, name, version, parents) {
20
+ let manifest;
21
+ switch (manager) {
22
+ case 'pnpm':
23
+ manifest = {
24
+ pnpm: {
25
+ overrides: Object.fromEntries(parents.length > 0 ? parents.map((p) => [`${p}>${name}`, version]) : [[name, version]])
26
+ }
27
+ };
28
+ break;
29
+ case 'yarn':
30
+ manifest = {
31
+ resolutions: Object.fromEntries(parents.length > 0 ? parents.map((p) => [`${p}/${name}`, version]) : [[name, version]])
32
+ };
33
+ break;
34
+ case 'bun':
35
+ // bun only supports top-level overrides.
36
+ manifest = { overrides: { [name]: version } };
37
+ break;
38
+ default:
39
+ manifest = {
40
+ overrides: Object.fromEntries(parents.length > 0 ? parents.map((p) => [p, { [name]: version }]) : [[name, version]])
41
+ };
42
+ }
43
+ return JSON.stringify(manifest, null, 2).replace(/\{\n\s+("[^"\n]+": "[^"\n]+")\n\s+\}/g, '{ $1 }');
44
+ }
45
+ function groupFailures(failures) {
46
+ const byName = new Map();
47
+ for (const failure of failures) {
48
+ let entry = byName.get(failure.name);
49
+ if (entry === undefined) {
50
+ entry = { versions: new Set(), ranges: new Set() };
51
+ byName.set(failure.name, entry);
52
+ }
53
+ if (failure.version !== undefined)
54
+ entry.versions.add(failure.version);
55
+ entry.ranges.add(failure.enginesBare);
56
+ }
57
+ return byName;
58
+ }
59
+ function findParents(name, packages) {
60
+ const seen = new Set();
61
+ const parents = [];
62
+ for (const record of packages) {
63
+ const range = record.dependencies[name];
64
+ if (range === undefined)
65
+ continue;
66
+ const key = `${record.name}\0${range}`;
67
+ if (seen.has(key))
68
+ continue;
69
+ seen.add(key);
70
+ parents.push({ name: record.name, range });
71
+ }
72
+ return parents.sort((a, b) => a.name.localeCompare(b.name));
73
+ }
74
+ function runsOn(runtimeVersion, engines) {
75
+ const range = engines?.bare;
76
+ if (typeof range !== 'string')
77
+ return true;
78
+ return semver.validRange(range) !== null && semver.satisfies(runtimeVersion, range);
79
+ }
80
+ /**
81
+ * Newest release that runs on the runtime and that every parent accepts,
82
+ * else one that at least one parent accepts. Without known parents, only the
83
+ * installed release line (`^installed`) is considered. A version no
84
+ * dependent accepts would only trade one failure for another, so none is
85
+ * suggested then.
86
+ */
87
+ export function pickOverrideVersion(metadata, runtimeVersion, installedVersions, parentRanges) {
88
+ const compatible = Object.entries(metadata.versions)
89
+ .filter(([version, manifest]) => {
90
+ if (semver.valid(version) === null || semver.prerelease(version) !== null)
91
+ return false;
92
+ return runsOn(runtimeVersion, manifest.engines);
93
+ })
94
+ .map(([version]) => version)
95
+ .sort(semver.rcompare);
96
+ if (parentRanges.length > 0) {
97
+ const acceptedByAll = compatible.find((version) => parentRanges.every((range) => semver.satisfies(version, range)));
98
+ if (acceptedByAll !== undefined)
99
+ return acceptedByAll;
100
+ return (compatible.find((version) => parentRanges.some((range) => semver.satisfies(version, range))) ?? null);
101
+ }
102
+ const lines = installedVersions.filter((v) => semver.valid(v) !== null).map(releaseLine);
103
+ return (compatible.find((version) => lines.some((range) => semver.satisfies(version, range))) ?? null);
104
+ }
105
+ /** Versions `^version` would treat as compatible, including older ones: 1.4.0 -> 1.x, 0.20.3 -> 0.20.x. */
106
+ function releaseLine(version) {
107
+ const major = semver.major(version);
108
+ const minor = semver.minor(version);
109
+ if (major > 0)
110
+ return `${major}.x`;
111
+ if (minor > 0)
112
+ return `0.${minor}.x`;
113
+ return `0.0.${semver.patch(version)}`;
114
+ }
115
+ function requiredBareVersion(ranges) {
116
+ let required = null;
117
+ for (const range of ranges) {
118
+ const min = semver.validRange(range) === null ? null : semver.minVersion(range);
119
+ if (min !== null && (required === null || semver.gt(min.version, required))) {
120
+ required = min.version;
121
+ }
122
+ }
123
+ return required ?? 'unknown';
124
+ }
125
+ function newestTableEntry() {
126
+ const versions = Object.keys(REACT_NATIVE_BARE_KIT_RUNTIMES).sort(semver.rcompare);
127
+ const newest = versions[0];
128
+ return newest === undefined
129
+ ? null
130
+ : { version: newest, ...REACT_NATIVE_BARE_KIT_RUNTIMES[newest] };
131
+ }
132
+ function buildUpgrade(runtime, ranges, requiredBare) {
133
+ if (runtime.source === 'react-native-bare-kit' && runtime.packageVersion !== undefined) {
134
+ const target = findReactNativeBareKitUpgrade(runtime.packageVersion, ranges);
135
+ if (target !== null) {
136
+ return {
137
+ upgrade: {
138
+ packageName: REACT_NATIVE_BARE_KIT,
139
+ from: runtime.packageVersion,
140
+ to: target.version,
141
+ bare: target.bare
142
+ },
143
+ upgradeHint: `Upgrade ${REACT_NATIVE_BARE_KIT} to ${target.version} or newer ` +
144
+ `(embeds bare-kit ${target.bareKit}, Bare ${target.bare}).`
145
+ };
146
+ }
147
+ const newest = newestTableEntry();
148
+ return {
149
+ upgrade: null,
150
+ upgradeHint: `No ${REACT_NATIVE_BARE_KIT} release in the built-in table embeds Bare ${requiredBare} or newer` +
151
+ (newest === null
152
+ ? '.'
153
+ : `; the newest known, ${newest.version}, embeds Bare ${newest.bare}. Check for a newer release.`)
154
+ };
155
+ }
156
+ if (runtime.source === 'bare-runtime' || runtime.source === 'bare') {
157
+ return {
158
+ upgrade: null,
159
+ upgradeHint: `Upgrade ${runtime.source} to ${requiredBare} or newer.`
160
+ };
161
+ }
162
+ return {
163
+ upgrade: null,
164
+ upgradeHint: `The runtime version comes from ${runtime.source}; raise it to ${requiredBare} or newer if the target runtime allows it.`
165
+ };
166
+ }
167
+ export async function buildEnginesAdvice(options) {
168
+ const { projectRoot, hosts, runtime, failures, packages, network = true, onProgress, fetchPackageMetadata: fetchOne = fetchPackageMetadata } = options;
169
+ const grouped = groupFailures(failures);
170
+ const allRanges = [...grouped.values()].flatMap((entry) => [...entry.ranges]);
171
+ const requiredBare = requiredBareVersion(allRanges);
172
+ const { manager: packageManager } = await detectPackageManager(projectRoot);
173
+ const names = [...grouped.keys()].sort();
174
+ if (network && names.length > 0) {
175
+ onProgress?.(`Looking up releases of ${names.join(', ')} on registry.npmjs.org that run on Bare ` +
176
+ `${runtime.version} (${names.length} request${names.length === 1 ? '' : 's'}, timeout ${NETWORK_TIMEOUT_MS / 1000}s)...`);
177
+ }
178
+ const overrides = await Promise.all(names.map(async (name) => {
179
+ const entry = grouped.get(name);
180
+ const installedVersions = [...entry.versions].sort();
181
+ const parents = findParents(name, packages);
182
+ const override = {
183
+ name,
184
+ installedVersions,
185
+ enginesBare: [...entry.ranges].sort(),
186
+ version: null,
187
+ parents: parents.map((parent) => ({ ...parent, satisfied: false })),
188
+ snippet: null
189
+ };
190
+ if (!network) {
191
+ override.lookupSkipped = true;
192
+ return override;
193
+ }
194
+ try {
195
+ const metadata = await fetchOne(name);
196
+ override.version = pickOverrideVersion(metadata, runtime.version, installedVersions, parents.map((parent) => parent.range));
197
+ }
198
+ catch (error) {
199
+ override.lookupError = error instanceof Error ? error.message : String(error);
200
+ return override;
201
+ }
202
+ const version = override.version;
203
+ if (version === null)
204
+ return override;
205
+ override.parents = parents.map((parent) => ({
206
+ ...parent,
207
+ satisfied: semver.satisfies(version, parent.range)
208
+ }));
209
+ // Scoping only matters when some dependent must keep its current copy.
210
+ const accepting = override.parents.filter((parent) => parent.satisfied);
211
+ override.snippet = formatOverrideSnippet(packageManager, name, version, accepting.length === override.parents.length ? [] : accepting.map((parent) => parent.name));
212
+ return override;
213
+ }));
214
+ return {
215
+ hosts,
216
+ runtime,
217
+ requiredBare,
218
+ ...buildUpgrade(runtime, allRanges, requiredBare),
219
+ overrides,
220
+ packageManager
221
+ };
222
+ }
223
+ const MANIFEST_FIELD = {
224
+ npm: '"overrides"',
225
+ pnpm: '"pnpm.overrides"',
226
+ yarn: '"resolutions"',
227
+ bun: '"overrides"'
228
+ };
229
+ export function formatEnginesAdvice(advice) {
230
+ const lines = [
231
+ ` Fix for ${advice.hosts.join(', ')}:`,
232
+ ` Runtime: Bare ${advice.runtime.version} (from ${formatRuntimeSource(advice.runtime)})`,
233
+ ...advice.overrides.map((override) => ` Requires: ${override.name}@${override.installedVersions.join(', ') || 'unknown'} needs Bare ${override.enginesBare.join(' and ')}`),
234
+ ` Option 1: ${advice.upgradeHint}`
235
+ ];
236
+ for (const override of advice.overrides) {
237
+ const installed = override.installedVersions.join(', ') || 'unknown';
238
+ if (override.lookupSkipped === true) {
239
+ lines.push(` Option 2 (${override.name}): pin an older release; run again with network lookups ` +
240
+ 'enabled (without --offline) to find one that runs on this Bare.');
241
+ continue;
242
+ }
243
+ if (override.version === null) {
244
+ const acceptedBy = override.parents.length === 0
245
+ ? ` on the ${installed} release line`
246
+ : ` that ${override.parents.map((p) => `${p.name} (${p.range})`).join(', ')} accept${override.parents.length === 1 ? 's' : ''}`;
247
+ lines.push(` Option 2 (${override.name}): no release that runs on Bare ${advice.runtime.version}${acceptedBy} was found` +
248
+ (override.lookupError === undefined
249
+ ? '; upgrade the runtime.'
250
+ : ` (${override.lookupError}).`));
251
+ continue;
252
+ }
253
+ const field = advice.packageManager === null
254
+ ? '"overrides" (npm/bun), "pnpm.overrides" or "resolutions" (yarn)'
255
+ : MANIFEST_FIELD[advice.packageManager];
256
+ lines.push(` Option 2 (${override.name}): pin ${installed} -> ${override.version} with ${field} in your app's package.json:`);
257
+ for (const line of (override.snippet ?? '').split('\n'))
258
+ lines.push(` ${line}`);
259
+ const rejected = override.parents.filter((parent) => !parent.satisfied);
260
+ if (rejected.length > 0) {
261
+ lines.push(` ${rejected.map((p) => `${p.name} (${p.range})`).join(', ')} ` +
262
+ `${rejected.length === 1 ? 'does' : 'do'} not accept ${override.version}; ` +
263
+ 'those copies are left as they are.');
264
+ }
265
+ if (advice.packageManager === 'bun' && override.parents.length > 0) {
266
+ lines.push(' bun applies overrides to every copy in the tree.');
267
+ }
268
+ }
269
+ lines.push('');
270
+ return lines;
271
+ }
@@ -1,12 +1,29 @@
1
1
  import { type AddonSourceKind, type NativeAddon } from './addon-source.js';
2
2
  import { type MissingPrebuildIssue } from './prebuilds.js';
3
3
  import { type AbiIssue, type BareRuntimeResolution } from './abi.js';
4
+ import { type FetchText, type ProgressFn } from './bare-kit-runtime.js';
5
+ import { type EnginesAdvice, type FetchPackageMetadata } from './engines-advice.js';
4
6
  export interface VerifyBundleOptions {
5
7
  projectRoot: string;
6
8
  addonsSource: string;
7
9
  hosts: string[];
8
10
  bareRuntimeVersion?: string;
9
11
  configPath?: string;
12
+ /**
13
+ * Allow GitHub and npm registry requests: GitHub for a react-native-bare-kit
14
+ * release newer than the built-in table, the registry only after a mismatch
15
+ * is found, to suggest an override. Defaults to true.
16
+ */
17
+ network?: boolean;
18
+ /** Called before any step that waits on the network. */
19
+ onProgress?: ProgressFn;
20
+ /** Test seams for the network lookups. */
21
+ fetchText?: FetchText;
22
+ fetchPackageMetadata?: FetchPackageMetadata;
23
+ }
24
+ export interface RuntimeGroup {
25
+ hosts: string[];
26
+ resolution: BareRuntimeResolution;
10
27
  }
11
28
  export interface InvalidSourceIssue {
12
29
  code: 'invalid-source';
@@ -48,9 +65,14 @@ export interface VerifyBundleResult {
48
65
  resolvedAddonsSource: string;
49
66
  sourceKind: AddonSourceKind | null;
50
67
  hosts: string[];
68
+ /** Runtime of the first entry in `runtimes`: the mobile one when any host is mobile. */
51
69
  runtime: BareRuntimeResolution | null;
70
+ /** Mobile and desktop hosts run different Bare builds, so each is resolved separately. */
71
+ runtimes?: RuntimeGroup[];
52
72
  addons: NativeAddon[];
53
73
  issues: VerifyBundleIssue[];
74
+ /** How to fix each runtime that fails an engines.bare range. */
75
+ advice?: EnginesAdvice[];
54
76
  }
55
77
  export declare function verifyBundle(options: VerifyBundleOptions): Promise<VerifyBundleResult>;
56
78
  export declare function hasErrors(result: VerifyBundleResult): boolean;
@@ -4,8 +4,10 @@ import { resolveConfigFileInProject, loadConfigFromPath } from '../../client/con
4
4
  import { createCollectDiagnostics, formatAddonId } from './addon-source.js';
5
5
  import { collectAddonsFromBundle, InvalidBundleSourceError } from './bundle-source.js';
6
6
  import { collectAddonsFromNodeModules, InvalidNodeModulesSourceError } from './node-modules-source.js';
7
- import { checkPrebuilds } from './prebuilds.js';
7
+ import { checkPrebuilds, isMobileHost } from './prebuilds.js';
8
8
  import { checkAbi, formatConfigLabel, normalizeVersion, resolveBareRuntime } from './abi.js';
9
+ import { isReactNativeBareKitInstalled, resolveMobileBareRuntime } from './bare-kit-runtime.js';
10
+ import { buildEnginesAdvice, formatEnginesAdvice } from './engines-advice.js';
9
11
  export async function verifyBundle(options) {
10
12
  const { projectRoot, addonsSource, hosts, bareRuntimeVersion, configPath } = options;
11
13
  const resolvedAddonsSource = path.isAbsolute(addonsSource)
@@ -119,6 +121,9 @@ export async function verifyBundle(options) {
119
121
  };
120
122
  }
121
123
  const diagnostics = createCollectDiagnostics();
124
+ if (sourceKind === 'node-modules') {
125
+ options.onProgress?.(`Scanning ${resolvedAddonsSource} for native addons and engines.bare...`);
126
+ }
122
127
  let addons;
123
128
  try {
124
129
  addons =
@@ -178,20 +183,57 @@ export async function verifyBundle(options) {
178
183
  issues.push(...prebuildIssues);
179
184
  }
180
185
  let runtime = null;
186
+ let runtimes;
187
+ const advice = [];
181
188
  if (invalidRuntimeVersion === null) {
182
- const runtimeOptions = { projectRoot };
183
- if (bareRuntimeVersion !== undefined) {
184
- runtimeOptions.explicitVersion = bareRuntimeVersion;
185
- runtimeOptions.explicitSource = 'flag';
186
- }
187
- else if (configRuntimeVersion !== undefined) {
188
- runtimeOptions.explicitVersion = configRuntimeVersion;
189
- runtimeOptions.explicitSource = 'config';
189
+ const explicit = bareRuntimeVersion !== undefined
190
+ ? { version: bareRuntimeVersion, source: 'flag' }
191
+ : configRuntimeVersion !== undefined
192
+ ? { version: configRuntimeVersion, source: 'config' }
193
+ : null;
194
+ runtimes = await resolveRuntimeGroups({
195
+ projectRoot,
196
+ hosts,
197
+ explicit,
198
+ network: options.network,
199
+ onProgress: options.onProgress,
200
+ fetchText: options.fetchText
201
+ });
202
+ runtime = runtimes[0]?.resolution ?? null;
203
+ const packages = enginesPackages(diagnostics.packages);
204
+ const seen = new Set();
205
+ for (const group of runtimes) {
206
+ const groupIssues = checkAbi({
207
+ addons: addonsLinkedOn(addons, group.hosts),
208
+ runtime: group.resolution,
209
+ packages
210
+ }).filter((issue) => {
211
+ if (seen.has(issue.message))
212
+ return false;
213
+ seen.add(issue.message);
214
+ return true;
215
+ });
216
+ issues.push(...groupIssues);
217
+ const failures = groupIssues.flatMap((issue) => issue.code === 'abi-mismatch' || issue.code === 'engines-mismatch'
218
+ ? [
219
+ failureOf(issue.code === 'abi-mismatch' ? issue.addon : issue.package, issue.enginesBare)
220
+ ]
221
+ : []);
222
+ if (failures.length > 0 && group.resolution.resolved) {
223
+ advice.push(await buildEnginesAdvice({
224
+ projectRoot,
225
+ hosts: group.hosts,
226
+ runtime: group.resolution.runtime,
227
+ failures,
228
+ packages: diagnostics.packages,
229
+ network: options.network,
230
+ onProgress: options.onProgress,
231
+ fetchPackageMetadata: options.fetchPackageMetadata
232
+ }));
233
+ }
190
234
  }
191
- runtime = await resolveBareRuntime(runtimeOptions);
192
- issues.push(...checkAbi({ addons, runtime }));
193
235
  }
194
- return {
236
+ const result = {
195
237
  addonsSource,
196
238
  resolvedAddonsSource,
197
239
  sourceKind,
@@ -200,6 +242,85 @@ export async function verifyBundle(options) {
200
242
  addons,
201
243
  issues
202
244
  };
245
+ if (runtimes !== undefined)
246
+ result.runtimes = runtimes;
247
+ if (advice.length > 0)
248
+ result.advice = advice;
249
+ return result;
250
+ }
251
+ /** `name@version` ids as built by `formatAddonId`; scoped names keep their leading `@`. */
252
+ function failureOf(id, enginesBare) {
253
+ const at = id.lastIndexOf('@');
254
+ if (at <= 0)
255
+ return { name: id, enginesBare };
256
+ const version = id.slice(at + 1);
257
+ return version === 'unknown'
258
+ ? { name: id.slice(0, at), enginesBare }
259
+ : { name: id.slice(0, at), version, enginesBare };
260
+ }
261
+ /**
262
+ * Addons a runtime group loads: those the bundle links on one of its hosts.
263
+ * Without link information (`linkedHosts` unset), every addon counts, as in
264
+ * `checkPrebuilds`.
265
+ */
266
+ function addonsLinkedOn(addons, hosts) {
267
+ return addons.filter((addon) => addon.linkedHosts === undefined || addon.linkedHosts.some((host) => hosts.includes(host)));
268
+ }
269
+ function enginesPackages(records) {
270
+ const byId = new Map();
271
+ for (const record of records) {
272
+ if (record.isAddon || record.enginesBare === undefined)
273
+ continue;
274
+ const id = `${record.name}@${record.version ?? 'unknown'}`;
275
+ if (byId.has(id))
276
+ continue;
277
+ const pkg = { name: record.name, enginesBare: record.enginesBare };
278
+ if (record.version !== undefined)
279
+ pkg.version = record.version;
280
+ byId.set(id, pkg);
281
+ }
282
+ return [...byId.values()];
283
+ }
284
+ /**
285
+ * Mobile hosts run the Bare build inside react-native-bare-kit; desktop hosts
286
+ * run `bare-runtime` (or `bare`). An explicit version applies to every host.
287
+ * Without react-native-bare-kit installed, mobile hosts fall back to the
288
+ * desktop detection so projects that only bundle for mobile keep a runtime.
289
+ */
290
+ async function resolveRuntimeGroups(options) {
291
+ const { projectRoot, hosts, explicit } = options;
292
+ if (explicit !== null) {
293
+ return [
294
+ {
295
+ hosts,
296
+ resolution: await resolveBareRuntime({
297
+ projectRoot,
298
+ explicitVersion: explicit.version,
299
+ explicitSource: explicit.source
300
+ })
301
+ }
302
+ ];
303
+ }
304
+ const mobileHosts = hosts.filter(isMobileHost);
305
+ const desktopHosts = hosts.filter((host) => !isMobileHost(host));
306
+ const groups = [];
307
+ if (mobileHosts.length > 0) {
308
+ groups.push({
309
+ hosts: mobileHosts,
310
+ resolution: isReactNativeBareKitInstalled(projectRoot)
311
+ ? await resolveMobileBareRuntime({
312
+ projectRoot,
313
+ network: options.network,
314
+ onProgress: options.onProgress,
315
+ fetchText: options.fetchText
316
+ })
317
+ : await resolveBareRuntime({ projectRoot })
318
+ });
319
+ }
320
+ if (desktopHosts.length > 0) {
321
+ groups.push({ hosts: desktopHosts, resolution: await resolveBareRuntime({ projectRoot }) });
322
+ }
323
+ return groups;
203
324
  }
204
325
  function buildInvalidPackageJsonIssues(records) {
205
326
  return records.map((record) => {
@@ -262,6 +383,9 @@ export function formatVerifyBundleResult(result) {
262
383
  sections.push('');
263
384
  sections.push(...formatMissingPrebuilds(result.issues));
264
385
  sections.push(...formatAbiMismatches(result.issues));
386
+ sections.push(...formatEnginesMismatches(result.issues));
387
+ for (const advice of result.advice ?? [])
388
+ sections.push(...formatEnginesAdvice(advice));
265
389
  sections.push(...formatInvalidRuntimeVersions(result.issues));
266
390
  sections.push(...formatMalformedEnginesBare(result.issues));
267
391
  sections.push(...formatConfigLoadFailed(result.issues));
@@ -288,7 +412,9 @@ function formatMissingPrebuilds(issues) {
288
412
  return [];
289
413
  const lines = [' Missing prebuild:'];
290
414
  for (const issue of matches) {
291
- lines.push(` - ${issue.addon} for ${issue.host}`);
415
+ const pin = issue.platformPackage;
416
+ const needs = pin === undefined ? '' : ` (needs "${pin.name}": "${pin.version}" in package.json)`;
417
+ lines.push(` - ${issue.addon} for ${issue.host}${needs}`);
292
418
  }
293
419
  lines.push('');
294
420
  return lines;
@@ -305,6 +431,18 @@ function formatAbiMismatches(issues) {
305
431
  lines.push('');
306
432
  return lines;
307
433
  }
434
+ function formatEnginesMismatches(issues) {
435
+ const matches = issues.filter((issue) => issue.code === 'engines-mismatch');
436
+ if (matches.length === 0)
437
+ return [];
438
+ const lines = [' engines.bare mismatch:'];
439
+ for (const issue of matches) {
440
+ lines.push(` - ${issue.package} requires bare ${issue.enginesBare}, ` +
441
+ `runtime is ${issue.runtimeVersion}`);
442
+ }
443
+ lines.push('');
444
+ return lines;
445
+ }
308
446
  function formatUnknownRuntime(issues) {
309
447
  const matches = issues.filter((issue) => issue.code === 'unknown-runtime-version');
310
448
  if (matches.length === 0)
@@ -1,6 +1,6 @@
1
1
  import { promises as fsp } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { deduplicateAddons, readAddonPackageJson } from './addon-source.js';
3
+ import { createLimiter, deduplicateAddons, FS_CONCURRENCY, readAddonPackageJson } from './addon-source.js';
4
4
  export class InvalidNodeModulesSourceError extends Error {
5
5
  nodeModulesPath;
6
6
  constructor(nodeModulesPath, cause) {
@@ -14,63 +14,67 @@ export class InvalidNodeModulesSourceError extends Error {
14
14
  }
15
15
  export async function collectAddonsFromNodeModules(options) {
16
16
  const { nodeModulesRoot, diagnostics } = options;
17
+ const limit = createLimiter(FS_CONCURRENCY);
18
+ const results = await walkNodeModules(nodeModulesRoot, limit, true);
17
19
  const addons = [];
18
- await walkNodeModules(nodeModulesRoot, addons, diagnostics, true);
20
+ for (const result of results) {
21
+ if (result.record !== undefined && diagnostics !== undefined) {
22
+ diagnostics.packages.push(result.record);
23
+ }
24
+ if (result.isAddon && result.addon) {
25
+ addons.push(result.addon);
26
+ }
27
+ else if (result.invalid !== undefined && diagnostics !== undefined) {
28
+ diagnostics.invalidPackageJsons.push(result.invalid);
29
+ }
30
+ }
19
31
  return deduplicateAddons(addons);
20
32
  }
21
- async function walkNodeModules(nodeModulesDir, addons, diagnostics, isRoot) {
33
+ /**
34
+ * Directory reads run in parallel under `limit`; results are concatenated in
35
+ * directory-entry order so the output does not depend on I/O timing.
36
+ */
37
+ async function walkNodeModules(nodeModulesDir, limit, isRoot) {
22
38
  let entries;
23
39
  try {
24
- entries = await fsp.readdir(nodeModulesDir, { withFileTypes: true });
40
+ entries = await limit(() => fsp.readdir(nodeModulesDir, { withFileTypes: true }));
25
41
  }
26
42
  catch (error) {
27
43
  if (isRoot)
28
44
  throw new InvalidNodeModulesSourceError(nodeModulesDir, error);
29
- return;
30
- }
31
- for (const entry of entries) {
32
- if (entry.name.startsWith('.'))
33
- continue;
34
- if (!isPackageEntry(entry))
35
- continue;
36
- if (entry.name.startsWith('@')) {
37
- await walkScopeDirectory(path.join(nodeModulesDir, entry.name), addons, diagnostics);
38
- continue;
39
- }
40
- await visitPackageDirectory(path.join(nodeModulesDir, entry.name), entry.name, addons, diagnostics);
45
+ return [];
41
46
  }
47
+ const visits = entries
48
+ .filter((entry) => !entry.name.startsWith('.') && isPackageEntry(entry))
49
+ .map((entry) => entry.name.startsWith('@')
50
+ ? walkScopeDirectory(path.join(nodeModulesDir, entry.name), limit)
51
+ : visitPackageDirectory(path.join(nodeModulesDir, entry.name), entry.name, limit));
52
+ return (await Promise.all(visits)).flat();
42
53
  }
43
- async function walkScopeDirectory(scopeDir, addons, diagnostics) {
54
+ async function walkScopeDirectory(scopeDir, limit) {
44
55
  const scope = path.basename(scopeDir);
45
56
  let entries;
46
57
  try {
47
- entries = await fsp.readdir(scopeDir, { withFileTypes: true });
58
+ entries = await limit(() => fsp.readdir(scopeDir, { withFileTypes: true }));
48
59
  }
49
60
  catch {
50
- return;
51
- }
52
- for (const entry of entries) {
53
- if (entry.name.startsWith('.'))
54
- continue;
55
- if (!isPackageEntry(entry))
56
- continue;
57
- await visitPackageDirectory(path.join(scopeDir, entry.name), `${scope}/${entry.name}`, addons, diagnostics);
61
+ return [];
58
62
  }
63
+ const visits = entries
64
+ .filter((entry) => !entry.name.startsWith('.') && isPackageEntry(entry))
65
+ .map((entry) => visitPackageDirectory(path.join(scopeDir, entry.name), `${scope}/${entry.name}`, limit));
66
+ return (await Promise.all(visits)).flat();
59
67
  }
60
68
  function isPackageEntry(entry) {
61
69
  return entry.isDirectory() || entry.isSymbolicLink();
62
70
  }
63
- async function visitPackageDirectory(packageDir, packageName, addons, diagnostics) {
64
- const result = await readAddonPackageJson({
65
- packageJsonPath: path.join(packageDir, 'package.json'),
66
- expectedName: packageName
67
- });
68
- if (result.isAddon && result.addon) {
69
- addons.push(result.addon);
70
- }
71
- else if (result.invalid !== undefined && diagnostics !== undefined) {
72
- diagnostics.invalidPackageJsons.push(result.invalid);
73
- }
74
- const nestedNodeModules = path.join(packageDir, 'node_modules');
75
- await walkNodeModules(nestedNodeModules, addons, diagnostics, false);
71
+ async function visitPackageDirectory(packageDir, packageName, limit) {
72
+ const [result, nested] = await Promise.all([
73
+ limit(() => readAddonPackageJson({
74
+ packageJsonPath: path.join(packageDir, 'package.json'),
75
+ expectedName: packageName
76
+ })),
77
+ walkNodeModules(path.join(packageDir, 'node_modules'), limit, false)
78
+ ]);
79
+ return [result, ...nested];
76
80
  }
@@ -6,7 +6,20 @@ export interface MissingPrebuildIssue {
6
6
  host: string;
7
7
  message: string;
8
8
  packageRoot: string;
9
+ /**
10
+ * The per-platform package to declare for this mobile host, at the exact
11
+ * version it must be installed at. Set when the addon's `#host-addon` map
12
+ * names one and it is either not installed or installed at another version.
13
+ * Never set for desktop hosts, whose platform packages install as the
14
+ * addon's os/cpu-filtered optionalDependencies.
15
+ */
16
+ platformPackage?: PlatformPackagePin;
9
17
  }
18
+ export interface PlatformPackagePin {
19
+ name: string;
20
+ version: string;
21
+ }
22
+ export declare function isMobileHost(host: string): boolean;
10
23
  export interface CheckPrebuildsOptions {
11
24
  addon: NativeAddon;
12
25
  hosts: string[];
@@ -19,6 +32,12 @@ export interface CheckPrebuildsOptions {
19
32
  export interface PrebuildLocation {
20
33
  hostDir: string;
21
34
  platformPackage?: string;
35
+ /**
36
+ * The installed platform package's version when it differs from the
37
+ * addon's. Platform packages are version-locked to their addon, so a
38
+ * prebuild in a mismatched one does not count.
39
+ */
40
+ mismatchedVersion?: string;
22
41
  }
23
42
  /**
24
43
  * Directories to search for an addon's `<host>` prebuild, in precedence order: