@expo-harmony/expo-modules-autolinking 55.0.25-harmony.1

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 (103) hide show
  1. package/README.md +12 -0
  2. package/bin/expo-harmony-autolinking.ts +14 -0
  3. package/build/bin/expo-harmony-autolinking.d.ts +2 -0
  4. package/build/bin/expo-harmony-autolinking.js +11 -0
  5. package/build/index.d.ts +8 -0
  6. package/build/index.js +15 -0
  7. package/build/src/autolinking/link.d.ts +3 -0
  8. package/build/src/autolinking/link.js +150 -0
  9. package/build/src/autolinking/resolve.d.ts +4 -0
  10. package/build/src/autolinking/resolve.js +25 -0
  11. package/build/src/autolinking/search.d.ts +3 -0
  12. package/build/src/autolinking/search.js +357 -0
  13. package/build/src/autolinking/verify.d.ts +4 -0
  14. package/build/src/autolinking/verify.js +366 -0
  15. package/build/src/commands/autolinkingOptions.d.ts +12 -0
  16. package/build/src/commands/autolinkingOptions.js +35 -0
  17. package/build/src/commands/cli.d.ts +2 -0
  18. package/build/src/commands/cli.js +50 -0
  19. package/build/src/commands/link.d.ts +2 -0
  20. package/build/src/commands/link.js +27 -0
  21. package/build/src/commands/output.d.ts +3 -0
  22. package/build/src/commands/output.js +33 -0
  23. package/build/src/commands/resolve.d.ts +2 -0
  24. package/build/src/commands/resolve.js +13 -0
  25. package/build/src/commands/search.d.ts +2 -0
  26. package/build/src/commands/search.js +13 -0
  27. package/build/src/commands/verify.d.ts +2 -0
  28. package/build/src/commands/verify.js +17 -0
  29. package/build/src/config/constants.d.ts +17 -0
  30. package/build/src/config/constants.js +40 -0
  31. package/build/src/config/options.d.ts +13 -0
  32. package/build/src/config/options.js +202 -0
  33. package/build/src/errors.d.ts +18 -0
  34. package/build/src/errors.js +35 -0
  35. package/build/src/harmony/manifest/generate.d.ts +4 -0
  36. package/build/src/harmony/manifest/generate.js +55 -0
  37. package/build/src/harmony/manifest/index.d.ts +7 -0
  38. package/build/src/harmony/manifest/index.js +64 -0
  39. package/build/src/harmony/materialize.d.ts +12 -0
  40. package/build/src/harmony/materialize.js +278 -0
  41. package/build/src/harmony/ohpm/dependencies.d.ts +7 -0
  42. package/build/src/harmony/ohpm/dependencies.js +52 -0
  43. package/build/src/harmony/persistence/canonicalize.d.ts +15 -0
  44. package/build/src/harmony/persistence/canonicalize.js +108 -0
  45. package/build/src/harmony/providers/host.d.ts +3 -0
  46. package/build/src/harmony/providers/host.js +88 -0
  47. package/build/src/harmony/rnoh/cli.d.ts +2 -0
  48. package/build/src/harmony/rnoh/cli.js +55 -0
  49. package/build/src/harmony/rnoh/link.d.ts +2 -0
  50. package/build/src/harmony/rnoh/link.js +419 -0
  51. package/build/src/harmony/rnoh/packageMetadata.d.ts +12 -0
  52. package/build/src/harmony/rnoh/packageMetadata.js +125 -0
  53. package/build/src/harmony/transaction/previousState.d.ts +15 -0
  54. package/build/src/harmony/transaction/previousState.js +96 -0
  55. package/build/src/harmony/transaction/publish.d.ts +6 -0
  56. package/build/src/harmony/transaction/publish.js +272 -0
  57. package/build/src/harmony/transaction/stageProject.d.ts +18 -0
  58. package/build/src/harmony/transaction/stageProject.js +313 -0
  59. package/build/src/metadata/descriptor.d.ts +19 -0
  60. package/build/src/metadata/descriptor.js +306 -0
  61. package/build/src/metadata/host.d.ts +5 -0
  62. package/build/src/metadata/host.js +37 -0
  63. package/build/src/metadata/schema.d.ts +10 -0
  64. package/build/src/metadata/schema.js +171 -0
  65. package/build/src/types.d.ts +194 -0
  66. package/build/src/types.js +2 -0
  67. package/build/src/utilities/values.d.ts +17 -0
  68. package/build/src/utilities/values.js +205 -0
  69. package/harmony-module.schema.json +28 -0
  70. package/index.ts +41 -0
  71. package/package.json +66 -0
  72. package/src/autolinking/link.ts +170 -0
  73. package/src/autolinking/resolve.ts +33 -0
  74. package/src/autolinking/search.ts +369 -0
  75. package/src/autolinking/verify.ts +415 -0
  76. package/src/commands/autolinkingOptions.ts +38 -0
  77. package/src/commands/cli.ts +56 -0
  78. package/src/commands/link.ts +28 -0
  79. package/src/commands/output.ts +36 -0
  80. package/src/commands/resolve.ts +14 -0
  81. package/src/commands/search.ts +14 -0
  82. package/src/commands/verify.ts +17 -0
  83. package/src/config/constants.ts +45 -0
  84. package/src/config/options.ts +195 -0
  85. package/src/errors.ts +48 -0
  86. package/src/harmony/manifest/generate.ts +65 -0
  87. package/src/harmony/manifest/index.ts +81 -0
  88. package/src/harmony/materialize.ts +382 -0
  89. package/src/harmony/ohpm/dependencies.ts +59 -0
  90. package/src/harmony/persistence/canonicalize.ts +156 -0
  91. package/src/harmony/providers/host.ts +147 -0
  92. package/src/harmony/rnoh/cli.ts +59 -0
  93. package/src/harmony/rnoh/link.ts +481 -0
  94. package/src/harmony/rnoh/packageMetadata.ts +105 -0
  95. package/src/harmony/transaction/previousState.ts +71 -0
  96. package/src/harmony/transaction/publish.ts +287 -0
  97. package/src/harmony/transaction/stageProject.ts +345 -0
  98. package/src/metadata/descriptor.ts +324 -0
  99. package/src/metadata/host.ts +41 -0
  100. package/src/metadata/schema.ts +184 -0
  101. package/src/types.ts +220 -0
  102. package/src/utilities/values.ts +190 -0
  103. package/tsconfig.json +24 -0
@@ -0,0 +1,170 @@
1
+ import path from 'node:path';
2
+
3
+ import type { LinkOptions, LinkResult, VerifyOptions } from '../types';
4
+ import { ExpoArtifacts, ManifestArtifact, RnohArtifacts, managedArtifactsForHarmonyRoot } from '../config/constants';
5
+ import { renderArkTsHostProviderSource } from '../harmony/providers/host';
6
+ import { serializeManifest } from '../harmony/manifest/generate';
7
+ import { HarmonyAutolinkingError } from '../errors';
8
+ import { normalizeOptionsAsync } from '../config/options';
9
+ import { linkRnohAsync } from '../harmony/rnoh/link';
10
+ import { cleanupStagingProjectAsync, stageProjectAsync } from '../harmony/transaction/stageProject';
11
+ import { publishArtifactsAsync } from '../harmony/transaction/publish';
12
+ import { readPreviousAutolinkingStateAsync } from '../harmony/transaction/previousState';
13
+ import { assertVerificationSucceeded, verifyModulesAsync } from './verify';
14
+ import { emitLog } from '../utilities/values';
15
+ import { materializeLocalSourcesAsync } from '../harmony/materialize';
16
+
17
+ function buildPublishEntries(staging, hostProviderSource, manifest) {
18
+ return [
19
+ ...Object.entries(RnohArtifacts).map(([name, relative]) => ({
20
+ source: staging.rnohArtifacts[name].path,
21
+ target: path.join(staging.harmonyProjectPath, relative),
22
+ })),
23
+ {
24
+ content: hostProviderSource,
25
+ target: path.join(staging.harmonyProjectPath, ExpoArtifacts.hostProvider),
26
+ },
27
+ {
28
+ content: manifest,
29
+ target: path.join(staging.projectRoot, ManifestArtifact),
30
+ },
31
+ ];
32
+ }
33
+
34
+ async function linkModulesAsync(rawOptions: LinkOptions): Promise<LinkResult> {
35
+ const options = await normalizeOptionsAsync(rawOptions);
36
+
37
+ if (typeof rawOptions.harmonyProjectPath !== 'string' || !rawOptions.harmonyProjectPath) {
38
+ throw new HarmonyAutolinkingError('INVALID_OPTIONS', 'link requires harmonyProjectPath.', { stage: 'link' });
39
+ }
40
+
41
+ const verify = await verifyModulesAsync((rawOptions.modules !== undefined
42
+ ? { modules: rawOptions.modules }
43
+ : { ...options, searchResult: rawOptions.searchResult }) as VerifyOptions);
44
+ assertVerificationSucceeded(verify, 'link');
45
+
46
+ const modules = verify.modules;
47
+ const pendingSource = modules.filter(module => module.artifact.kind === 'local-source');
48
+ if (pendingSource.length > 0) {
49
+ if (pendingSource.some(module => module.source !== 'nativeModulesDir')) {
50
+ throw new HarmonyAutolinkingError(
51
+ 'SOURCE_ARTIFACT_MATERIALIZATION_REQUIRED',
52
+ 'Only nativeModulesDir modules may build a local Harmony HAR.',
53
+ { stage: 'link', details: pendingSource.map(module => module.packageName).sort() }
54
+ );
55
+ }
56
+ await materializeLocalSourcesAsync(pendingSource, {
57
+ projectRoot: options.projectRoot,
58
+ timeoutMs: rawOptions.timeoutMs,
59
+ outputLimit: rawOptions.outputLimit,
60
+ env: rawOptions.env,
61
+ });
62
+ }
63
+
64
+ const hostProviderSource = renderArkTsHostProviderSource(modules);
65
+
66
+ let staging;
67
+ let failure;
68
+ let cleanupWarning;
69
+ const warnings = [];
70
+
71
+ try {
72
+ const previous = await readPreviousAutolinkingStateAsync(options.projectRoot);
73
+
74
+ if (previous.warning) {
75
+ warnings.push(previous.warning);
76
+ emitLog(rawOptions.logger, 'warn', previous.warning.message, previous.warning);
77
+ }
78
+
79
+ staging = await stageProjectAsync({
80
+ projectRoot: options.projectRoot,
81
+ harmonyProjectPath: rawOptions.harmonyProjectPath,
82
+ nodeModulesPath: rawOptions.nodeModulesPath,
83
+ modules,
84
+ buildType: options.buildType,
85
+ previousManagedOhpmPackageNames: previous.managedOhpmPackageNames,
86
+ rnohCliPackageJsonPath: rawOptions.rnohCliPackageJsonPath,
87
+ });
88
+
89
+ staging.rnohArtifacts = await linkRnohAsync({
90
+ ...staging,
91
+ modules,
92
+ buildType: options.buildType,
93
+ exclude: staging.rnohRuntimePackages,
94
+ commandNodeModulesPath: staging.sourceNodeModulesPath,
95
+ reactNativeExecutable: rawOptions.reactNativeExecutable,
96
+ rnohCliPackageJsonPath: rawOptions.rnohCliPackageJsonPath,
97
+ timeoutMs: rawOptions.timeoutMs,
98
+ outputLimit: rawOptions.outputLimit,
99
+ env: rawOptions.env,
100
+ });
101
+
102
+ const harmonyRoot = path.relative(staging.projectRoot, staging.harmonyProjectPath)
103
+ .split(path.sep).join('/');
104
+ const managed = managedArtifactsForHarmonyRoot(harmonyRoot);
105
+
106
+ const manifest = serializeManifest(modules, {
107
+ buildType: options.buildType,
108
+ managedArtifacts: managed,
109
+ });
110
+
111
+ const published = await publishArtifactsAsync({
112
+ allowedRoot: staging.projectRoot,
113
+ lockPath: path.join(staging.projectRoot, '.expo/harmony/autolinking.lock'),
114
+ files: buildPublishEntries(staging, hostProviderSource, manifest),
115
+ stale: previous.artifacts.filter(artifact => !managed.some((relative) => {
116
+ return artifact === path.join(staging.projectRoot, ...relative.split('/'));
117
+ })),
118
+ });
119
+
120
+ const result: LinkResult = {
121
+ platform: 'harmony',
122
+ modules,
123
+ buildType: options.buildType,
124
+ managedArtifacts: [...managed],
125
+ changedArtifacts: published.changed.map(target => path.relative(staging.projectRoot, target).split(path.sep).join('/')),
126
+ unchangedArtifacts: published.unchanged.map(target => path.relative(staging.projectRoot, target).split(path.sep).join('/')),
127
+ diagnostics: verify.diagnostics,
128
+ warnings,
129
+ };
130
+
131
+ emitLog(rawOptions.logger, 'info', 'Harmony autolinking completed.', {
132
+ moduleCount: result.modules.length,
133
+ changedArtifactCount: result.changedArtifacts.length,
134
+ });
135
+ return result;
136
+ } catch (error) {
137
+ failure = error;
138
+ emitLog(rawOptions.logger, 'error', 'Harmony autolinking failed.', {
139
+ code: error.code || 'ERR_EXPO_HARMONY_UNKNOWN',
140
+ stage: error.stage || 'link',
141
+ });
142
+ } finally {
143
+ if (staging) {
144
+ const cleanup = await cleanupStagingProjectAsync(staging);
145
+ if (cleanup) {
146
+ const warning = { ...cleanup };
147
+ delete warning.cause;
148
+ warnings.push(warning);
149
+ emitLog(rawOptions.logger, 'warn', warning.message, warning);
150
+ if (failure) cleanupWarning = warning;
151
+ }
152
+ }
153
+ }
154
+
155
+ throw new HarmonyAutolinkingError(
156
+ typeof failure.code === 'string' ? failure.code : 'UNKNOWN',
157
+ failure.message || 'Harmony autolinking failed.',
158
+ {
159
+ cause: failure,
160
+ stage: failure.stage || 'link',
161
+ packageName: failure.packageName,
162
+ diagnostics: failure.diagnostics,
163
+ details: cleanupWarning
164
+ ? { ...(failure.details || {}), cleanupWarning }
165
+ : failure.details,
166
+ }
167
+ );
168
+ }
169
+
170
+ export { linkModulesAsync };
@@ -0,0 +1,33 @@
1
+ import type { ModuleDescriptor, ResolveOptions } from '../types';
2
+ import { searchModulesAsync } from './search';
3
+ import { createDescriptorFromSearchRecordAsync } from '../metadata/descriptor';
4
+ import { compareText, emitLog } from '../utilities/values';
5
+
6
+ function compareModuleDescriptors(left, right) {
7
+ return compareText(left.packageName, right.packageName)
8
+ || compareText(left.packageVersion, right.packageVersion)
9
+ || compareText(left.packageRoot, right.packageRoot);
10
+ }
11
+
12
+ async function resolveModulesAsync(options: ResolveOptions = {}): Promise<ModuleDescriptor[]> {
13
+ const searchResult = options.searchResult || await searchModulesAsync(options);
14
+
15
+ const records = [...searchResult.modules].sort(compareModuleDescriptors);
16
+ const descriptors = [];
17
+
18
+ for (const record of records) {
19
+ descriptors.push(await createDescriptorFromSearchRecordAsync(record, searchResult.options.buildType));
20
+ }
21
+
22
+ const result = descriptors.sort(compareModuleDescriptors);
23
+
24
+ emitLog(options.logger, 'debug', 'Harmony module resolution completed.', {
25
+ moduleCount: descriptors.length,
26
+ });
27
+ return result;
28
+ }
29
+
30
+ export {
31
+ compareModuleDescriptors,
32
+ resolveModulesAsync,
33
+ };
@@ -0,0 +1,369 @@
1
+ import * as expoAutolinking from 'expo-modules-autolinking/exports';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { isDeepStrictEqual } from 'node:util';
5
+
6
+ import type { SearchOptions, SearchResult } from '../types';
7
+ import { Platform } from '../config/constants';
8
+ import { HarmonyAutolinkingError } from '../errors';
9
+ import { normalizeModuleOverrides, normalizeOptionsAsync } from '../config/options';
10
+ import { compareText, emitLog, isObject, isPathInside, pathExistsAsync, readJsonAsync } from '../utilities/values';
11
+
12
+ function loadUpstreamAutolinkingApi() {
13
+ const upstreamApi = expoAutolinking;
14
+
15
+ for (const name of ['makeCachedDependenciesLinker']) {
16
+ if (typeof upstreamApi[name] !== 'function') {
17
+ throw new HarmonyAutolinkingError('INVALID_OPTIONS', `expo-modules-autolinking public export ${name} is unavailable.`, { stage: 'search' });
18
+ }
19
+ }
20
+ return upstreamApi;
21
+ }
22
+
23
+ function supportsHarmony(config) {
24
+ return Array.isArray(config?.platforms) && config.platforms.includes(Platform);
25
+ }
26
+
27
+ function hasHarmonyExpoMetadata(config) {
28
+ return supportsHarmony(config);
29
+ }
30
+
31
+ async function readExpoModuleConfigAsync(packageRoot) {
32
+ const target = path.join(packageRoot, 'expo-module.config.json');
33
+ return await pathExistsAsync(target)
34
+ ? readJsonAsync(target, 'INVALID_METADATA', 'search')
35
+ : null;
36
+ }
37
+
38
+ function nodeModulesDepth(originPath) {
39
+ return String(originPath).split(`${path.sep}node_modules${path.sep}`).length;
40
+ }
41
+
42
+ function isPreferredRevision(left, right) {
43
+ const leftDepth = Number.isInteger(left.depth) ? left.depth : Number.POSITIVE_INFINITY;
44
+ const rightDepth = Number.isInteger(right.depth) ? right.depth : Number.POSITIVE_INFINITY;
45
+
46
+ if (leftDepth !== rightDepth) return leftDepth < rightDepth;
47
+
48
+ const leftNest = nodeModulesDepth(left.originPath);
49
+ const rightNest = nodeModulesDepth(right.originPath);
50
+
51
+ if (leftNest !== rightNest) return leftNest < rightNest;
52
+ if (left.source !== right.source) return left.source < right.source;
53
+ return compareText(left.originPath, right.originPath) < 0;
54
+ }
55
+
56
+ function mergeRevisions(resultSets) {
57
+ const merged = Object.create(null);
58
+
59
+ for (const results of resultSets) {
60
+ for (const packageName of Object.keys(results)) {
61
+ const incoming = results[packageName];
62
+ if (!incoming) continue;
63
+
64
+ const current = merged[packageName];
65
+ if (!current) {
66
+ merged[packageName] = {
67
+ ...incoming,
68
+ duplicates: [...(incoming.duplicates || [])],
69
+ };
70
+ continue;
71
+ }
72
+
73
+ if (current.path === incoming.path) {
74
+ if (!current.version && incoming.version) current.version = incoming.version;
75
+ for (const candidate of incoming.duplicates || []) {
76
+ if (candidate.path !== current.path
77
+ && !current.duplicates.some(item => item.path === candidate.path)) {
78
+ current.duplicates.push(candidate);
79
+ }
80
+ }
81
+ continue;
82
+ }
83
+
84
+ let winner = current;
85
+ let duplicate = incoming;
86
+ if (isPreferredRevision(incoming, current)) {
87
+ winner = { ...incoming, duplicates: [...(incoming.duplicates || [])] };
88
+ duplicate = current;
89
+ merged[packageName] = winner;
90
+ }
91
+
92
+ const candidates = [
93
+ { ...duplicate, duplicates: undefined },
94
+ ...(duplicate.duplicates || []),
95
+ ];
96
+ for (const candidate of candidates) {
97
+ if (candidate.path !== winner.path
98
+ && !(winner.duplicates || []).some(item => item.path === candidate.path)) {
99
+ winner.duplicates.push(candidate);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ return merged;
105
+ }
106
+
107
+ function classifySource(revision, packageRoot, options) {
108
+ if (revision.source === 2) {
109
+ return 'reactNativeProjectConfig';
110
+ }
111
+ if (options.nativeModulesDir
112
+ && isPathInside(options.nativeModulesDir, packageRoot)) {
113
+ return 'nativeModulesDir';
114
+ }
115
+ if (options.searchPaths.some(searchPath => isPathInside(searchPath, packageRoot))) {
116
+ return 'searchPath';
117
+ }
118
+ return 'dependency';
119
+ }
120
+
121
+ async function resolveDependencyRevisionsAsync(upstreamApi, options) {
122
+ const linker = upstreamApi.makeCachedDependenciesLinker({ projectRoot: options.projectRoot });
123
+ const resultSets = [];
124
+
125
+ if (options.includeReactNativeProjectConfig) {
126
+ resultSets.push(await linker.scanDependenciesFromRNProjectConfig());
127
+ }
128
+ if (options.nativeModulesDir && await pathExistsAsync(options.nativeModulesDir)) {
129
+ resultSets.push(await linker.scanDependenciesInSearchPath(options.nativeModulesDir));
130
+ }
131
+
132
+ for (const searchPath of options.searchPaths) {
133
+ if (await pathExistsAsync(searchPath)) {
134
+ resultSets.push(await linker.scanDependenciesInSearchPath(searchPath));
135
+ }
136
+ }
137
+
138
+ resultSets.push(await linker.scanDependenciesRecursively());
139
+ return mergeRevisions(resultSets);
140
+ }
141
+
142
+ async function loadOverridesAsync(deps) {
143
+ const overrides = Object.create(null);
144
+ const owners = Object.create(null);
145
+ const required = new Set();
146
+
147
+ for (const name of Object.keys(deps).sort(compareText)) {
148
+ const revision = deps[name];
149
+ if (!revision) continue;
150
+ let packageRoot;
151
+ let packageJson;
152
+ try {
153
+ packageRoot = await fs.promises.realpath(revision.path);
154
+ packageJson = await readJsonAsync(path.join(packageRoot, 'package.json'), 'INVALID_METADATA', 'search');
155
+ } catch (_cause) {
156
+ continue;
157
+ }
158
+
159
+ const packageName = typeof packageJson.name === 'string' && packageJson.name ? packageJson.name : name;
160
+ const harmony = isObject(packageJson.harmony) ? packageJson.harmony : {};
161
+ const declared = normalizeModuleOverrides(harmony.modules, {
162
+ code: 'INVALID_METADATA',
163
+ field: `${packageName} package.json#harmony.modules`,
164
+ stage: 'search',
165
+ });
166
+
167
+ const dependencies = isObject(packageJson.dependencies) ? packageJson.dependencies : {};
168
+ const optional = isObject(packageJson.optionalDependencies) ? packageJson.optionalDependencies : {};
169
+ const peers = isObject(packageJson.peerDependencies) ? packageJson.peerDependencies : {};
170
+ const peerMeta = isObject(packageJson.peerDependenciesMeta) ? packageJson.peerDependenciesMeta : {};
171
+
172
+ for (const [target, metadata] of Object.entries(declared)) {
173
+ if (![dependencies, optional, peers].some(group => Object.hasOwn(group, target))) {
174
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `${packageName} declares Harmony module ${target} but does not list it in a production dependency field.`, { packageName, stage: 'search' });
175
+ }
176
+ if (Object.hasOwn(dependencies, target)
177
+ || (Object.hasOwn(peers, target) && peerMeta[target]?.optional !== true)) {
178
+ required.add(target);
179
+ }
180
+ if (overrides[target] !== undefined && !isDeepStrictEqual(overrides[target], metadata)) {
181
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `${packageName} and ${owners[target]} declare conflicting Harmony metadata for ${target}.`, { packageName: target, stage: 'search' });
182
+ }
183
+ overrides[target] = metadata;
184
+ owners[target] = packageName;
185
+ }
186
+ }
187
+
188
+ return {
189
+ requiredTargets: required,
190
+ overrides: Object.freeze(overrides),
191
+ owners: Object.freeze(owners),
192
+ };
193
+ }
194
+
195
+ async function createSearchRecordAsync(revision, options) {
196
+ let packageRoot;
197
+ try {
198
+ packageRoot = await fs.promises.realpath(revision.path);
199
+ } catch (_cause) {
200
+ return null;
201
+ }
202
+
203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
204
+ let packageJson: Record<string, any> = {};
205
+ try {
206
+ packageJson = await readJsonAsync(path.join(packageRoot, 'package.json'), 'INVALID_METADATA', 'search');
207
+ } catch (_cause) {
208
+ // Expo's search-path scanner tolerates roots without a readable
209
+ // package.json and falls back to the dependency revision name.
210
+ }
211
+
212
+ const packageName = typeof packageJson.name === 'string' && packageJson.name
213
+ ? packageJson.name
214
+ : revision.name;
215
+ if (options.exclude.includes(revision.name || packageName)) return null;
216
+ const expo = await readExpoModuleConfigAsync(packageRoot);
217
+ const harmony = hasHarmonyExpoMetadata(expo);
218
+ const metadata = isObject(packageJson.harmony) ? packageJson.harmony : null;
219
+ const sidecar = options.moduleOverrides[packageName];
220
+
221
+ let rnohMetadata = metadata?.autolinking === undefined ? null : metadata.autolinking;
222
+ if (sidecar !== undefined) {
223
+ const { version: _version, ...metadata } = sidecar;
224
+ rnohMetadata = metadata;
225
+ }
226
+
227
+ const included = options.include.includes(revision.name || packageName);
228
+
229
+ return {
230
+ packageName,
231
+ packageVersion: typeof packageJson.version === 'string' ? packageJson.version : String(revision.version || ''),
232
+ packageRoot,
233
+ source: classifySource(revision, packageRoot, options),
234
+ supportsHarmony: harmony,
235
+ expoModuleConfig: expo,
236
+ rnohMetadata,
237
+ nativeCandidate: harmony || rnohMetadata != null,
238
+ includedForVerification: included,
239
+ revision: {
240
+ depth: Number.isInteger(revision.depth) ? revision.depth : null,
241
+ originPath: typeof revision.originPath === 'string' ? revision.originPath : packageRoot,
242
+ },
243
+ };
244
+ }
245
+
246
+ function compareSearchRecords(left, right) {
247
+ return compareText(left.packageName, right.packageName)
248
+ || compareText(left.packageVersion, right.packageVersion)
249
+ || compareText(left.packageRoot, right.packageRoot);
250
+ }
251
+
252
+ function findDuplicates(records) {
253
+ const byName = new Map();
254
+
255
+ for (const record of records) {
256
+ const revisions = byName.get(record.packageName) || [];
257
+ revisions.push({ version: record.packageVersion, path: record.packageRoot });
258
+ byName.set(record.packageName, revisions);
259
+ }
260
+
261
+ return [...byName.entries()]
262
+ .filter(([, revisions]) => new Set(revisions.map(revision => `${revision.version}\0${revision.path}`)).size > 1)
263
+ .map(([packageName, revisions]) => ({
264
+ packageName,
265
+ revisions: revisions.sort((left, right) => compareText(left.version, right.version)
266
+ || compareText(left.path, right.path)),
267
+ }))
268
+ .sort((left, right) => compareText(left.packageName, right.packageName));
269
+ }
270
+
271
+ type SearchImplementationOptions = SearchOptions & {
272
+ includeReactNativeProjectConfig?: boolean;
273
+ };
274
+
275
+ async function searchModulesAsync(rawOptions: SearchOptions = {}): Promise<SearchResult> {
276
+ const input = rawOptions as SearchImplementationOptions;
277
+ const options = {
278
+ ...await normalizeOptionsAsync(rawOptions),
279
+ includeReactNativeProjectConfig: input.includeReactNativeProjectConfig === true,
280
+ };
281
+
282
+ const upstreamApi = loadUpstreamAutolinkingApi();
283
+ const deps = await resolveDependencyRevisionsAsync(upstreamApi, options);
284
+ const overrides = await loadOverridesAsync(deps);
285
+
286
+ const modules = Object.freeze({
287
+ ...overrides.overrides,
288
+ ...options.moduleOverrides,
289
+ });
290
+ const search = { ...options, moduleOverrides: modules };
291
+ const selected = new Map();
292
+ const unique = new Map();
293
+
294
+ for (const name of Object.keys(deps).sort(compareText)) {
295
+ const resolution = deps[name];
296
+ if (!resolution) continue;
297
+
298
+ const revisions = [
299
+ resolution,
300
+ ...(resolution.duplicates || []).map(duplicate => ({
301
+ ...resolution,
302
+ ...duplicate,
303
+ duplicates: null,
304
+ })),
305
+ ];
306
+
307
+ const records = (await Promise.all(revisions.map(revision => createSearchRecordAsync(revision, search))))
308
+ .filter(Boolean);
309
+ const winner = records[0];
310
+ let chosen = winner?.nativeCandidate || winner?.includedForVerification ? winner : null;
311
+
312
+ if (!chosen && resolution.source === 1) {
313
+ chosen = records.slice(1).find(record => record.nativeCandidate || record.includedForVerification) || null;
314
+ }
315
+ if (!chosen) continue;
316
+
317
+ if (chosen.nativeCandidate) selected.set(chosen.packageName, chosen);
318
+ for (const record of records) {
319
+ const key = `${record.packageName}\0${record.packageVersion}\0${record.packageRoot}`;
320
+ if (!unique.has(key)) unique.set(key, record);
321
+ }
322
+ }
323
+
324
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
325
+ for (const [packageName, sidecar] of Object.entries(modules) as Array<[string, Record<string, any>]>) {
326
+ const record = selected.get(packageName);
327
+ if (record && sidecar.version !== undefined && sidecar.version !== record.packageVersion) {
328
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `Harmony sidecar module ${packageName} targets ${sidecar.version}, but ${record.packageVersion} is installed.`, { packageName, stage: 'search' });
329
+ }
330
+ }
331
+
332
+ const all = [...unique.values()];
333
+ const found = new Set(all.map(record => record.packageName));
334
+ for (const [packageName, owner] of Object.entries(overrides.owners)) {
335
+ if (!found.has(packageName) && overrides.requiredTargets.has(packageName)) {
336
+ throw new HarmonyAutolinkingError('INVALID_METADATA', `${owner} declares Harmony module ${packageName}, but that dependency is not installed.`, { packageName, stage: 'search' });
337
+ }
338
+ }
339
+
340
+ const verified = all.filter(record =>
341
+ modules[record.packageName] === undefined || selected.get(record.packageName)?.packageRoot === record.packageRoot);
342
+ const records = [...selected.values()].map(({
343
+ includedForVerification: _includedForVerification,
344
+ nativeCandidate: _nativeCandidate,
345
+ ...record
346
+ }) => record).sort(compareSearchRecords);
347
+
348
+ const result: SearchResult = {
349
+ platform: Platform,
350
+ modules: records,
351
+ duplicates: findDuplicates(verified),
352
+ missingIncludes: options.include.filter(name => !found.has(name)).sort(compareText),
353
+ options: {
354
+ projectRoot: options.projectRoot,
355
+ searchPaths: options.searchPaths,
356
+ nativeModulesDir: options.nativeModulesDir,
357
+ exclude: options.exclude,
358
+ include: options.include,
359
+ buildType: options.buildType,
360
+ },
361
+ };
362
+
363
+ emitLog(rawOptions.logger, 'debug', 'Harmony module search completed.', { moduleCount: records.length, duplicateCount: result.duplicates.length });
364
+ return result;
365
+ }
366
+
367
+ export {
368
+ searchModulesAsync,
369
+ };