@expo-harmony/expo-modules-autolinking 55.0.25-harmony.4 → 55.0.25-harmony.5

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.
@@ -7,7 +7,7 @@ export interface HarmonyNativeModule {
7
7
  artifacts: string[];
8
8
  }
9
9
  export interface HarmonyRuntimeContract {
10
- schemaVersion: 1;
10
+ schemaVersion: 1 | 2;
11
11
  platform: 'harmony';
12
12
  runtimeVersion: string;
13
13
  development: boolean;
@@ -22,8 +22,9 @@ export interface HarmonyRuntimeContract {
22
22
  modules: HarmonyNativeModule[];
23
23
  config: {
24
24
  nativeCompiler: string;
25
- targetApiVersion: number;
26
- compatibleApiVersion: number;
25
+ targetApiVersion: number | string;
26
+ compatibleApiVersion: number | string;
27
+ nativeLibFilterHash?: string;
27
28
  permissions: string[];
28
29
  querySchemes: string[];
29
30
  backgroundModes: string[];
@@ -40,6 +41,7 @@ export declare class HarmonyCompatibilityError extends Error {
40
41
  readonly code = "ERR_HARMONY_INCOMPATIBLE";
41
42
  constructor(issues: HarmonyCompatibilityIssue[]);
42
43
  }
44
+ export declare function harmonyRuntimeSchemaVersion(config: HarmonyRuntimeContract['config']): 1 | 2;
43
45
  export declare function validateHarmonyRuntime(value: unknown): HarmonyCompatibilityIssue[];
44
46
  export declare function checkHarmonyCompatibility(client: unknown, requirements: unknown): HarmonyCompatibilityIssue[];
45
47
  export declare function assertHarmonyCompatibility(client: unknown, requirements: unknown): asserts requirements is HarmonyRuntimeContract;
package/build/runtime.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HarmonyCompatibilityError = void 0;
4
+ exports.harmonyRuntimeSchemaVersion = harmonyRuntimeSchemaVersion;
4
5
  exports.validateHarmonyRuntime = validateHarmonyRuntime;
5
6
  exports.checkHarmonyCompatibility = checkHarmonyCompatibility;
6
7
  exports.assertHarmonyCompatibility = assertHarmonyCompatibility;
@@ -27,11 +28,37 @@ function strings(value) {
27
28
  }
28
29
  const Versions = ['runtimeVersion', 'expo', 'expoModulesCore', 'rnoh', 'react', 'hermes'];
29
30
  const Settings = ['permissions', 'querySchemes', 'backgroundModes', 'abiFilters'];
31
+ function apiVersion(value, schema) {
32
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 13 && value < 26)
33
+ return [value, 0, 0];
34
+ if (schema === 2 && typeof value === 'string' && /^(?:[1-9]\d{1,2})\.(?:0|[1-9]\d{0,2})\.(?:0|[1-9]\d{0,2})$/.test(value)) {
35
+ const parts = value.split('.').map(Number);
36
+ if (parts[0] >= 26)
37
+ return parts;
38
+ }
39
+ return undefined;
40
+ }
41
+ function compareApiVersions(left, right) {
42
+ for (let index = 0; index < 3; index++) {
43
+ if (left[index] !== right[index])
44
+ return left[index] - right[index];
45
+ }
46
+ return 0;
47
+ }
48
+ function harmonyRuntimeSchemaVersion(config) {
49
+ return typeof config.targetApiVersion === 'string' || typeof config.compatibleApiVersion === 'string'
50
+ || config.nativeLibFilterHash !== undefined
51
+ ? 2
52
+ : 1;
53
+ }
30
54
  function validateHarmonyRuntime(value) {
31
55
  if (!record(value))
32
56
  return [{ code: 'INVALID_CONTRACT', field: '', message: 'Runtime contract must be an object.' }];
33
57
  const issues = [];
34
- for (const [field, expected] of Object.entries({ schemaVersion: 1, platform: 'harmony', engine: 'hermes-v1', bundleFormat: 'js-source', renderer: 'rnoh' })) {
58
+ if (value.schemaVersion !== 1 && value.schemaVersion !== 2) {
59
+ issues.push({ code: 'INVALID_CONTRACT', field: 'schemaVersion', message: 'schemaVersion must be 1 or 2.' });
60
+ }
61
+ for (const [field, expected] of Object.entries({ platform: 'harmony', engine: 'hermes-v1', bundleFormat: 'js-source', renderer: 'rnoh' })) {
35
62
  if (value[field] !== expected) {
36
63
  issues.push({ code: field === 'platform' ? 'PLATFORM_MISMATCH' : 'INVALID_CONTRACT', field, message: `${field} must be ${expected}.` });
37
64
  }
@@ -62,10 +89,12 @@ function validateHarmonyRuntime(value) {
62
89
  }
63
90
  }
64
91
  const config = value.config;
92
+ const target = record(config) ? apiVersion(config.targetApiVersion, value.schemaVersion) : undefined;
93
+ const compatible = record(config) ? apiVersion(config.compatibleApiVersion, value.schemaVersion) : undefined;
65
94
  if (!record(config) || !token(config.nativeCompiler) || !Settings.every(field => strings(config[field]))
66
- || !Number.isInteger(config.targetApiVersion) || Number(config.targetApiVersion) < 13
67
- || !Number.isInteger(config.compatibleApiVersion) || Number(config.compatibleApiVersion) < 13
68
- || Number(config.compatibleApiVersion) > Number(config.targetApiVersion)
95
+ || !target || !compatible || compareApiVersions(compatible, target) > 0
96
+ || (config.nativeLibFilterHash !== undefined && (value.schemaVersion !== 2
97
+ || typeof config.nativeLibFilterHash !== 'string' || !/^[a-f0-9]{64}$/.test(config.nativeLibFilterHash)))
69
98
  || (Array.isArray(config.abiFilters) && config.abiFilters.length === 0)) {
70
99
  issues.push({ code: 'INVALID_CONTRACT', field: 'config', message: 'Invalid native build configuration.' });
71
100
  }
@@ -103,7 +132,7 @@ function checkHarmonyCompatibility(client, requirements) {
103
132
  issues.push({ code: 'NATIVE_MODULE_MISMATCH', field: module.packageName, message: `Native contract differs for ${module.packageName}. Rebuild or select a matching client.` });
104
133
  }
105
134
  }
106
- for (const field of ['nativeCompiler', 'targetApiVersion', 'compatibleApiVersion']) {
135
+ for (const field of ['nativeCompiler', 'targetApiVersion', 'compatibleApiVersion', 'nativeLibFilterHash']) {
107
136
  if (available.config[field] !== required.config[field]) {
108
137
  issues.push({ code: 'CONFIG_MISMATCH', field, message: `${field}: project requires ${required.config[field]}, client provides ${available.config[field]}.` });
109
138
  }
@@ -43,6 +43,7 @@ const path = __importStar(require("node:path"));
43
43
  const cross_spawn_1 = __importDefault(require("cross-spawn"));
44
44
  const json5_1 = __importDefault(require("json5"));
45
45
  const dependencies_1 = require("./ohpm/dependencies");
46
+ const mirror_1 = require("./ohpm/mirror");
46
47
  const toolCommand_1 = require("../utilities/toolCommand");
47
48
  const terminateProcess_1 = require("../utilities/terminateProcess");
48
49
  const errors_1 = require("../errors");
@@ -256,22 +257,25 @@ async function materializeLocalSourceAsync(options) {
256
257
  throw new errors_1.HarmonyAutolinkingError('INVALID_OPTIONS', `${module.packageName} is not a nativeModulesDir local-source module.`, { packageName: module.packageName, stage: 'artifact-materialize' });
257
258
  }
258
259
  const artifact = module.artifact;
259
- const projectRoot = await fs.promises.realpath(options.projectRoot);
260
- const packageRoot = await fs.promises.realpath(module.packageRoot);
261
- if (!(0, values_1.isPathInside)(projectRoot, packageRoot)) {
260
+ const project = await fs.promises.realpath(options.projectRoot);
261
+ const root = await fs.promises.realpath(module.packageRoot);
262
+ if (!(0, values_1.isPathInside)(project, root)) {
262
263
  throw new errors_1.HarmonyAutolinkingError('UNSAFE_SOURCE_ARTIFACT', 'Harmony local modules must live inside the application project root.', { packageName: module.packageName, stage: 'artifact-materialize' });
263
264
  }
264
265
  if (!module.arkTs) {
265
266
  throw new errors_1.HarmonyAutolinkingError('INVALID_METADATA', `${module.packageName} local-source descriptor has no derived ArkTS package.`, { packageName: module.packageName, stage: 'artifact-materialize' });
266
267
  }
267
- const harPath = await (0, values_1.resolveInsideAsync)(packageRoot, module.arkTs.harPath, 'Harmony materialized HAR path', { packageName: module.packageName, mustExist: false });
268
+ const har = await (0, values_1.resolveInsideAsync)(root, module.arkTs.harPath, 'Harmony materialized HAR path', { packageName: module.packageName, mustExist: false });
268
269
  const file = path.join(artifact.build.cwd, 'oh-package.json5');
269
270
  const original = await fs.promises.readFile(file, 'utf8');
270
271
  const config = json5_1.default.parse(original);
271
272
  const dependencies = (options.dependencies ?? []).filter(value => value.artifact.kind === 'bundled');
272
- const overrides = Object.fromEntries((0, dependencies_1.collectOhpmDeps)(dependencies).map(({ descriptor, mapping }) => [
273
- mapping.ohPackageName, (0, dependencies_1.resolveOhpmSpecifier)(descriptor, mapping, { harmonyProjectPath: artifact.build.cwd }),
274
- ]));
273
+ const overrides = {};
274
+ const mirror = { ...process.env, ...options.env }.EXPO_HARMONY_CHECK_MIRROR_ROOT;
275
+ for (const { descriptor, mapping } of (0, dependencies_1.collectOhpmDeps)(dependencies)) {
276
+ const link = await (0, mirror_1.mirrorPackageLinkAsync)(descriptor.packageLinkPath, artifact.build.cwd, mirror);
277
+ overrides[mapping.ohPackageName] = (0, dependencies_1.resolveOhpmSpecifier)({ ...descriptor, packageLinkPath: link }, mapping, { harmonyProjectPath: artifact.build.cwd });
278
+ }
275
279
  try {
276
280
  await fs.promises.writeFile(file, JSON.stringify({ ...config, overrides: { ...config.overrides, ...overrides } }, null, 2) + '\n');
277
281
  for (const step of fixedLocalSourceBuildSteps(artifact.build)) {
@@ -283,19 +287,19 @@ async function materializeLocalSourceAsync(options) {
283
287
  }
284
288
  const ready = await isNonEmptyRegularHarAsync(artifact.outputPath, artifact.build.cwd, module.packageName);
285
289
  if (!ready) {
286
- throw new errors_1.HarmonyAutolinkingError('SOURCE_BUILD_FAILED', `Hvigor did not produce a non-empty HAR at ${(0, values_1.normalizeSlashes)(path.relative(packageRoot, artifact.outputPath))}.`, { packageName: module.packageName, stage: 'artifact-materialize' });
290
+ throw new errors_1.HarmonyAutolinkingError('SOURCE_BUILD_FAILED', `Hvigor did not produce a non-empty HAR at ${(0, values_1.normalizeSlashes)(path.relative(root, artifact.outputPath))}.`, { packageName: module.packageName, stage: 'artifact-materialize' });
287
291
  }
288
292
  await (0, publish_1.publishArtifactsAsync)({
289
- allowedRoot: projectRoot,
290
- lockPath: path.join(projectRoot, '.expo/harmony/module-materialize.lock'),
291
- files: [{ source: artifact.outputPath, target: harPath }],
293
+ allowedRoot: project,
294
+ lockPath: path.join(project, '.expo/harmony/module-materialize.lock'),
295
+ files: [{ source: artifact.outputPath, target: har }],
292
296
  });
293
- if (!await isNonEmptyRegularHarAsync(harPath, packageRoot, module.packageName)) {
297
+ if (!await isNonEmptyRegularHarAsync(har, root, module.packageName)) {
294
298
  throw new errors_1.HarmonyAutolinkingError('SOURCE_BUILD_FAILED', 'Harmony HAR materialization produced a missing or empty file.', { packageName: module.packageName, stage: 'artifact-materialize' });
295
299
  }
296
300
  return {
297
301
  packageName: module.packageName,
298
- harPath,
302
+ harPath: har,
299
303
  };
300
304
  }
301
305
  async function materializeLocalSourcesAsync(modules, options) {
@@ -0,0 +1 @@
1
+ export declare function mirrorPackageLinkAsync(link: string, project: string, root?: string): Promise<string>;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.mirrorPackageLinkAsync = mirrorPackageLinkAsync;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const errors_1 = require("../../errors");
10
+ const values_1 = require("../../utilities/values");
11
+ async function mirrorPackageLinkAsync(link, project, root) {
12
+ if (!root)
13
+ return link;
14
+ const mirror = await promises_1.default.realpath(root);
15
+ const roots = [node_path_1.default.resolve(root, 'filesystem'), node_path_1.default.join(mirror, 'filesystem')];
16
+ if (!roots.some(root => (0, values_1.isPathInside)(root, project))
17
+ || roots.some(root => (0, values_1.isPathInside)(root, link)))
18
+ return link;
19
+ const source = node_path_1.default.resolve(link);
20
+ const parsed = node_path_1.default.parse(source);
21
+ const volume = parsed.root.replace(/[^A-Za-z0-9]+/gu, '') || 'root';
22
+ const target = node_path_1.default.join(mirror, 'filesystem', volume, source.slice(parsed.root.length));
23
+ let existing;
24
+ try {
25
+ existing = await promises_1.default.realpath(target);
26
+ }
27
+ catch (cause) {
28
+ if (cause.code !== 'ENOENT') {
29
+ throw new errors_1.HarmonyAutolinkingError('MIRROR_PATH_FAILED', `Cannot inspect the isolated dependency path: ${target}`, { cause, stage: 'artifact-materialize' });
30
+ }
31
+ }
32
+ if (existing !== undefined) {
33
+ if (existing !== await promises_1.default.realpath(source)) {
34
+ throw new errors_1.HarmonyAutolinkingError('UNSAFE_MIRROR_PATH', `The isolated dependency path conflicts with an existing file or link: ${target}`, { stage: 'artifact-materialize' });
35
+ }
36
+ return target;
37
+ }
38
+ // Do not create directories through a copied symlink into the original project.
39
+ let directory = mirror;
40
+ for (const segment of node_path_1.default.relative(mirror, node_path_1.default.dirname(target)).split(node_path_1.default.sep)) {
41
+ directory = node_path_1.default.join(directory, segment);
42
+ try {
43
+ await promises_1.default.mkdir(directory);
44
+ }
45
+ catch (cause) {
46
+ if (cause.code !== 'EEXIST') {
47
+ throw new errors_1.HarmonyAutolinkingError('MIRROR_PATH_FAILED', `Cannot create the isolated dependency directory: ${directory}`, { cause, stage: 'artifact-materialize' });
48
+ }
49
+ if (!(await promises_1.default.lstat(directory)).isDirectory()) {
50
+ throw new errors_1.HarmonyAutolinkingError('UNSAFE_MIRROR_PATH', `The isolated dependency path conflicts with an existing file or link: ${directory}`, { cause, stage: 'artifact-materialize' });
51
+ }
52
+ }
53
+ }
54
+ await promises_1.default.symlink(source, target, process.platform === 'win32' ? 'junction' : 'dir');
55
+ return target;
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo-harmony/expo-modules-autolinking",
3
- "version": "55.0.25-harmony.4",
3
+ "version": "55.0.25-harmony.5",
4
4
  "description": "HarmonyOS port of expo-modules-autolinking",
5
5
  "keywords": [
6
6
  "react-native",
package/runtime.ts CHANGED
@@ -8,7 +8,7 @@ export interface HarmonyNativeModule {
8
8
  }
9
9
 
10
10
  export interface HarmonyRuntimeContract {
11
- schemaVersion: 1;
11
+ schemaVersion: 1 | 2;
12
12
  platform: 'harmony';
13
13
  runtimeVersion: string;
14
14
  development: boolean;
@@ -23,8 +23,9 @@ export interface HarmonyRuntimeContract {
23
23
  modules: HarmonyNativeModule[];
24
24
  config: {
25
25
  nativeCompiler: string;
26
- targetApiVersion: number;
27
- compatibleApiVersion: number;
26
+ targetApiVersion: number | string;
27
+ compatibleApiVersion: number | string;
28
+ nativeLibFilterHash?: string;
28
29
  permissions: string[];
29
30
  querySchemes: string[];
30
31
  backgroundModes: string[];
@@ -64,11 +65,40 @@ function strings(value: unknown): value is string[] {
64
65
  const Versions = ['runtimeVersion', 'expo', 'expoModulesCore', 'rnoh', 'react', 'hermes'] as const;
65
66
  const Settings = ['permissions', 'querySchemes', 'backgroundModes', 'abiFilters'] as const;
66
67
 
68
+ function apiVersion(value: unknown, schema: unknown): number[] | undefined {
69
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 13 && value < 26) return [value, 0, 0];
70
+ if (schema === 2 && typeof value === 'string' && /^(?:[1-9]\d{1,2})\.(?:0|[1-9]\d{0,2})\.(?:0|[1-9]\d{0,2})$/.test(value)) {
71
+ const parts = value.split('.').map(Number);
72
+ if (parts[0] >= 26) return parts;
73
+ }
74
+
75
+ return undefined;
76
+ }
77
+
78
+ function compareApiVersions(left: number[], right: number[]): number {
79
+ for (let index = 0; index < 3; index++) {
80
+ if (left[index] !== right[index]) return left[index] - right[index];
81
+ }
82
+
83
+ return 0;
84
+ }
85
+
86
+ export function harmonyRuntimeSchemaVersion(config: HarmonyRuntimeContract['config']): 1 | 2 {
87
+ return typeof config.targetApiVersion === 'string' || typeof config.compatibleApiVersion === 'string'
88
+ || config.nativeLibFilterHash !== undefined
89
+ ? 2
90
+ : 1;
91
+ }
92
+
67
93
  export function validateHarmonyRuntime(value: unknown): HarmonyCompatibilityIssue[] {
68
94
  if (!record(value)) return [{ code: 'INVALID_CONTRACT', field: '', message: 'Runtime contract must be an object.' }];
69
95
 
70
96
  const issues: HarmonyCompatibilityIssue[] = [];
71
- for (const [field, expected] of Object.entries({ schemaVersion: 1, platform: 'harmony', engine: 'hermes-v1', bundleFormat: 'js-source', renderer: 'rnoh' })) {
97
+ if (value.schemaVersion !== 1 && value.schemaVersion !== 2) {
98
+ issues.push({ code: 'INVALID_CONTRACT', field: 'schemaVersion', message: 'schemaVersion must be 1 or 2.' });
99
+ }
100
+
101
+ for (const [field, expected] of Object.entries({ platform: 'harmony', engine: 'hermes-v1', bundleFormat: 'js-source', renderer: 'rnoh' })) {
72
102
  if (value[field] !== expected) {
73
103
  issues.push({ code: field === 'platform' ? 'PLATFORM_MISMATCH' : 'INVALID_CONTRACT', field, message: `${field} must be ${expected}.` });
74
104
  }
@@ -101,10 +131,13 @@ export function validateHarmonyRuntime(value: unknown): HarmonyCompatibilityIssu
101
131
  }
102
132
 
103
133
  const config = value.config;
134
+ const target = record(config) ? apiVersion(config.targetApiVersion, value.schemaVersion) : undefined;
135
+ const compatible = record(config) ? apiVersion(config.compatibleApiVersion, value.schemaVersion) : undefined;
136
+
104
137
  if (!record(config) || !token(config.nativeCompiler) || !Settings.every(field => strings(config[field]))
105
- || !Number.isInteger(config.targetApiVersion) || Number(config.targetApiVersion) < 13
106
- || !Number.isInteger(config.compatibleApiVersion) || Number(config.compatibleApiVersion) < 13
107
- || Number(config.compatibleApiVersion) > Number(config.targetApiVersion)
138
+ || !target || !compatible || compareApiVersions(compatible, target) > 0
139
+ || (config.nativeLibFilterHash !== undefined && (value.schemaVersion !== 2
140
+ || typeof config.nativeLibFilterHash !== 'string' || !/^[a-f0-9]{64}$/.test(config.nativeLibFilterHash)))
108
141
  || (Array.isArray(config.abiFilters) && config.abiFilters.length === 0)) {
109
142
  issues.push({ code: 'INVALID_CONTRACT', field: 'config', message: 'Invalid native build configuration.' });
110
143
  }
@@ -150,7 +183,7 @@ export function checkHarmonyCompatibility(client: unknown, requirements: unknown
150
183
  }
151
184
  }
152
185
 
153
- for (const field of ['nativeCompiler', 'targetApiVersion', 'compatibleApiVersion'] as const) {
186
+ for (const field of ['nativeCompiler', 'targetApiVersion', 'compatibleApiVersion', 'nativeLibFilterHash'] as const) {
154
187
  if (available.config[field] !== required.config[field]) {
155
188
  issues.push({ code: 'CONFIG_MISMATCH', field, message: `${field}: project requires ${required.config[field]}, client provides ${available.config[field]}.` });
156
189
  }
@@ -3,6 +3,7 @@ import * as path from 'node:path';
3
3
  import spawn from 'cross-spawn';
4
4
  import JSON5 from 'json5';
5
5
  import { collectOhpmDeps, resolveOhpmSpecifier } from './ohpm/dependencies';
6
+ import { mirrorPackageLinkAsync } from './ohpm/mirror';
6
7
  import { resolveHarmonyCommand } from '../utilities/toolCommand';
7
8
  import { terminateProcess } from '../utilities/terminateProcess';
8
9
 
@@ -314,6 +315,7 @@ async function materializeLocalSourceAsync(
314
315
  options: MaterializeLocalSourceOptions
315
316
  ): Promise<MaterializedLocalSource> {
316
317
  const module = options.module;
318
+
317
319
  if (module.source !== 'nativeModulesDir' || module.artifact.kind !== 'local-source') {
318
320
  throw new HarmonyAutolinkingError(
319
321
  'INVALID_OPTIONS',
@@ -323,9 +325,10 @@ async function materializeLocalSourceAsync(
323
325
  }
324
326
 
325
327
  const artifact = module.artifact;
326
- const projectRoot = await fs.promises.realpath(options.projectRoot);
327
- const packageRoot = await fs.promises.realpath(module.packageRoot);
328
- if (!isPathInside(projectRoot, packageRoot)) {
328
+ const project = await fs.promises.realpath(options.projectRoot);
329
+ const root = await fs.promises.realpath(module.packageRoot);
330
+
331
+ if (!isPathInside(project, root)) {
329
332
  throw new HarmonyAutolinkingError(
330
333
  'UNSAFE_SOURCE_ARTIFACT',
331
334
  'Harmony local modules must live inside the application project root.',
@@ -340,8 +343,8 @@ async function materializeLocalSourceAsync(
340
343
  );
341
344
  }
342
345
 
343
- const harPath = await resolveInsideAsync(
344
- packageRoot,
346
+ const har = await resolveInsideAsync(
347
+ root,
345
348
  module.arkTs.harPath,
346
349
  'Harmony materialized HAR path',
347
350
  { packageName: module.packageName, mustExist: false }
@@ -351,12 +354,20 @@ async function materializeLocalSourceAsync(
351
354
  const original = await fs.promises.readFile(file, 'utf8');
352
355
  const config = JSON5.parse(original);
353
356
  const dependencies = (options.dependencies ?? []).filter(value => value.artifact.kind === 'bundled');
354
- const overrides = Object.fromEntries(collectOhpmDeps(dependencies).map(({ descriptor, mapping }) => [
355
- mapping.ohPackageName, resolveOhpmSpecifier(descriptor, mapping, { harmonyProjectPath: artifact.build.cwd }),
356
- ]));
357
+ const overrides: Record<string, string> = {};
358
+ const mirror = { ...process.env, ...options.env }.EXPO_HARMONY_CHECK_MIRROR_ROOT;
359
+
360
+ for (const { descriptor, mapping } of collectOhpmDeps(dependencies)) {
361
+ const link = await mirrorPackageLinkAsync(descriptor.packageLinkPath, artifact.build.cwd, mirror);
362
+
363
+ overrides[mapping.ohPackageName] = resolveOhpmSpecifier(
364
+ { ...descriptor, packageLinkPath: link }, mapping, { harmonyProjectPath: artifact.build.cwd }
365
+ );
366
+ }
357
367
 
358
368
  try {
359
369
  await fs.promises.writeFile(file, JSON.stringify({ ...config, overrides: { ...config.overrides, ...overrides } }, null, 2) + '\n');
370
+
360
371
  for (const step of fixedLocalSourceBuildSteps(artifact.build)) {
361
372
  await runFixedBuildStepAsync(step, module.packageName, options);
362
373
  }
@@ -369,20 +380,22 @@ async function materializeLocalSourceAsync(
369
380
  artifact.build.cwd,
370
381
  module.packageName
371
382
  );
383
+
372
384
  if (!ready) {
373
385
  throw new HarmonyAutolinkingError(
374
386
  'SOURCE_BUILD_FAILED',
375
- `Hvigor did not produce a non-empty HAR at ${normalizeSlashes(path.relative(packageRoot, artifact.outputPath))}.`,
387
+ `Hvigor did not produce a non-empty HAR at ${normalizeSlashes(path.relative(root, artifact.outputPath))}.`,
376
388
  { packageName: module.packageName, stage: 'artifact-materialize' }
377
389
  );
378
390
  }
379
391
 
380
392
  await publishArtifactsAsync({
381
- allowedRoot: projectRoot,
382
- lockPath: path.join(projectRoot, '.expo/harmony/module-materialize.lock'),
383
- files: [{ source: artifact.outputPath, target: harPath }],
393
+ allowedRoot: project,
394
+ lockPath: path.join(project, '.expo/harmony/module-materialize.lock'),
395
+ files: [{ source: artifact.outputPath, target: har }],
384
396
  });
385
- if (!await isNonEmptyRegularHarAsync(harPath, packageRoot, module.packageName)) {
397
+
398
+ if (!await isNonEmptyRegularHarAsync(har, root, module.packageName)) {
386
399
  throw new HarmonyAutolinkingError(
387
400
  'SOURCE_BUILD_FAILED',
388
401
  'Harmony HAR materialization produced a missing or empty file.',
@@ -392,7 +405,7 @@ async function materializeLocalSourceAsync(
392
405
 
393
406
  return {
394
407
  packageName: module.packageName,
395
- harPath,
408
+ harPath: har,
396
409
  };
397
410
  }
398
411
 
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { HarmonyAutolinkingError } from '../../errors';
5
+ import { isPathInside } from '../../utilities/values';
6
+
7
+ export async function mirrorPackageLinkAsync(
8
+ link: string,
9
+ project: string,
10
+ root?: string
11
+ ): Promise<string> {
12
+ if (!root) return link;
13
+
14
+ const mirror = await fs.realpath(root);
15
+ const roots = [path.resolve(root, 'filesystem'), path.join(mirror, 'filesystem')];
16
+
17
+ if (!roots.some(root => isPathInside(root, project))
18
+ || roots.some(root => isPathInside(root, link))) return link;
19
+
20
+ const source = path.resolve(link);
21
+ const parsed = path.parse(source);
22
+ const volume = parsed.root.replace(/[^A-Za-z0-9]+/gu, '') || 'root';
23
+ const target = path.join(mirror, 'filesystem', volume, source.slice(parsed.root.length));
24
+ let existing: string | undefined;
25
+
26
+ try {
27
+ existing = await fs.realpath(target);
28
+ } catch (cause) {
29
+ if (cause.code !== 'ENOENT') {
30
+ throw new HarmonyAutolinkingError(
31
+ 'MIRROR_PATH_FAILED',
32
+ `Cannot inspect the isolated dependency path: ${target}`,
33
+ { cause, stage: 'artifact-materialize' }
34
+ );
35
+ }
36
+ }
37
+
38
+ if (existing !== undefined) {
39
+ if (existing !== await fs.realpath(source)) {
40
+ throw new HarmonyAutolinkingError(
41
+ 'UNSAFE_MIRROR_PATH',
42
+ `The isolated dependency path conflicts with an existing file or link: ${target}`,
43
+ { stage: 'artifact-materialize' }
44
+ );
45
+ }
46
+
47
+ return target;
48
+ }
49
+
50
+ // Do not create directories through a copied symlink into the original project.
51
+ let directory = mirror;
52
+
53
+ for (const segment of path.relative(mirror, path.dirname(target)).split(path.sep)) {
54
+ directory = path.join(directory, segment);
55
+
56
+ try {
57
+ await fs.mkdir(directory);
58
+ } catch (cause) {
59
+ if (cause.code !== 'EEXIST') {
60
+ throw new HarmonyAutolinkingError(
61
+ 'MIRROR_PATH_FAILED',
62
+ `Cannot create the isolated dependency directory: ${directory}`,
63
+ { cause, stage: 'artifact-materialize' }
64
+ );
65
+ }
66
+
67
+ if (!(await fs.lstat(directory)).isDirectory()) {
68
+ throw new HarmonyAutolinkingError(
69
+ 'UNSAFE_MIRROR_PATH',
70
+ `The isolated dependency path conflicts with an existing file or link: ${directory}`,
71
+ { cause, stage: 'artifact-materialize' }
72
+ );
73
+ }
74
+ }
75
+ }
76
+
77
+ await fs.symlink(source, target, process.platform === 'win32' ? 'junction' : 'dir');
78
+
79
+ return target;
80
+ }