@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.
- package/build/runtime.d.ts +5 -3
- package/build/runtime.js +34 -5
- package/build/src/harmony/materialize.js +17 -13
- package/build/src/harmony/ohpm/mirror.d.ts +1 -0
- package/build/src/harmony/ohpm/mirror.js +56 -0
- package/package.json +1 -1
- package/runtime.ts +41 -8
- package/src/harmony/materialize.ts +27 -14
- package/src/harmony/ohpm/mirror.ts +80 -0
package/build/runtime.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|| !
|
|
67
|
-
||
|
|
68
|
-
|
|
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
|
|
260
|
-
const
|
|
261
|
-
if (!(0, values_1.isPathInside)(
|
|
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
|
|
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 =
|
|
273
|
-
|
|
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(
|
|
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:
|
|
290
|
-
lockPath: path.join(
|
|
291
|
-
files: [{ source: artifact.outputPath, target:
|
|
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(
|
|
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
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
|
-
|
|
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
|
-
|| !
|
|
106
|
-
||
|
|
107
|
-
|
|
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
|
|
327
|
-
const
|
|
328
|
-
|
|
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
|
|
344
|
-
|
|
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 =
|
|
355
|
-
|
|
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(
|
|
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:
|
|
382
|
-
lockPath: path.join(
|
|
383
|
-
files: [{ source: artifact.outputPath, target:
|
|
393
|
+
allowedRoot: project,
|
|
394
|
+
lockPath: path.join(project, '.expo/harmony/module-materialize.lock'),
|
|
395
|
+
files: [{ source: artifact.outputPath, target: har }],
|
|
384
396
|
});
|
|
385
|
-
|
|
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
|
+
}
|