@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
@@ -1,8 +1,12 @@
1
1
  import { parseBuiltinSpecifier } from './plugins.js';
2
- export function generateWorkerEntry(pluginSpecifiers, sdkName, resolveImport = (specifier) => specifier) {
2
+ export function generateWorkerEntry(pluginSpecifiers, sdkName, resolveImport = (specifier) => specifier, rpcServerProvider) {
3
3
  const imports = [];
4
4
  const registrations = [];
5
5
  let varIndex = 0;
6
+ if (rpcServerProvider) {
7
+ imports.push(`import { registerRpcServerProvider } from ${JSON.stringify(resolveImport(`${sdkName}/rpc-server-provider`))};`, `import rpcServerProvider from ${JSON.stringify(resolveImport(rpcServerProvider))};`);
8
+ registrations.push('registerRpcServerProvider(rpcServerProvider);');
9
+ }
6
10
  for (const specifier of pluginSpecifiers) {
7
11
  const builtin = parseBuiltinSpecifier(specifier, sdkName);
8
12
  if (builtin) {
@@ -46,9 +50,9 @@ if (hasRPCConfig) {
46
50
  }
47
51
  `;
48
52
  }
49
- export function generateWorkerEntries(pluginSpecifiers, sdkName, resolveBundleImport) {
53
+ export function generateWorkerEntries(pluginSpecifiers, sdkName, resolveBundleImport, rpcServerProvider) {
50
54
  return {
51
- runtimeEntry: generateWorkerEntry(pluginSpecifiers, sdkName),
52
- bundleEntry: generateWorkerEntry(pluginSpecifiers, sdkName, resolveBundleImport)
55
+ runtimeEntry: generateWorkerEntry(pluginSpecifiers, sdkName, undefined, rpcServerProvider),
56
+ bundleEntry: generateWorkerEntry(pluginSpecifiers, sdkName, resolveBundleImport, rpcServerProvider)
53
57
  };
54
58
  }
@@ -1,3 +1,4 @@
1
+ import { type HostPrebuildPackage } from '../host-prebuilds/index.js';
1
2
  export interface BundleSdkOptions {
2
3
  projectRoot?: string | undefined;
3
4
  configPath?: string | undefined;
@@ -6,6 +7,23 @@ export interface BundleSdkOptions {
6
7
  defer?: string[] | undefined;
7
8
  quiet?: boolean | undefined;
8
9
  verbose?: boolean | undefined;
10
+ /**
11
+ * Install the addon platform packages the bundle needs for its mobile hosts
12
+ * with the project's package manager (see `ensureHostPrebuilds`), then
13
+ * bundle again. Off by default: without it, bundling never changes
14
+ * package.json or node_modules. When the install is refused, the bundle and
15
+ * its manifest are still written before `HostPrebuildsInstallRefusedError`
16
+ * is thrown.
17
+ */
18
+ installMissingPrebuilds?: boolean | undefined;
19
+ /**
20
+ * Check the bundled packages' engines.bare against the Bare runtime of each
21
+ * host and warn on a mismatch. Defaults to true; callers that run
22
+ * `verifyBundle` on the result can turn it off.
23
+ */
24
+ checkEngines?: boolean | undefined;
25
+ /** Allow network lookups during the engines check. Defaults to true. */
26
+ network?: boolean | undefined;
9
27
  }
10
28
  export interface BundleSdkResult {
11
29
  bundlePath: string;
@@ -15,6 +33,8 @@ export interface BundleSdkResult {
15
33
  worker: string;
16
34
  };
17
35
  manifestPath: string;
36
+ /** Platform packages `installMissingPrebuilds` installed; empty when it is off. */
37
+ installedPrebuilds: HostPrebuildPackage[];
18
38
  }
19
39
  export declare function bundleSdk(options?: BundleSdkOptions): Promise<BundleSdkResult>;
20
40
  //# sourceMappingURL=index.d.ts.map
@@ -3,13 +3,18 @@ import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
  import { DEFAULT_HOSTS, DEFAULT_SDK_NAME } from './constants.js';
5
5
  import { CONFIG_CANDIDATES, resolveConfigForProject } from '../../client/config-loader/resolve-config.node.js';
6
- import { getClientLogger } from '../../logging/index.js';
7
- import { BareImportsMapNotFoundError } from '../../utils/errors-client.js';
6
+ import { createCommandLogger } from '../command-logger.js';
7
+ import { BareImportsMapNotFoundError, HostPrebuildsInstallRefusedError } from '../../utils/errors-client.js';
8
8
  import { resolvePluginSpecifiers, parseBuiltinSpecifier } from './plugins.js';
9
9
  import { generateWorkerEntries } from './entry-gen.js';
10
10
  import { runBarePack } from './bare-pack.js';
11
11
  import { generateAddonsManifest } from './manifest.js';
12
12
  import { createSdkImportResolver } from './resolve-sdk-import.js';
13
+ import { installMissingHostPrebuilds } from '../host-prebuilds/index.js';
14
+ import { collectAddonsFromBundle } from '../verify/bundle-source.js';
15
+ import { verifyBundle } from '../verify/index.js';
16
+ import { formatRuntimeSource } from '../verify/abi.js';
17
+ import { formatEnginesAdvice } from '../verify/engines-advice.js';
13
18
  const require = createRequire(import.meta.url);
14
19
  function resolveSdkPath(projectRoot, explicitSdkPath) {
15
20
  if (explicitSdkPath) {
@@ -49,14 +54,43 @@ function resolveImportsMapPath(sdkPath, sdkName) {
49
54
  }
50
55
  throw new BareImportsMapNotFoundError(sdkName, importsMapPath);
51
56
  }
52
- function createCommandLogger(options) {
53
- if (options.quiet) {
54
- return getClientLogger({ level: 'error', enableConsole: false });
57
+ const ENGINES_ISSUE_CODES = new Set(['abi-mismatch', 'engines-mismatch']);
58
+ async function checkBundleEngines(options) {
59
+ const { projectRoot, bundlePath, hosts, configPath, network, logger } = options;
60
+ logger.info('\n🔎 Checking engines.bare against the Bare runtime of each host...');
61
+ const result = await verifyBundle({
62
+ projectRoot,
63
+ addonsSource: bundlePath,
64
+ hosts,
65
+ ...(configPath !== undefined ? { configPath } : {}),
66
+ ...(network !== undefined ? { network } : {}),
67
+ onProgress: (message) => logger.info(` ${message}`)
68
+ });
69
+ const checkedHosts = [];
70
+ for (const group of result.runtimes ?? []) {
71
+ const label = group.hosts.join(', ');
72
+ if (group.resolution.resolved) {
73
+ checkedHosts.push(...group.hosts);
74
+ logger.info(` ${label}: Bare ${group.resolution.runtime.version} (from ${formatRuntimeSource(group.resolution.runtime)})`);
75
+ }
76
+ else {
77
+ logger.warn(`engines.bare not checked for ${label}: ${group.resolution.error.reason}`);
78
+ }
55
79
  }
56
- if (options.verbose) {
57
- return getClientLogger({ level: 'debug' });
80
+ const mismatches = result.issues.filter((issue) => ENGINES_ISSUE_CODES.has(issue.code) && issue.level === 'error');
81
+ if (mismatches.length === 0) {
82
+ if (checkedHosts.length > 0) {
83
+ logger.info(` No engines.bare mismatches for ${checkedHosts.join(', ')}`);
84
+ }
85
+ return;
58
86
  }
59
- return getClientLogger();
87
+ const lines = ['The bundle contains packages that require a newer Bare runtime:'];
88
+ for (const issue of mismatches)
89
+ lines.push(` - ${issue.message}`);
90
+ lines.push('');
91
+ for (const advice of result.advice ?? [])
92
+ lines.push(...formatEnginesAdvice(advice));
93
+ logger.warn(lines.join('\n').trimEnd());
60
94
  }
61
95
  export async function bundleSdk(options = {}) {
62
96
  const startTime = Date.now();
@@ -94,7 +128,7 @@ export async function bundleSdk(options = {}) {
94
128
  await fsp.mkdir(outputDir, { recursive: true });
95
129
  logger.info('\n📝 Generating worker entry...');
96
130
  const resolveSdkImport = createSdkImportResolver(sdkPath, sdkName);
97
- const { runtimeEntry, bundleEntry } = generateWorkerEntries(pluginSpecifiers, sdkName, resolveSdkImport);
131
+ const { runtimeEntry, bundleEntry } = generateWorkerEntries(pluginSpecifiers, sdkName, resolveSdkImport, config.rpcServerProvider);
98
132
  await fsp.writeFile(entryPath, runtimeEntry, 'utf8');
99
133
  logger.info(` Created: ${path.relative(projectRoot, entryPath)}`);
100
134
  logger.info(` Using: ${path.relative(projectRoot, importsMapPath)}`);
@@ -103,9 +137,11 @@ export async function bundleSdk(options = {}) {
103
137
  if (deferModules.length > 0) {
104
138
  logger.debug(` Deferred: ${deferModules.join(', ')}`);
105
139
  }
140
+ let installedPrebuilds = [];
141
+ let installRefused;
106
142
  try {
107
143
  await fsp.writeFile(bundleEntryPath, bundleEntry, 'utf8');
108
- await runBarePack({
144
+ const barePackOptions = {
109
145
  entryPath: bundleEntryPath,
110
146
  outputPath: bundlePath,
111
147
  hosts,
@@ -113,7 +149,34 @@ export async function bundleSdk(options = {}) {
113
149
  deferModules,
114
150
  quiet: options.quiet === true,
115
151
  logger
116
- });
152
+ };
153
+ await runBarePack(barePackOptions);
154
+ if (options.installMissingPrebuilds === true) {
155
+ try {
156
+ const { installed } = await installMissingHostPrebuilds({
157
+ projectRoot,
158
+ hosts,
159
+ addons: await collectAddonsFromBundle({ bundlePath, projectRoot, hosts }),
160
+ quiet: options.quiet === true,
161
+ logger
162
+ });
163
+ installedPrebuilds = installed;
164
+ // Where a platform package was missing, bare-pack resolved the addon's
165
+ // `#host-addon` import to its fallback module; bundle again to pick up
166
+ // the installed package.
167
+ if (installed.length > 0) {
168
+ logger.info('\n🔨 Bundling again with the installed platform packages...');
169
+ await runBarePack(barePackOptions);
170
+ }
171
+ }
172
+ catch (error) {
173
+ if (!(error instanceof HostPrebuildsInstallRefusedError))
174
+ throw error;
175
+ // A refusal happens before anything is installed, so the bundle
176
+ // already written is final and only its manifest is left to write.
177
+ installRefused = error;
178
+ }
179
+ }
117
180
  }
118
181
  finally {
119
182
  await fsp.rm(bundleEntryPath, { force: true });
@@ -128,6 +191,18 @@ export async function bundleSdk(options = {}) {
128
191
  projectRoot,
129
192
  logger
130
193
  });
194
+ if (options.checkEngines !== false) {
195
+ await checkBundleEngines({
196
+ projectRoot,
197
+ bundlePath,
198
+ hosts,
199
+ configPath: configPath ?? undefined,
200
+ network: options.network,
201
+ logger
202
+ });
203
+ }
204
+ if (installRefused !== undefined)
205
+ throw installRefused;
131
206
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
132
207
  logger.info(`\n🎉 Done in ${elapsed}s!\n`);
133
208
  logger.info('Generated files:');
@@ -143,6 +218,7 @@ export async function bundleSdk(options = {}) {
143
218
  entryPaths: {
144
219
  worker: entryPath
145
220
  },
146
- manifestPath: manifestResult.manifestPath
221
+ manifestPath: manifestResult.manifestPath,
222
+ installedPrebuilds
147
223
  };
148
224
  }
@@ -17,7 +17,7 @@ export function parseBuiltinSpecifier(specifier, sdkName) {
17
17
  }
18
18
  export function resolvePluginSpecifiers(config, sdkName, logger) {
19
19
  let plugins = config.plugins ?? [];
20
- if (plugins.length === 0) {
20
+ if (plugins.length === 0 && !(config.rpcServerProvider && config.plugins !== undefined)) {
21
21
  const allBuiltins = BUILTIN_SUFFIXES.map((suffix) => buildBuiltinSpecifier(sdkName, suffix));
22
22
  logger.warn('No plugins specified — bundling ALL built-in plugins.');
23
23
  logger.info(" For smaller bundles, add a 'plugins' array to qvac.config.*\n");
@@ -0,0 +1,6 @@
1
+ export interface CommandLoggerOptions {
2
+ quiet?: boolean | undefined;
3
+ verbose?: boolean | undefined;
4
+ }
5
+ export declare function createCommandLogger(options: CommandLoggerOptions): import("../logging/index.js").Logger;
6
+ //# sourceMappingURL=command-logger.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { getClientLogger } from '../logging/index.js';
2
+ export function createCommandLogger(options) {
3
+ if (options.quiet) {
4
+ return getClientLogger({ level: 'error', enableConsole: false });
5
+ }
6
+ // The client logger keeps console output off unless asked; a command's
7
+ // progress and warnings are meant for the person running it.
8
+ if (options.verbose) {
9
+ return getClientLogger({ level: 'debug', enableConsole: true });
10
+ }
11
+ return getClientLogger({ enableConsole: true });
12
+ }
@@ -0,0 +1,71 @@
1
+ import type { Logger } from '../../logging/types.js';
2
+ import { type NativeAddon } from '../verify/addon-source.js';
3
+ import { type PackageManagerName } from './package-manager.js';
4
+ export type { PackageManagerName } from './package-manager.js';
5
+ export interface EnsureHostPrebuildsOptions {
6
+ /** The package whose package.json receives the pins; its package manager installs them. */
7
+ projectRoot: string;
8
+ /** Bare hosts to cover. Only mobile hosts (`android-*`, `ios-*`) can need an install. */
9
+ hosts: string[];
10
+ /**
11
+ * Addon package names to cover. Defaults to every addon installed for the
12
+ * project: in its own node_modules or in that of a workspace root listing it.
13
+ */
14
+ addons?: string[] | undefined;
15
+ /** Install with this package manager instead of detecting the project's own. */
16
+ packageManager?: PackageManagerName | undefined;
17
+ quiet?: boolean | undefined;
18
+ verbose?: boolean | undefined;
19
+ }
20
+ export interface HostPrebuildPackage {
21
+ /** The platform package, e.g. `@qvac/tts-ggml-android-arm64`. */
22
+ name: string;
23
+ /** Always the addon's own version: platform packages are version-locked to it. */
24
+ version: string;
25
+ /** The addon the package ships prebuilds for. */
26
+ addon: string;
27
+ /** The requested hosts the package covers. */
28
+ hosts: string[];
29
+ }
30
+ export interface EnsureHostPrebuildsResult {
31
+ /** The packages this call installed; empty when every prebuild already resolved. */
32
+ installed: HostPrebuildPackage[];
33
+ /** The package manager that installed them, or null when nothing was missing. */
34
+ packageManager: PackageManagerName | null;
35
+ }
36
+ /**
37
+ * Installs the per-platform prebuild packages that addons need for mobile
38
+ * hosts and that the project does not have yet.
39
+ *
40
+ * A split addon names one platform package per host in the `#host-addon` map
41
+ * of its package.json (`<addon>-ios` covers every iOS host). Desktop platform
42
+ * packages install as the addon's os/cpu-filtered optionalDependencies, but a
43
+ * build host never reports a mobile os, so mobile ones have to be declared.
44
+ * This declares the missing ones in package.json, pinned to each addon's
45
+ * exact version, using the project's own package manager.
46
+ *
47
+ * A host whose prebuild already resolves is skipped: a local
48
+ * `prebuilds/<host>` in the addon (source builds) or its platform package at
49
+ * the addon's version. Throws `HostPrebuildsInstallRefusedError` when no
50
+ * supported package manager (npm 7+, pnpm, bun, Yarn Berry) is found, and
51
+ * `HostPrebuildsInstallFailedError` when the install does not produce the
52
+ * packages.
53
+ */
54
+ export declare function ensureHostPrebuilds(options: EnsureHostPrebuildsOptions): Promise<EnsureHostPrebuildsResult>;
55
+ export interface InstallMissingHostPrebuildsOptions {
56
+ projectRoot: string;
57
+ hosts: string[];
58
+ addons: NativeAddon[];
59
+ packageManager?: PackageManagerName | undefined;
60
+ quiet: boolean;
61
+ logger: Logger;
62
+ }
63
+ /** `ensureHostPrebuilds` for addons the caller has already located. */
64
+ export declare function installMissingHostPrebuilds(options: InstallMissingHostPrebuildsOptions): Promise<EnsureHostPrebuildsResult>;
65
+ /**
66
+ * The platform packages `addons` still need for the mobile hosts in `hosts`,
67
+ * one entry per package. Throws when two installed versions of one addon need
68
+ * the same platform package, since only one version of it can be installed.
69
+ */
70
+ export declare function findMissingHostPrebuilds(addons: NativeAddon[], hosts: string[]): Promise<HostPrebuildPackage[]>;
71
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,227 @@
1
+ import { promises as fsp } from 'node:fs';
2
+ import path from 'node:path';
3
+ import semver from 'semver';
4
+ import { createCommandLogger } from '../command-logger.js';
5
+ import { readAddonPackageJson } from '../verify/addon-source.js';
6
+ import { checkPrebuilds, isMobileHost } from '../verify/prebuilds.js';
7
+ import { checkPackageManagerSupport, detectPackageManager, installArgs, isPackageManagerName, isWorkspaceMember, runPackageManager } from './package-manager.js';
8
+ import { HostPrebuildsInstallFailedError, HostPrebuildsInstallRefusedError } from '../../utils/errors-client.js';
9
+ /** npm's package-name grammar. On Windows the names pass through a shell. */
10
+ const PACKAGE_NAME = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
11
+ /**
12
+ * Installs the per-platform prebuild packages that addons need for mobile
13
+ * hosts and that the project does not have yet.
14
+ *
15
+ * A split addon names one platform package per host in the `#host-addon` map
16
+ * of its package.json (`<addon>-ios` covers every iOS host). Desktop platform
17
+ * packages install as the addon's os/cpu-filtered optionalDependencies, but a
18
+ * build host never reports a mobile os, so mobile ones have to be declared.
19
+ * This declares the missing ones in package.json, pinned to each addon's
20
+ * exact version, using the project's own package manager.
21
+ *
22
+ * A host whose prebuild already resolves is skipped: a local
23
+ * `prebuilds/<host>` in the addon (source builds) or its platform package at
24
+ * the addon's version. Throws `HostPrebuildsInstallRefusedError` when no
25
+ * supported package manager (npm 7+, pnpm, bun, Yarn Berry) is found, and
26
+ * `HostPrebuildsInstallFailedError` when the install does not produce the
27
+ * packages.
28
+ */
29
+ export async function ensureHostPrebuilds(options) {
30
+ const logger = createCommandLogger(options);
31
+ const addons = await resolveAddons(options.projectRoot, options.addons, logger);
32
+ return installMissingHostPrebuilds({
33
+ projectRoot: options.projectRoot,
34
+ hosts: options.hosts,
35
+ addons,
36
+ packageManager: options.packageManager,
37
+ quiet: options.quiet === true,
38
+ logger
39
+ });
40
+ }
41
+ /** `ensureHostPrebuilds` for addons the caller has already located. */
42
+ export async function installMissingHostPrebuilds(options) {
43
+ const { projectRoot, hosts, addons, quiet, logger } = options;
44
+ const missing = await findMissingHostPrebuilds(addons, hosts);
45
+ if (missing.length === 0)
46
+ return { installed: [], packageManager: null };
47
+ const dependencies = toDependencies(missing);
48
+ const manager = await choosePackageManager(projectRoot, options.packageManager, dependencies);
49
+ const args = installArgs(manager, missing.map((pkg) => `${pkg.name}@${pkg.version}`));
50
+ const command = `${manager} ${args.join(' ')}`;
51
+ logger.info(`\n📦 Installing addon platform packages with ${manager}:`);
52
+ for (const pkg of missing) {
53
+ logger.info(` ${pkg.name}@${pkg.version} (${pkg.hosts.join(', ')})`);
54
+ }
55
+ const result = await runPackageManager(manager, args, { cwd: projectRoot, quiet });
56
+ if (result.error !== undefined || result.code !== 0) {
57
+ const outcome = result.error?.message ?? `exited with code ${result.code ?? 'unknown'}`;
58
+ const stderr = result.stderr.trim();
59
+ throw new HostPrebuildsInstallFailedError(`\`${command}\` ${outcome}${stderr.length > 0 ? `\n\n${stderr}` : ''}`, dependencies, result.error);
60
+ }
61
+ const stillMissing = await findMissingHostPrebuilds(addons, hosts);
62
+ if (stillMissing.length > 0) {
63
+ const names = stillMissing.map((pkg) => pkg.name).join(', ');
64
+ const pnpHint = manager === 'yarn'
65
+ ? "; Yarn Plug'n'Play installs no node_modules, so set `nodeLinker: node-modules`"
66
+ : '';
67
+ throw new HostPrebuildsInstallFailedError(`\`${command}\` succeeded, but ${names} is still not installed under node_modules${pnpHint}`, toDependencies(stillMissing));
68
+ }
69
+ logger.info(` Installed ${missing.length} platform package${missing.length === 1 ? '' : 's'}`);
70
+ return { installed: missing, packageManager: manager };
71
+ }
72
+ /**
73
+ * The platform packages `addons` still need for the mobile hosts in `hosts`,
74
+ * one entry per package. Throws when two installed versions of one addon need
75
+ * the same platform package, since only one version of it can be installed.
76
+ */
77
+ export async function findMissingHostPrebuilds(addons, hosts) {
78
+ const mobileHosts = hosts.filter(isMobileHost);
79
+ const packages = new Map();
80
+ if (mobileHosts.length === 0)
81
+ return [];
82
+ for (const addon of addons) {
83
+ for (const issue of await checkPrebuilds({ addon, hosts: mobileHosts })) {
84
+ const pin = issue.platformPackage;
85
+ if (pin === undefined)
86
+ continue;
87
+ const known = packages.get(pin.name);
88
+ if (known === undefined) {
89
+ packages.set(pin.name, { ...pin, addon: addon.name, hosts: [issue.host] });
90
+ continue;
91
+ }
92
+ if (known.version !== pin.version) {
93
+ throw new HostPrebuildsInstallRefusedError(`${addon.name} is installed at both ${known.version} and ${pin.version}, but ` +
94
+ `${pin.name} can be installed at only one version. ` +
95
+ `Deduplicate ${addon.name} to a single version and try again`, {});
96
+ }
97
+ if (!known.hosts.includes(issue.host))
98
+ known.hosts.push(issue.host);
99
+ }
100
+ }
101
+ const missing = [...packages.values()].sort((a, b) => a.name.localeCompare(b.name));
102
+ for (const pkg of missing) {
103
+ if (!PACKAGE_NAME.test(pkg.name) || semver.valid(pkg.version) === null) {
104
+ throw new HostPrebuildsInstallRefusedError(`${pkg.addon} names "${pkg.name}@${pkg.version}", which is not a valid package name and version`, {});
105
+ }
106
+ }
107
+ return missing;
108
+ }
109
+ async function choosePackageManager(projectRoot, requested, dependencies) {
110
+ let manager;
111
+ let source = '';
112
+ if (requested !== undefined) {
113
+ if (!isPackageManagerName(requested)) {
114
+ throw new HostPrebuildsInstallRefusedError(`packageManager "${String(requested)}" is not npm, pnpm, bun, or yarn`, dependencies);
115
+ }
116
+ manager = requested;
117
+ }
118
+ else {
119
+ const detected = await detectPackageManager(projectRoot);
120
+ if (detected.manager === null) {
121
+ throw new HostPrebuildsInstallRefusedError(`could not tell which package manager the project uses (${detected.reason})`, dependencies);
122
+ }
123
+ manager = detected.manager;
124
+ source = ` (detected from ${detected.source})`;
125
+ }
126
+ const unsupported = await checkPackageManagerSupport(manager, projectRoot);
127
+ if (unsupported !== null) {
128
+ throw new HostPrebuildsInstallRefusedError(`${unsupported}${source}`, dependencies);
129
+ }
130
+ return manager;
131
+ }
132
+ /**
133
+ * The installed addons named in `names`, or every installed addon when
134
+ * `names` is omitted. For each name the copy in the nearest modules directory
135
+ * wins, as it does for Node's resolver.
136
+ */
137
+ async function resolveAddons(projectRoot, names, logger) {
138
+ const moduleDirs = await reachableModuleDirs(projectRoot);
139
+ const wanted = names === undefined ? null : new Set(names);
140
+ const found = new Map();
141
+ for (const modulesDir of moduleDirs) {
142
+ const candidates = wanted === null ? await listPackageNames(modulesDir) : [...wanted];
143
+ for (const name of candidates) {
144
+ if (found.has(name))
145
+ continue;
146
+ const result = await readAddonPackageJson({
147
+ packageJsonPath: path.join(modulesDir, ...name.split('/'), 'package.json'),
148
+ expectedName: name
149
+ });
150
+ if (result.addon !== undefined)
151
+ found.set(name, result.addon);
152
+ }
153
+ }
154
+ for (const name of wanted ?? []) {
155
+ if (!found.has(name))
156
+ logger.warn(`${name} is not an installed native addon; skipping it`);
157
+ }
158
+ return [...found.values()];
159
+ }
160
+ /**
161
+ * Every modules directory a package installed for `projectRoot` can sit in,
162
+ * nearest first: the project's `node_modules` and that of each workspace root
163
+ * above it that lists the project (npm, Yarn, and bun hoist workspace
164
+ * dependencies to the root), plus the views pnpm (`.pnpm/node_modules`) and
165
+ * bun's isolated layout (`.bun/node_modules`) keep of the packages that are not
166
+ * direct dependencies. Any other enclosing project installed its node_modules
167
+ * for itself, so its addons do not belong to this one.
168
+ */
169
+ async function reachableModuleDirs(projectRoot) {
170
+ const start = path.resolve(projectRoot);
171
+ const dirs = [];
172
+ let dir = start;
173
+ for (;;) {
174
+ if (dir === start || (await isWorkspaceMember(dir, start))) {
175
+ const nodeModules = path.join(dir, 'node_modules');
176
+ for (const candidate of [
177
+ nodeModules,
178
+ path.join(nodeModules, '.pnpm', 'node_modules'),
179
+ path.join(nodeModules, '.bun', 'node_modules')
180
+ ]) {
181
+ if (await isDirectory(candidate))
182
+ dirs.push(candidate);
183
+ }
184
+ }
185
+ const parent = path.dirname(dir);
186
+ if (parent === dir)
187
+ return dirs;
188
+ dir = parent;
189
+ }
190
+ }
191
+ async function listPackageNames(modulesDir) {
192
+ const names = [];
193
+ for (const entry of await readDirNames(modulesDir)) {
194
+ if (entry.startsWith('.'))
195
+ continue;
196
+ if (!entry.startsWith('@')) {
197
+ names.push(entry);
198
+ continue;
199
+ }
200
+ for (const scoped of await readDirNames(path.join(modulesDir, entry))) {
201
+ if (!scoped.startsWith('.'))
202
+ names.push(`${entry}/${scoped}`);
203
+ }
204
+ }
205
+ return names;
206
+ }
207
+ async function readDirNames(dir) {
208
+ try {
209
+ return (await fsp.readdir(dir, { withFileTypes: true }))
210
+ .filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
211
+ .map((entry) => entry.name);
212
+ }
213
+ catch {
214
+ return [];
215
+ }
216
+ }
217
+ async function isDirectory(dir) {
218
+ try {
219
+ return (await fsp.stat(dir)).isDirectory();
220
+ }
221
+ catch {
222
+ return false;
223
+ }
224
+ }
225
+ function toDependencies(packages) {
226
+ return Object.fromEntries(packages.map((pkg) => [pkg.name, pkg.version]));
227
+ }
@@ -0,0 +1,54 @@
1
+ export type PackageManagerName = 'npm' | 'pnpm' | 'bun' | 'yarn';
2
+ /**
3
+ * The package manager that owns `projectRoot`, from the project's own
4
+ * `packageManager` field, lockfile, or node_modules install files. A project
5
+ * with none of those is a workspace member only if the nearest directory above
6
+ * it that has them is a workspace root listing it; any other enclosing project
7
+ * belongs to someone else, and its package manager is not this one's.
8
+ */
9
+ export declare function detectPackageManager(projectRoot: string): Promise<{
10
+ manager: null;
11
+ reason: string;
12
+ source?: never;
13
+ } | {
14
+ manager: PackageManagerName;
15
+ source: string;
16
+ reason?: never;
17
+ }>;
18
+ export declare function isPackageManagerName(value: string): value is PackageManagerName;
19
+ /**
20
+ * Whether the workspace globs declared at `rootDir` (package.json `workspaces`
21
+ * for npm, Yarn, and bun; `pnpm-workspace.yaml` for pnpm) include `memberDir`.
22
+ */
23
+ export declare function isWorkspaceMember(rootDir: string, memberDir: string): Promise<boolean>;
24
+ /**
25
+ * The `packages` list of a pnpm-workspace.yaml, in block (`- 'apps/*'`) or
26
+ * flow (`packages: ['apps/*']`) form. The SDK has no YAML parser, and the
27
+ * file's other keys are irrelevant here.
28
+ */
29
+ export declare function parsePnpmWorkspacePackages(yaml: string): string[];
30
+ /**
31
+ * Why `manager` cannot install in `cwd`, or null when it can. The supported
32
+ * set matches the installers the split addons document: npm 7+, pnpm, bun,
33
+ * and Yarn Berry.
34
+ */
35
+ export declare function checkPackageManagerSupport(manager: PackageManagerName, cwd: string): Promise<string | null>;
36
+ /** Arguments that add `specs` as exact-version dependencies of the package in cwd. */
37
+ export declare function installArgs(manager: PackageManagerName, specs: string[]): string[];
38
+ export interface RunPackageManagerResult {
39
+ code: number | null;
40
+ stdout: string;
41
+ stderr: string;
42
+ error?: Error;
43
+ }
44
+ /**
45
+ * Quiet runs capture stdout and stderr; other runs stream them to this
46
+ * process. Windows resolves the package managers' `.cmd` shims through the
47
+ * shell, which does not escape arguments, so callers must pass only
48
+ * validated ones.
49
+ */
50
+ export declare function runPackageManager(manager: PackageManagerName, args: string[], options: {
51
+ cwd: string;
52
+ quiet: boolean;
53
+ }): Promise<RunPackageManagerResult>;
54
+ //# sourceMappingURL=package-manager.d.ts.map