* Copyright (c) 2026 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE-MIT file in the root directory of this source tree.
*/
import { Autolinking, AutolinkingConfig } from './Autolinking';
import { NestedDirectoryJSON } from 'memfs';
import { AbsolutePath, DescriptiveError } from '../core/';
import { MockedLogger, MemFS } from '../io/__fixtures__';
import pathUtils from 'node:path';
function createAutolinking({
fsStructure,
}: {
fsStructure: NestedDirectoryJSON;
}) {
const memFS = new MemFS(fsStructure);
const mockedLogger = new MockedLogger();
const autolinking = new Autolinking(memFS, mockedLogger);
return {
runAutolinking: async (config: Partial<AutolinkingConfig> = {}) => {
const input = await autolinking.prepareInput({
harmonyProjectPath:
config.harmonyProjectPath ?? new AbsolutePath('./harmony'),
nodeModulesPath:
config.nodeModulesPath ?? new AbsolutePath('./node_modules'),
cppRNOHPackagesFactoryPathRelativeToHarmony:
config.cppRNOHPackagesFactoryPathRelativeToHarmony ??
'./entry/src/main/cpp/RNOHPackageFactory.h',
ohPackagePathRelativeToHarmony:
config.ohPackagePathRelativeToHarmony ?? './oh-package.json5',
etsRNOHPackagesFactoryPathRelativeToHarmony:
config.etsRNOHPackagesFactoryPathRelativeToHarmony ??
'./entry/src/main/ets/RNOHPackageFactory.ets',
cmakeAutolinkPathRelativeToHarmony:
config.cmakeAutolinkPathRelativeToHarmony ??
'./entry/src/main/cpp/autolink.cmake',
excludedNpmPackageNames: config.excludedNpmPackageNames ?? new Set(),
includedNpmPackageNames: config.includedNpmPackageNames ?? new Set(),
});
const output = autolinking.evaluate(input);
autolinking.saveAndLogOutput(output);
return {
cmakeAutolinkingPath: output.cmakeAutolinkingPathAndContent[0],
cppRNOHPackagesFactoryPath:
output.cppRNOHPackagesFactoryPathAndContent[0],
etsRNOHPackagesFactoryPath:
output.etsRNOHPackagesFactoryPathAndContent[0],
ohPackagePath: output.ohPackagePathAndContent[0],
logs: mockedLogger.getLogs(),
};
},
memFS,
};
}
const baseFileStructure = {
harmony: {
entry: {
src: {
main: {
ets: {},
cpp: {},
},
},
},
'oh-package.json5': `{
"dependencies": {
}
}`,
},
};
it('should generate correct templates with scoped package default configuration', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'@rnoh': {
'link-scoped': {
harmony: {
'link_scoped.har': '',
},
'package.json': JSON.stringify({
name: '@rnoh/link-scoped',
harmony: {
autolinking: true,
},
}),
},
},
},
},
});
await runAutolinking();
const ohPackageContent = memFS.readTextSync(
new AbsolutePath('./harmony/oh-package.json5')
);
expect(ohPackageContent).toContain('dependencies');
expect(ohPackageContent).toContain('@rnoh/rnoh--link-scoped');
expect(ohPackageContent).toContain(
'file:../node_modules/@rnoh/link-scoped/harmony/link_scoped.har'
);
expect(
memFS.readTextSync(
new AbsolutePath('./harmony/entry/src/main/ets/RNOHPackageFactory.ets')
)
).toBe(
`
/*
* This file was generated by RNOH autolinking.
* DO NOT modify it manually, your changes WILL be overwritten.
*/
import type { RNPackageContext, RNOHPackage } from '@rnoh/react-native-openharmony';
import RnohLinkScopedPackage from '@rnoh/rnoh--link-scoped';
export function createRNOHPackages(ctx: RNPackageContext): RNOHPackage[] {
return [
new RnohLinkScopedPackage(ctx),
];
}
`.trimStart()
);
expect(
memFS.readTextSync(
new AbsolutePath('./harmony/entry/src/main/cpp/RNOHPackageFactory.h')
)
).toBe(
`
/*
* This file was generated by RNOH autolinking.
* DO NOT modify it manually, your changes WILL be overwritten.
*/
// clang-format off
#pragma once
#include "RNOH/Package.h"
#include "RnohLinkScopedPackage.h"
std::vector<rnoh::Package::Shared> createRNOHPackages(const rnoh::Package::Context &ctx) {
return {
std::make_shared<rnoh::RnohLinkScopedPackage>(ctx),
};
}
`.trimStart()
);
expect(
memFS.readTextSync(
new AbsolutePath('./harmony/entry/src/main/cpp/autolink.cmake')
)
).toBe(
`
# This file was generated by RNOH autolinking.
# DO NOT modify it manually, your changes WILL be overwritten.
cmake_minimum_required(VERSION 3.5)
# @actor RNOH_APP
function(autolink_libraries target)
add_subdirectory("\${OH_MODULES_DIR}/@rnoh/rnoh--link-scoped/src/main/cpp" ./rnoh__rnoh__link_scoped)
set(AUTOLINKED_LIBRARIES
rnoh__rnoh__link_scoped
)
foreach(lib \${AUTOLINKED_LIBRARIES})
target_link_libraries(\${target} PUBLIC \${lib})
endforeach()
endfunction()
`.trimStart()
);
});
it('should handle custom autolinking configuration', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'@rnoh': {
'custom-config': {
harmony: {
'custom_config.har': '',
},
'package.json': JSON.stringify({
name: '@rnoh/custom-config',
harmony: {
autolinking: {
ohPackageName: '@rnoh/custom-oh-name',
etsPackageClassName: 'CustomEtsClass',
cppPackageClassName: 'CustomCppClass',
cmakeLibraryTargetName: 'custom_cmake_target',
},
},
}),
},
},
},
},
});
const output = await runAutolinking();
expect(memFS.readTextSync(output.etsRNOHPackagesFactoryPath)).toContain(
'CustomEtsClass'
);
expect(memFS.readTextSync(output.etsRNOHPackagesFactoryPath)).not.toContain(
'CustomCppClass'
);
expect(memFS.readTextSync(output.cppRNOHPackagesFactoryPath)).toContain(
'CustomCppClass'
);
expect(memFS.readTextSync(output.cppRNOHPackagesFactoryPath)).not.toContain(
'CustomEtsClass'
);
expect(memFS.readTextSync(output.cmakeAutolinkingPath)).toContain(
'custom_cmake_target'
);
expect(memFS.readTextSync(output.ohPackagePath)).toContain(
'@rnoh/custom-oh-name'
);
});
it('should handle unscoped package correctly', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'unscoped-package': {
harmony: {
'unscoped_package.har': '',
},
'package.json': JSON.stringify({
name: 'unscoped-package',
harmony: {
autolinking: true,
},
}),
},
},
},
});
const output = await runAutolinking();
expect(memFS.readTextSync(output.etsRNOHPackagesFactoryPath)).toContain(
'UnscopedPackage'
);
expect(memFS.readTextSync(output.cppRNOHPackagesFactoryPath)).toContain(
'UnscopedPackage'
);
expect(memFS.readTextSync(output.ohPackagePath)).toContain(
'@rnoh/unscoped-package'
);
});
it('should remove entries to missing hars from oh-package.json5 and preserve custom entries not managed by autolinking', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'@rnoh': {
'existing-har': {
harmony: {
'existing_har.har': '',
},
'package.json': JSON.stringify({
name: '@rnoh/existing-har',
harmony: {
autolinking: null,
},
}),
},
'missing-har': {
harmony: {
},
'package.json': JSON.stringify({
name: '@rnoh/missing-har',
}),
},
},
},
harmony: {
...baseFileStructure.harmony,
'oh-package.json5': `{
"dependencies": {
"@rnoh/existing-har": "file:../node_modules/@rnoh/existing-har/harmony/existing_har.har",
"@rnoh/missing-har": "file:../node_modules/@rnoh/missing-har/harmony/missing_har.har"
}
}`,
},
},
});
const output = await runAutolinking();
const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
expect(ohPackageContent).toContain('@rnoh/existing-har');
expect(ohPackageContent).not.toContain('@rnoh/missing-har');
});
it('should log updated files', async () => {
const { runAutolinking } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'unscoped-package': {
harmony: {
'unscoped_package.har': '',
},
'package.json': JSON.stringify({
name: 'unscoped-package',
harmony: {
autolinking: true,
},
}),
},
},
},
});
const { logs } = await runAutolinking();
const combinedLogs = logs.map((log) => log.msg).join('\n');
expect(combinedLogs).toContain(
pathUtils.normalize('harmony/entry/src/main/cpp/autolink.cmake')
);
expect(combinedLogs).toContain(
pathUtils.normalize('harmony/entry/src/main/ets/RNOHPackageFactory.ets')
);
expect(combinedLogs).toContain(
pathUtils.normalize('harmony/entry/src/main/cpp/RNOHPackageFactory.h')
);
expect(combinedLogs).toContain(
pathUtils.normalize('harmony/oh-package.json5')
);
});
it('should log linked and skipped packages', async () => {
const { runAutolinking } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'autolinkable-package': {
harmony: {
'autolinkable_package.har': '',
},
'package.json': JSON.stringify({
name: 'autolinkable-package',
harmony: {
autolinking: true,
},
}),
},
'ignored-package': {
harmony: {
'ignored_package.har': '',
},
'package.json': JSON.stringify({
name: 'ignored-package',
harmony: {
autolinking: true,
},
}),
},
},
},
});
const { logs } = await runAutolinking({
excludedNpmPackageNames: new Set<string>().add('ignored-package'),
});
const combinedLogs = logs.map((log) => log.msg).join('\n');
expect(combinedLogs).toContain('[link] autolinkable-package');
expect(combinedLogs).toContain('[skip] ignored-package');
});
it('should fail when user lists included and excluded packages', async () => {
const { runAutolinking } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'autolinkable-package': {
harmony: {
'autolinkable_package.har': '',
},
'package.json': JSON.stringify({
name: 'autolinkable-package',
}),
},
'ignored-package': {
harmony: {
'ignored_package.har': '',
},
'package.json': JSON.stringify({
name: 'ignored-package',
}),
},
},
},
});
expect(() =>
runAutolinking({
includedNpmPackageNames: new Set<string>().add('autolinkable-package'),
excludedNpmPackageNames: new Set<string>().add('autolinkable-package'),
})
).rejects.toThrow(DescriptiveError);
});
it('should link only specified packages', async () => {
const { runAutolinking } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'autolinkable-package': {
harmony: {
'autolinkable_package.har': '',
},
'package.json': JSON.stringify({
name: 'autolinkable-package',
harmony: {
autolinking: true,
},
}),
},
'ignored-package': {
harmony: {
'ignored_package.har': '',
},
'package.json': JSON.stringify({
name: 'ignored-package',
harmony: {
autolinking: true,
},
}),
},
},
},
});
const { logs } = await runAutolinking({
includedNpmPackageNames: new Set<string>().add('autolinkable-package'),
});
const combinedLogs = logs.map((log) => log.msg).join('\n');
expect(combinedLogs).toContain('[link] autolinkable-package');
expect(combinedLogs).toContain('[skip] ignored-package');
});
it('should link by default only those packages that support autolinking', async () => {
const { runAutolinking } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'autolinkable-package': {
harmony: {
'autolinkable_package.har': '',
},
'package.json': JSON.stringify({
name: 'autolinkable-package',
harmony: {
autolinking: true,
},
}),
},
'not-autolinkable-package': {
harmony: {
'not_autolinkable_package.har': '',
},
'package.json': JSON.stringify({
name: 'not-autolinkable-package',
}),
},
},
},
});
const { logs } = await runAutolinking({});
const combinedLogs = logs.map((log) => log.msg).join('\n');
expect(combinedLogs).toContain('[link] autolinkable-package');
expect(combinedLogs).toContain('[skip] not-autolinkable-package');
});
it('not delete packages with two hars', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'@rnoh': {
'multiple-har': {
harmony: {
'multiple_har.har': '',
'some_other_har.har': '',
},
'package.json': JSON.stringify({
name: '@rnoh/multiple-har',
harmony: {
autolinking: null,
},
}),
},
},
},
harmony: {
...baseFileStructure.harmony,
'oh-package.json5': `{
"dependencies": {
"@rnoh/multiple-har": "file:../node_modules/@rnoh/multiple-har/harmony/multiple_har.har",
}
}`,
},
},
});
const output = await runAutolinking();
const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
expect(ohPackageContent).toContain('@rnoh/multiple-har');
});
it('should preserve comments in oh-package.json5', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'test-package': {
harmony: {
'test_package.har': '',
},
'package.json': JSON.stringify({
name: 'test-package',
harmony: {
autolinking: true,
},
}),
},
},
harmony: {
...baseFileStructure.harmony,
'oh-package.json5': `{
// This is a top-level comment
"modelVersion": "5.0.0",
"license": "ISC",
/* This is a multi-line
comment */
"dependencies": {
// Existing dependency comment
"@rnoh/react-native-openharmony/": "./react_native_openharmony",
},
}`,
},
},
});
const output = await runAutolinking();
const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
expect(ohPackageContent).toContain('// This is a top-level comment');
expect(ohPackageContent).toContain('/* This is a multi-line');
expect(ohPackageContent).toContain('// Existing dependency comment');
expect(ohPackageContent).toContain('@rnoh/test-package');
});
it('should support custom mainHarPath for HAR scanning', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'custom-path-package': {
'custom_harmony': {
'custom.har': '',
'subdir': {
'nested.har': '',
},
},
'package.json': JSON.stringify({
name: 'custom-path-package',
harmony: {
autolinking: {
mainHarPath: 'custom_harmony',
},
},
}),
},
},
},
});
const output = await runAutolinking();
const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
expect(ohPackageContent).toContain('@rnoh/custom-path-package--custom');
expect(ohPackageContent).toContain('@rnoh/custom-path-package--nested');
expect(ohPackageContent).toContain('custom_harmony/custom.har');
expect(ohPackageContent).toContain('custom_harmony/subdir/nested.har');
});
it('should support multiple HARs with ohPackageName array mapping', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'multi-har-package': {
harmony: {
'core.har': '',
'ui.har': '',
},
'package.json': JSON.stringify({
name: 'multi-har-package',
harmony: {
autolinking: {
ohPackageName: [
{ harName: 'core.har', packageName: '@rnoh/multi-har--core' },
{ harName: 'ui.har', packageName: '@rnoh/multi-har--ui' },
],
etsPackageClassName: 'MultiHarPackage',
cppPackageClassName: 'MultiHarPackage',
cmakeLibraryTargetName: 'rnoh__multi_har',
},
},
}),
},
},
},
});
const output = await runAutolinking();
const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
expect(ohPackageContent).toContain('@rnoh/multi-har--core');
expect(ohPackageContent).toContain('@rnoh/multi-har--ui');
expect(ohPackageContent).toContain('harmony/core.har');
expect(ohPackageContent).toContain('harmony/ui.har');
const etsContent = memFS.readTextSync(output.etsRNOHPackagesFactoryPath);
expect(etsContent).toContain('import MultiHarPackage from \'@rnoh/multi-har--core\'');
expect(etsContent).toContain('new MultiHarPackage(ctx)');
const cppContent = memFS.readTextSync(output.cppRNOHPackagesFactoryPath);
expect(cppContent).toContain('#include "MultiHarPackage.h"');
expect(cppContent).toContain('std::make_shared<rnoh::MultiHarPackage>(ctx)');
});
it('should use default naming with suffix for unmatched HARs when ohPackageName is array', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'partial-mapping-package': {
harmony: {
'matched.har': '',
'unmatched.har': '',
},
'package.json': JSON.stringify({
name: 'partial-mapping-package',
harmony: {
autolinking: {
ohPackageName: [
{ harName: 'matched.har', packageName: '@rnoh/partial--matched' },
],
},
},
}),
},
},
},
});
const output = await runAutolinking();
const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
expect(ohPackageContent).toContain('@rnoh/partial--matched');
expect(ohPackageContent).toContain('@rnoh/partial-mapping-package--unmatched');
});
it('should support remote dependency with version field', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'remote-lib': {
harmony: {
'remote.har': '',
},
'package.json': JSON.stringify({
name: 'remote-lib',
harmony: {
autolinking: {
ohPackageName: [
{ harName: 'remote.har', packageName: '@ohos/remote-lib', version: '1.0.0' }
],
etsPackageClassName: 'RemoteLibPackage',
cppPackageClassName: 'RemoteLibPackage',
},
},
}),
},
},
},
});
const output = await runAutolinking();
const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
expect(ohPackageContent).toContain('"@ohos/remote-lib": "1.0.0"');
expect(ohPackageContent).not.toContain('file:');
const etsContent = memFS.readTextSync(output.etsRNOHPackagesFactoryPath);
expect(etsContent).toContain('import RemoteLibPackage from \'@ohos/remote-lib\'');
const cppContent = memFS.readTextSync(output.cppRNOHPackagesFactoryPath);
expect(cppContent).toContain('#include "RemoteLibPackage.h"');
});
it('should support mixed local and remote dependencies in same package', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'mixed-deps-package': {
harmony: {
'local.har': '',
'remote.har': '',
},
'package.json': JSON.stringify({
name: 'mixed-deps-package',
harmony: {
autolinking: {
ohPackageName: [
{ harName: 'local.har', packageName: '@rnoh/mixed--local' },
{ harName: 'remote.har', packageName: '@ohos/mixed--remote', version: '2.0.0' }
],
etsPackageClassName: 'MixedDepsPackage',
cppPackageClassName: 'MixedDepsPackage',
},
},
}),
},
},
},
});
const output = await runAutolinking();
const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
expect(ohPackageContent).toContain('"@rnoh/mixed--local": "file:');
expect(ohPackageContent).toContain('harmony/local.har');
expect(ohPackageContent).toContain('"@ohos/mixed--remote": "2.0.0"');
});
it('should support multiple remote dependencies', async () => {
const { runAutolinking, memFS } = createAutolinking({
fsStructure: {
...baseFileStructure,
node_modules: {
'multi-remote-package': {
harmony: {
'core.har': '',
'ui.har': '',
},
'package.json': JSON.stringify({
name: 'multi-remote-package',
harmony: {
autolinking: {
ohPackageName: [
{ harName: 'core.har', packageName: '@ohos/multi--core', version: '^1.0.0' },
{ harName: 'ui.har', packageName: '@ohos/multi--ui', version: '^2.0.0' }
],
etsPackageClassName: 'MultiRemotePackage',
cppPackageClassName: 'MultiRemotePackage',
},
},
}),
},
},
},
});
const output = await runAutolinking();
const ohPackageContent = memFS.readTextSync(output.ohPackagePath);
expect(ohPackageContent).toContain('"@ohos/multi--core": "^1.0.0"');
expect(ohPackageContent).toContain('"@ohos/multi--ui": "^2.0.0"');
expect(ohPackageContent).not.toContain('file:');
});