@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,147 @@
1
+ import { HarmonyAutolinkingError } from '../../errors';
2
+ import { HostMetadataFields } from '../../metadata/host';
3
+
4
+ function collectHostExtensions(descriptors) {
5
+ const collected = Object.fromEntries(HostMetadataFields.map(field => [field, []]));
6
+
7
+ for (const descriptor of [...descriptors].sort((left, right) => (
8
+ left.packageName.localeCompare(right.packageName, 'en')
9
+ ))) {
10
+ const declarations = HostMetadataFields.flatMap(field => descriptor.expo[field] || []);
11
+ if (declarations.length === 0) continue;
12
+
13
+ const name = descriptor.arkTs?.ohPackageName;
14
+ if (!name) {
15
+ throw new HarmonyAutolinkingError(
16
+ 'INVALID_METADATA',
17
+ 'Harmony host extension classes must be exported by the module primary HAR.',
18
+ { packageName: descriptor.packageName, stage: 'generate-host-provider' }
19
+ );
20
+ }
21
+
22
+ for (const field of HostMetadataFields) {
23
+ for (const className of descriptor.expo[field] || []) {
24
+ collected[field].push({ className, ohPackageName: name, packageName: descriptor.packageName });
25
+ }
26
+ }
27
+ }
28
+
29
+ return collected;
30
+ }
31
+
32
+ function moduleRegistrationId(ohPackageName, moduleClass) {
33
+ return `${ohPackageName}:${moduleClass}`;
34
+ }
35
+
36
+ function collectArkTsModuleRegistrations(descriptors) {
37
+ const registrations = [];
38
+
39
+ for (const descriptor of [...descriptors].sort((left, right) => (
40
+ left.packageName.localeCompare(right.packageName, 'en')
41
+ ))) {
42
+ if (descriptor.harmony.modules.length === 0) continue;
43
+ const arkTs = descriptor.arkTs;
44
+ if (!arkTs) {
45
+ throw new HarmonyAutolinkingError(
46
+ 'INVALID_METADATA',
47
+ 'Harmony ArkTS modules require the conventional Harmony library package.',
48
+ { packageName: descriptor.packageName, stage: 'generate-host-provider' }
49
+ );
50
+ }
51
+ for (const moduleClass of descriptor.harmony.modules) {
52
+ registrations.push({
53
+ moduleClass,
54
+ ohPackageName: arkTs.ohPackageName,
55
+ packageName: descriptor.packageName,
56
+ registrationId: moduleRegistrationId(arkTs.ohPackageName, moduleClass),
57
+ });
58
+ }
59
+ }
60
+ return registrations;
61
+ }
62
+
63
+ function renderArkTsHostProviderSource(descriptors) {
64
+ const extensions = collectHostExtensions(descriptors);
65
+ const registrations = collectArkTsModuleRegistrations(descriptors);
66
+ const imports = new Map();
67
+
68
+ for (const field of HostMetadataFields) {
69
+ for (const extension of extensions[field]) {
70
+ const key = `${extension.ohPackageName}\0${extension.className}`;
71
+ if (!imports.has(key)) {
72
+ imports.set(key, {
73
+ ...extension,
74
+ alias: `ExpoHarmonyHostExtension${imports.size}`,
75
+ });
76
+ }
77
+ extension.alias = imports.get(key).alias;
78
+ }
79
+ }
80
+
81
+ const lines = [
82
+ '/**',
83
+ ' * Automatically generated by @expo-harmony/expo-modules-autolinking.',
84
+ ' */',
85
+ '',
86
+ ];
87
+
88
+ lines.push(
89
+ `import { ExpoHarmonyHostState } from '@expo-harmony/expo-modules-core/Autolinking';`,
90
+ 'import type {',
91
+ ' ExpoHarmonyHostProvider,',
92
+ ...(registrations.length > 0 ? [' ExpoModuleContext,'] : []),
93
+ ' ExpoModuleRegistration,',
94
+ `} from '@expo-harmony/expo-modules-core/Autolinking';`
95
+ );
96
+
97
+ registrations.forEach((registration, index) => {
98
+ registration.alias = `ExpoHarmonyModule${index}`;
99
+ lines.push(
100
+ `import { ${registration.moduleClass} as ${registration.alias} } from '${registration.ohPackageName}';`
101
+ );
102
+ });
103
+
104
+ for (const extension of imports.values()) {
105
+ lines.push(
106
+ `import { ${extension.className} as ${extension.alias} } from '${extension.ohPackageName}';`
107
+ );
108
+ }
109
+
110
+ lines.push(
111
+ '',
112
+ 'class GeneratedExpoHarmonyHostProvider implements ExpoHarmonyHostProvider {',
113
+ ' readonly hostState: ExpoHarmonyHostState = new ExpoHarmonyHostState();',
114
+ '',
115
+ ' readonly expoModules: ExpoModuleRegistration[] = ['
116
+ );
117
+ for (const registration of registrations) {
118
+ lines.push(
119
+ ' {',
120
+ ` registrationId: ${JSON.stringify(registration.registrationId)},`,
121
+ ` moduleClassName: ${JSON.stringify(registration.moduleClass)},`,
122
+ ` factory: (context: ExpoModuleContext) => new ${registration.alias}(context),`,
123
+ ' },'
124
+ );
125
+ }
126
+ lines.push(
127
+ ' ];',
128
+ '}',
129
+ '',
130
+ 'export const expoHarmonyHostProvider: ExpoHarmonyHostProvider =',
131
+ ' new GeneratedExpoHarmonyHostProvider();',
132
+ '',
133
+ '@Builder',
134
+ 'export function ExpoHarmonyRootView(): void {'
135
+ );
136
+
137
+ for (const extension of extensions.rootViewComponents) {
138
+ lines.push(` ${extension.alias}()`);
139
+ }
140
+ lines.push('}', '');
141
+ return lines.join('\n');
142
+ }
143
+
144
+ export {
145
+ moduleRegistrationId,
146
+ renderArkTsHostProviderSource,
147
+ };
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { RnohCliPackage } from '../../config/constants';
5
+ import { HarmonyAutolinkingError } from '../../errors';
6
+ import { readJsonAsync, realpathExistingAsync, resolvePackageFromProject } from '../../utilities/values';
7
+
8
+ function findPackageJson(projectRoot, packageName) {
9
+ try {
10
+ return resolvePackageFromProject(projectRoot, `${packageName}/package.json`);
11
+ } catch (cause) {
12
+ throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `${RnohCliPackage} is not installed.`, {
13
+ cause,
14
+ stage: 'rnoh-preflight',
15
+ });
16
+ }
17
+ }
18
+
19
+ async function resolveCliAsync(cliOptions) {
20
+ const projectRoot = await realpathExistingAsync(cliOptions.projectRoot, {
21
+ type: 'directory',
22
+ field: 'projectRoot',
23
+ stage: 'rnoh-preflight',
24
+ });
25
+
26
+ const raw = cliOptions.rnohCliPackageJsonPath
27
+ || findPackageJson(projectRoot, RnohCliPackage);
28
+ const pkg = await realpathExistingAsync(raw, {
29
+ type: 'file',
30
+ code: 'RNOH_CLI_NOT_FOUND',
31
+ field: 'RNOH CLI package.json',
32
+ stage: 'rnoh-preflight',
33
+ });
34
+
35
+ const packageJson = await readJsonAsync(pkg, 'RNOH_CLI_NOT_FOUND', 'rnoh-preflight');
36
+ if (packageJson.name !== RnohCliPackage) {
37
+ throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `Expected ${RnohCliPackage}, found ${String(packageJson.name)}.`, { stage: 'rnoh-preflight' });
38
+ }
39
+
40
+ const pluginRoot = path.dirname(pkg);
41
+ const pluginConfig = path.join(pluginRoot, 'react-native.config.js');
42
+ if (!fs.existsSync(pluginConfig)) {
43
+ throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `${RnohCliPackage} does not expose its public React Native CLI plugin config.`, { stage: 'rnoh-preflight' });
44
+ }
45
+
46
+ const exec = cliOptions.reactNativeExecutable
47
+ || path.join(cliOptions.nodeModulesPath || path.join(projectRoot, 'node_modules'), '.bin', 'react-native');
48
+ const executable = await realpathExistingAsync(exec, {
49
+ type: 'file',
50
+ code: 'RNOH_CLI_NOT_FOUND',
51
+ field: 'project-local react-native executable',
52
+ stage: 'rnoh-preflight',
53
+ });
54
+ return executable;
55
+ }
56
+
57
+ export {
58
+ resolveCliAsync,
59
+ };
@@ -0,0 +1,481 @@
1
+ import childProcess from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import { RnohArtifacts } from '../../config/constants';
6
+ import { HarmonyAutolinkingError } from '../../errors';
7
+ import { assertSafeRnohPackageList } from '../../config/options';
8
+ import { isPathInside, sanitizeOutput } from '../../utilities/values';
9
+ import { createManifest } from '../manifest/generate';
10
+ import { canonicalizeOhpmManifest } from '../persistence/canonicalize';
11
+ import { resolveRnohMetadata } from './packageMetadata';
12
+ import { resolveCliAsync } from './cli';
13
+
14
+ const DEFAULT_TIMEOUT_MS = 120_000;
15
+ const DEFAULT_OUTPUT_LIMIT = 64 * 1024;
16
+
17
+ function appendBounded(current, chunk, limit) {
18
+ if (current.length >= limit) return current;
19
+ return Buffer.concat([current, chunk.subarray(0, limit - current.length)]);
20
+ }
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ function spawnBoundedAsync(executable, argv, options: Record<string, any> = {}) {
24
+ return new Promise((resolve, reject) => {
25
+ const outputLimit = options.outputLimit === undefined ? DEFAULT_OUTPUT_LIMIT : options.outputLimit;
26
+ const timeoutMs = options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
27
+
28
+ if (!Number.isInteger(outputLimit) || outputLimit <= 0
29
+ || !Number.isInteger(timeoutMs) || timeoutMs <= 0) {
30
+ reject(new HarmonyAutolinkingError('INVALID_OPTIONS', 'timeoutMs and outputLimit must be positive integers.', { stage: 'rnoh-preflight' }));
31
+ return;
32
+ }
33
+
34
+ let stdout = Buffer.alloc(0);
35
+ let stderr = Buffer.alloc(0);
36
+ let stdoutTruncated = false;
37
+ let stderrTruncated = false;
38
+ let timedOut = false;
39
+ let killTimer;
40
+ // eslint-disable-next-line prefer-const
41
+ let timeout;
42
+ let settled = false;
43
+ let child;
44
+
45
+ try {
46
+ child = childProcess.spawn(executable, argv, {
47
+ cwd: options.cwd,
48
+ env: options.env || process.env,
49
+ shell: false,
50
+ stdio: ['ignore', 'pipe', 'pipe'],
51
+ });
52
+ } catch (cause) {
53
+ reject(cause);
54
+ return;
55
+ }
56
+
57
+ child.stdout.on('data', (chunk) => {
58
+ if (stdout.length + chunk.length > outputLimit) stdoutTruncated = true;
59
+ stdout = appendBounded(stdout, chunk, outputLimit);
60
+ });
61
+
62
+ child.stderr.on('data', (chunk) => {
63
+ if (stderr.length + chunk.length > outputLimit) stderrTruncated = true;
64
+ stderr = appendBounded(stderr, chunk, outputLimit);
65
+ });
66
+
67
+ child.on('error', (error) => {
68
+ if (settled) return;
69
+ settled = true;
70
+ clearTimeout(timeout);
71
+ clearTimeout(killTimer);
72
+ reject(error);
73
+ });
74
+
75
+ timeout = setTimeout(() => {
76
+ timedOut = true;
77
+ child.kill('SIGTERM');
78
+ killTimer = setTimeout(() => child.kill('SIGKILL'), 1_000);
79
+ killTimer.unref?.();
80
+ }, timeoutMs);
81
+ timeout.unref?.();
82
+
83
+ child.on('close', (code, signal) => {
84
+ if (settled) return;
85
+ settled = true;
86
+ clearTimeout(timeout);
87
+ clearTimeout(killTimer);
88
+ resolve({
89
+ code,
90
+ signal,
91
+ timedOut,
92
+ stdout: stdout.toString('utf8'),
93
+ stderr: stderr.toString('utf8'),
94
+ stdoutTruncated,
95
+ stderrTruncated,
96
+ });
97
+ });
98
+ });
99
+ }
100
+
101
+ async function readGeneratedArtifactsAsync(harmony, allowedRoot) {
102
+ const lexicalAllowed = path.resolve(allowedRoot);
103
+ const lexicalRoot = path.resolve(harmony);
104
+ let root;
105
+ try {
106
+ if (!isPathInside(lexicalAllowed, lexicalRoot)) throw new TypeError('outside staging root');
107
+ const allowed = await fs.promises.realpath(lexicalAllowed);
108
+ root = await fs.promises.realpath(lexicalRoot);
109
+ const stat = await fs.promises.stat(root);
110
+ if (!isPathInside(allowed, root) || !stat.isDirectory()) throw new TypeError('unsafe staging root');
111
+ } catch (cause) {
112
+ throw new HarmonyAutolinkingError(
113
+ 'GENERATED_ARTIFACT_MISSING',
114
+ 'RNOH output root must resolve to a directory inside the staging project.',
115
+ { cause, stage: 'rnoh-validate' }
116
+ );
117
+ }
118
+ const artifacts = {};
119
+
120
+ for (const [name, relative] of Object.entries(RnohArtifacts)) {
121
+ const target = path.join(root, relative);
122
+ let stat;
123
+ let realTarget;
124
+ let artifact;
125
+ try {
126
+ if (!isPathInside(root, target)) throw new TypeError('outside staging root');
127
+ stat = await fs.promises.lstat(target);
128
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size === 0) {
129
+ throw new TypeError('not a non-empty regular file');
130
+ }
131
+ realTarget = await fs.promises.realpath(target);
132
+ if (!isPathInside(root, realTarget)) throw new TypeError('outside staging root');
133
+ artifact = name === 'cppFactory'
134
+ ? { path: realTarget }
135
+ : { path: realTarget, content: await fs.promises.readFile(realTarget, 'utf8') };
136
+ } catch (cause) {
137
+ throw new HarmonyAutolinkingError('GENERATED_ARTIFACT_MISSING', `RNOH did not generate a non-empty regular ${relative}.`, {
138
+ cause,
139
+ stage: 'rnoh-validate',
140
+ details: { artifact: relative },
141
+ });
142
+ }
143
+
144
+ artifacts[name] = artifact;
145
+ }
146
+
147
+ return artifacts;
148
+ }
149
+
150
+ function buildLinkCommandArgs(linkOptions) {
151
+ assertSafeRnohPackageList(linkOptions.include || [], 'include', 'rnoh-preflight');
152
+ assertSafeRnohPackageList(linkOptions.exclude || [], 'exclude', 'rnoh-preflight');
153
+
154
+ if (linkOptions.include?.length && linkOptions.exclude?.length) {
155
+ throw new HarmonyAutolinkingError(
156
+ 'INVALID_OPTIONS',
157
+ 'include and exclude cannot both be set for RNOH link-harmony.',
158
+ { stage: 'rnoh-preflight' }
159
+ );
160
+ }
161
+
162
+ const argv = [
163
+ 'link-harmony',
164
+ '--harmony-project-path', linkOptions.harmonyProjectPath,
165
+ '--node-modules-path', linkOptions.nodeModulesPath,
166
+ '--cmake-autolink-path-relative-to-harmony', RnohArtifacts.cmake,
167
+ '--cpp-rnoh-packages-factory-path-relative-to-harmony', RnohArtifacts.cppFactory,
168
+ '--ets-rnoh-packages-factory-path-relative-to-harmony', RnohArtifacts.etsFactory,
169
+ '--oh-package-path-relative-to-harmony', RnohArtifacts.ohPackage,
170
+ ];
171
+
172
+ if (linkOptions.include?.length) argv.push('--include-npm-packages', linkOptions.include.join(';'));
173
+ if (linkOptions.exclude?.length) argv.push('--exclude-npm-packages', linkOptions.exclude.join(';'));
174
+ return argv;
175
+ }
176
+
177
+ function describeProcessFailure(result, roots) {
178
+ const reason = result.timedOut
179
+ ? 'timed out'
180
+ : result.signal
181
+ ? `was terminated by ${result.signal}`
182
+ : `exited with code ${result.code}`;
183
+
184
+ const stderr = sanitizeOutput(result.stderr, roots);
185
+ return `React Native link-harmony ${reason}${stderr ? `: ${stderr}` : '.'}`;
186
+ }
187
+
188
+ function patchEtsFactorySource(source, descriptors) {
189
+ // RNOH 0.84.1 emits this exact zero-argument factory shape. Convert only that
190
+ // current toolchain output so any upstream change is visible in CI.
191
+ const generated = 'import type { RNPackageContext, RNOHPackage } from \'@rnoh/react-native-openharmony\';';
192
+ const packageTypes = 'import type { RNPackage, RNPackageContext } from \'@rnoh/react-native-openharmony\';';
193
+ const generatedHostProvider = 'import { expoHarmonyHostProvider } from \'./generated/ExpoHarmonyHostProvider\';';
194
+ const coreRootImport = 'import ExpoModulesCorePackage from \'@expo-harmony/expo-modules-core\';';
195
+ const coreAutolinkingImport = 'import ExpoModulesCorePackage from \'@expo-harmony/expo-modules-core/Autolinking\';';
196
+ const replacements = [
197
+ [generated, `${packageTypes}\n${generatedHostProvider}`, 'RNOH package type import'],
198
+ [coreRootImport, coreAutolinkingImport, 'Expo Modules Core package import'],
199
+ ['): RNOHPackage[] {', '): RNPackage[] {', 'RNOH package factory return type'],
200
+ [
201
+ 'new ExpoModulesCorePackage(ctx)',
202
+ 'new ExpoModulesCorePackage(ctx, expoHarmonyHostProvider.expoModules, expoHarmonyHostProvider.hostState)',
203
+ 'Expo Modules Core zero-argument registration',
204
+ ],
205
+ ];
206
+ const namedImports = new Map();
207
+ for (const descriptor of descriptors.filter(item => item.rnoh.harPaths.length > 0)) {
208
+ const rnohMetadata = resolveRnohMetadata(descriptor);
209
+ if (rnohMetadata.etsPackageImport !== 'named') continue;
210
+
211
+ const name = rnohMetadata.harMappings[0].ohPackageName;
212
+ const defaultImport = `import ${rnohMetadata.etsPackageClassName} from '${name}';`;
213
+ const namedImport = `import { ${rnohMetadata.etsPackageClassName} } from '${name}';`;
214
+ namedImports.set(defaultImport, namedImport);
215
+ }
216
+ for (const [defaultImport, namedImport] of namedImports) {
217
+ replacements.push([defaultImport, namedImport, 'RNOH named package import']);
218
+ }
219
+
220
+ let content = source;
221
+ for (const [generatedFragment, replacement, name] of replacements) {
222
+ const fragments = content.split(generatedFragment);
223
+ if (fragments.length !== 2) {
224
+ throw new HarmonyAutolinkingError(
225
+ 'GENERATED_ARTIFACT_SET_MISMATCH',
226
+ `RNOH 0.84.1 generated factory did not contain exactly one expected ${name}.`,
227
+ { stage: 'rnoh-patch', details: { fragment: name } }
228
+ );
229
+ }
230
+ content = `${fragments[0]}${replacement}${fragments[1]}`;
231
+ }
232
+
233
+ return content;
234
+ }
235
+
236
+ async function patchEtsFactoryAsync(artifact, descriptors) {
237
+ const content = patchEtsFactorySource(artifact.content, descriptors);
238
+
239
+ if (content !== artifact.content) {
240
+ await fs.promises.writeFile(artifact.path, content);
241
+ artifact.content = content;
242
+ }
243
+ }
244
+
245
+ function patchCmakeSource(source, descriptors) {
246
+ let content = source;
247
+ const guardedBlocks = new Map();
248
+
249
+ for (const descriptor of descriptors.filter(item => item.rnoh.harPaths.length > 0)) {
250
+ const metadata = resolveRnohMetadata(descriptor);
251
+ const generated = ` add_subdirectory("\${OH_MODULES_DIR}/${descriptor.packageName}/src/main/cpp" ./${metadata.cmakeLibraryTargetName})`;
252
+ const guarded = [
253
+ ` if(NOT TARGET ${metadata.cmakeLibraryTargetName})`,
254
+ ` add_subdirectory("\${OH_MODULES_DIR}/${descriptor.packageName}/src/main/cpp" ./${metadata.cmakeLibraryTargetName})`,
255
+ ' endif()',
256
+ ].join('\n');
257
+ guardedBlocks.set(metadata.cmakeLibraryTargetName, guarded);
258
+ const fragments = content.split(generated);
259
+ if (fragments.length !== 2) {
260
+ throw new HarmonyAutolinkingError(
261
+ 'GENERATED_ARTIFACT_SET_MISMATCH',
262
+ `RNOH 0.84.1 generated CMake did not contain exactly one add_subdirectory for ${descriptor.packageName}.`,
263
+ { stage: 'rnoh-patch', packageName: descriptor.packageName }
264
+ );
265
+ }
266
+ content = `${fragments[0]}${guarded}${fragments[1]}`;
267
+ }
268
+
269
+ // Core consumes the dependency's public CMake target. Configure Worklets
270
+ // first instead of making Core compile a private copy of dependency sources.
271
+ const coreBlock = guardedBlocks.get('expo_harmony__expo_modules_core');
272
+ const workletsBlock = guardedBlocks.get('rnoh_worklets');
273
+ if (coreBlock && workletsBlock) {
274
+ const coreIndex = content.indexOf(coreBlock);
275
+ const workletsIndex = content.indexOf(workletsBlock);
276
+ if (coreIndex >= 0 && workletsIndex > coreIndex) {
277
+ content = content.replace(`${workletsBlock}\n`, '');
278
+ const updatedCoreIndex = content.indexOf(coreBlock);
279
+ content = `${content.slice(0, updatedCoreIndex)}${workletsBlock}\n${content.slice(updatedCoreIndex)}`;
280
+ }
281
+
282
+ // The Harmony Worklets package cannot infer the Hermes ABI selected by the
283
+ // application. Forward the real build setting after its public target has
284
+ // been configured; Core must not hard-code or override dependency sources.
285
+ const configuredWorkletsBlock = [
286
+ workletsBlock,
287
+ // Worklets 1.0.0 declares implementation files as PUBLIC sources. Do not
288
+ // make every linked consumer compile the dependency implementation again.
289
+ ' set_property(TARGET rnoh_worklets PROPERTY INTERFACE_SOURCES "")',
290
+ ' set(EXPO_RNOH_PACKAGE_JSON_PATH "${OH_MODULES_DIR}/@rnoh/react-native-openharmony/oh-package.json5")',
291
+ ' if(NOT EXISTS "${EXPO_RNOH_PACKAGE_JSON_PATH}")',
292
+ ' message(FATAL_ERROR "Unable to locate the RNOH package manifest")',
293
+ ' endif()',
294
+ ' file(READ "${EXPO_RNOH_PACKAGE_JSON_PATH}" EXPO_RNOH_PACKAGE_JSON)',
295
+ ' string(JSON EXPO_RNOH_VERSION GET "${EXPO_RNOH_PACKAGE_JSON}" version)',
296
+ ' if(NOT EXPO_RNOH_VERSION MATCHES "^0\\\\.([0-9]+)\\\\.")',
297
+ ' message(FATAL_ERROR "Unable to derive the React Native minor from RNOH ${EXPO_RNOH_VERSION}")',
298
+ ' endif()',
299
+ ' set(EXPO_RNOH_MINOR_VERSION "${CMAKE_MATCH_1}")',
300
+ ' target_compile_definitions(rnoh_worklets PUBLIC REACT_NATIVE_MINOR_VERSION=${EXPO_RNOH_MINOR_VERSION})',
301
+ ' if(DEFINED ENV{HERMES_V1_ENABLED} AND "$ENV{HERMES_V1_ENABLED}" STREQUAL "true")',
302
+ ' target_compile_definitions(rnoh_worklets PRIVATE HERMES_V1_ENABLED=1)',
303
+ ' endif()',
304
+ ' if(EXPO_RNOH_MINOR_VERSION GREATER_EQUAL 84)',
305
+ ' set(EXPO_WORKLETS_MODULE_SOURCE "${OH_MODULES_DIR}/@react-native-ohos/react-native-worklets/src/main/cpp/WorkletsModule.cpp")',
306
+ ' file(READ "${EXPO_WORKLETS_MODULE_SOURCE}" EXPO_WORKLETS_MODULE_CONTENT)',
307
+ ' set(EXPO_WORKLETS_OLD_FRAGMENT "std::shared_ptr<const BigStringBuffer> script = nullptr;")',
308
+ ' string(FIND "${EXPO_WORKLETS_MODULE_CONTENT}" "${EXPO_WORKLETS_OLD_FRAGMENT}" EXPO_WORKLETS_FRAGMENT_OFFSET)',
309
+ ' if(EXPO_WORKLETS_FRAGMENT_OFFSET EQUAL -1)',
310
+ ' message(FATAL_ERROR "Worklets Harmony wrapper no longer matches the guarded RN 0.84 compatibility patch")',
311
+ ' endif()',
312
+ ' string(REPLACE "${EXPO_WORKLETS_OLD_FRAGMENT}" "std::shared_ptr<const JSBigStringBuffer> script = nullptr;" EXPO_WORKLETS_MODULE_CONTENT "${EXPO_WORKLETS_MODULE_CONTENT}")',
313
+ ' set(EXPO_WORKLETS_OLD_FRAGMENT "auto jsBigString = std::make_unique<JSBigBufferString>(len);")',
314
+ ' string(FIND "${EXPO_WORKLETS_MODULE_CONTENT}" "${EXPO_WORKLETS_OLD_FRAGMENT}" EXPO_WORKLETS_FRAGMENT_OFFSET)',
315
+ ' if(EXPO_WORKLETS_FRAGMENT_OFFSET EQUAL -1)',
316
+ ' message(FATAL_ERROR "Worklets Harmony bundle allocation no longer matches the guarded RN 0.84 compatibility patch")',
317
+ ' endif()',
318
+ ' string(REPLACE "${EXPO_WORKLETS_OLD_FRAGMENT}" "auto jsBigString = std::make_shared<JSBigBufferString>(len);" EXPO_WORKLETS_MODULE_CONTENT "${EXPO_WORKLETS_MODULE_CONTENT}")',
319
+ ' set(EXPO_WORKLETS_OLD_FRAGMENT "char *buffer = jsBigString->data();")',
320
+ ' string(FIND "${EXPO_WORKLETS_MODULE_CONTENT}" "${EXPO_WORKLETS_OLD_FRAGMENT}" EXPO_WORKLETS_FRAGMENT_OFFSET)',
321
+ ' if(EXPO_WORKLETS_FRAGMENT_OFFSET EQUAL -1)',
322
+ ' message(FATAL_ERROR "Worklets Harmony bundle buffer access no longer matches the guarded RN 0.84 compatibility patch")',
323
+ ' endif()',
324
+ ' string(REPLACE "${EXPO_WORKLETS_OLD_FRAGMENT}" "char *buffer = jsBigString->mutableData();" EXPO_WORKLETS_MODULE_CONTENT "${EXPO_WORKLETS_MODULE_CONTENT}")',
325
+ ' set(EXPO_WORKLETS_OLD_FRAGMENT "script = std::make_shared<BigStringBuffer>(std::move(jsBigString));")',
326
+ ' string(FIND "${EXPO_WORKLETS_MODULE_CONTENT}" "${EXPO_WORKLETS_OLD_FRAGMENT}" EXPO_WORKLETS_FRAGMENT_OFFSET)',
327
+ ' if(EXPO_WORKLETS_FRAGMENT_OFFSET EQUAL -1)',
328
+ ' message(FATAL_ERROR "Worklets Harmony bundle assignment no longer matches the guarded RN 0.84 compatibility patch")',
329
+ ' endif()',
330
+ ' string(REPLACE "${EXPO_WORKLETS_OLD_FRAGMENT}" "script = std::move(jsBigString);" EXPO_WORKLETS_MODULE_CONTENT "${EXPO_WORKLETS_MODULE_CONTENT}")',
331
+ ' set(EXPO_WORKLETS_PATCH_DIR "${CMAKE_CURRENT_BINARY_DIR}/expo_worklets_compat")',
332
+ ' file(MAKE_DIRECTORY "${EXPO_WORKLETS_PATCH_DIR}")',
333
+ ' set(EXPO_WORKLETS_PATCHED_MODULE "${EXPO_WORKLETS_PATCH_DIR}/WorkletsModule.cpp")',
334
+ ' file(WRITE "${EXPO_WORKLETS_PATCHED_MODULE}" "${EXPO_WORKLETS_MODULE_CONTENT}")',
335
+ ' get_target_property(EXPO_WORKLETS_SOURCES rnoh_worklets SOURCES)',
336
+ ' set(EXPO_WORKLETS_FILTERED_SOURCES)',
337
+ ' foreach(EXPO_WORKLETS_SOURCE IN LISTS EXPO_WORKLETS_SOURCES)',
338
+ ' if(NOT EXPO_WORKLETS_SOURCE MATCHES "(^|/)WorkletsModule\\\\.cpp$")',
339
+ ' list(APPEND EXPO_WORKLETS_FILTERED_SOURCES "${EXPO_WORKLETS_SOURCE}")',
340
+ ' endif()',
341
+ ' endforeach()',
342
+ ' set_property(TARGET rnoh_worklets PROPERTY SOURCES ${EXPO_WORKLETS_FILTERED_SOURCES})',
343
+ ' target_sources(rnoh_worklets PRIVATE "${EXPO_WORKLETS_PATCHED_MODULE}")',
344
+ ' endif()',
345
+ ].join('\n');
346
+ content = content.replace(workletsBlock, configuredWorkletsBlock);
347
+ }
348
+
349
+ return content;
350
+ }
351
+
352
+ async function patchCmakeAsync(artifact, descriptors) {
353
+ const content = patchCmakeSource(artifact.content, descriptors);
354
+ if (content !== artifact.content) {
355
+ await fs.promises.writeFile(artifact.path, content);
356
+ artifact.content = content;
357
+ }
358
+ }
359
+
360
+ function patchOhpmManifest(artifact, descriptors, buildType, harmonyProjectPath) {
361
+ artifact.content = canonicalizeOhpmManifest(
362
+ artifact.content,
363
+ createManifest(descriptors, { buildType }),
364
+ {
365
+ errorCode: 'RNOH_LINK_FAILED',
366
+ harmonyProjectPath,
367
+ stage: 'rnoh-patch',
368
+ }
369
+ ).source;
370
+ }
371
+
372
+ async function linkRnohAsync(linkOptions) {
373
+ const executable = await resolveCliAsync({
374
+ ...linkOptions,
375
+ nodeModulesPath: linkOptions.commandNodeModulesPath || linkOptions.nodeModulesPath,
376
+ });
377
+
378
+ const spawn = {
379
+ cwd: linkOptions.stageProjectRoot,
380
+ env: linkOptions.env ? { ...process.env, ...linkOptions.env } : process.env,
381
+ outputLimit: linkOptions.outputLimit,
382
+ timeoutMs: linkOptions.timeoutMs,
383
+ };
384
+
385
+ let result;
386
+ try {
387
+ result = await spawnBoundedAsync(executable, buildLinkCommandArgs({
388
+ harmonyProjectPath: linkOptions.stageHarmonyProjectPath,
389
+ nodeModulesPath: linkOptions.nodeModulesPath,
390
+ include: linkOptions.include,
391
+ exclude: linkOptions.exclude,
392
+ }), spawn);
393
+ } catch (cause) {
394
+ throw new HarmonyAutolinkingError('RNOH_LINK_FAILED', 'Unable to execute React Native link-harmony.', { cause, stage: 'rnoh-link' });
395
+ }
396
+
397
+ if (result.code !== 0 || result.signal || result.timedOut) {
398
+ throw new HarmonyAutolinkingError(
399
+ 'RNOH_LINK_FAILED',
400
+ describeProcessFailure(result, [linkOptions.temporaryRoot, linkOptions.projectRoot]),
401
+ {
402
+ stage: 'rnoh-link',
403
+ details: {
404
+ exitCode: result.code,
405
+ signal: result.signal,
406
+ timedOut: result.timedOut,
407
+ stdoutTruncated: result.stdoutTruncated,
408
+ stderrTruncated: result.stderrTruncated,
409
+ },
410
+ }
411
+ );
412
+ }
413
+
414
+ let artifacts;
415
+ try {
416
+ artifacts = await readGeneratedArtifactsAsync(
417
+ linkOptions.stageHarmonyProjectPath,
418
+ linkOptions.stageProjectRoot
419
+ );
420
+ } catch (error) {
421
+ const roots = [linkOptions.temporaryRoot, linkOptions.stageProjectRoot, linkOptions.projectRoot];
422
+ const details = {
423
+ ...(error.details || {}),
424
+ stdout: sanitizeOutput(result.stdout, roots),
425
+ stderr: sanitizeOutput(result.stderr, roots),
426
+ stdoutTruncated: result.stdoutTruncated,
427
+ stderrTruncated: result.stderrTruncated,
428
+ };
429
+ throw new HarmonyAutolinkingError(
430
+ typeof error.code === 'string' ? error.code : 'RNOH_LINK_FAILED',
431
+ error.message || 'RNOH did not generate the required artifacts.',
432
+ {
433
+ cause: error,
434
+ stage: error.stage || 'rnoh-validate',
435
+ packageName: error.packageName,
436
+ diagnostics: error.diagnostics,
437
+ details,
438
+ }
439
+ );
440
+ }
441
+
442
+ if (linkOptions.modules) {
443
+ try {
444
+ await patchCmakeAsync(artifacts.cmake, linkOptions.modules);
445
+ await patchEtsFactoryAsync(artifacts.etsFactory, linkOptions.modules);
446
+ patchOhpmManifest(
447
+ artifacts.ohPackage,
448
+ linkOptions.modules,
449
+ linkOptions.buildType,
450
+ linkOptions.harmonyProjectPath
451
+ );
452
+ await fs.promises.writeFile(artifacts.ohPackage.path, artifacts.ohPackage.content);
453
+ } catch (error) {
454
+ const roots = [linkOptions.temporaryRoot, linkOptions.stageProjectRoot, linkOptions.projectRoot];
455
+ const details = {
456
+ ...(error.details || {}),
457
+ stdout: sanitizeOutput(result.stdout, roots),
458
+ stderr: sanitizeOutput(result.stderr, roots),
459
+ stdoutTruncated: result.stdoutTruncated,
460
+ stderrTruncated: result.stderrTruncated,
461
+ };
462
+ throw new HarmonyAutolinkingError(
463
+ typeof error.code === 'string' ? error.code : 'RNOH_LINK_FAILED',
464
+ error.message || 'Unable to patch RNOH artifacts.',
465
+ {
466
+ cause: error,
467
+ stage: error.stage || 'rnoh-validate',
468
+ packageName: error.packageName,
469
+ diagnostics: error.diagnostics,
470
+ details,
471
+ }
472
+ );
473
+ }
474
+ }
475
+
476
+ return artifacts;
477
+ }
478
+
479
+ export {
480
+ linkRnohAsync,
481
+ };