* 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 JSON5 from 'json5';
import {
AbsolutePath,
ProjectDependenciesManager,
FS,
DescriptiveError,
} from '../core';
import Mustache from 'mustache';
import Case from 'case';
import { Logger } from '../io';
import pathUtils from 'node:path';
import { JSON5Writer } from './JSON5Writer';
type Version = string;
type DependencySpecifier = `file:${string}` | Version;
export type AutolinkingConfig = {
harmonyProjectPath: AbsolutePath;
nodeModulesPath: AbsolutePath;
ohPackagePathRelativeToHarmony: string;
etsRNOHPackagesFactoryPathRelativeToHarmony: string;
cppRNOHPackagesFactoryPathRelativeToHarmony: string;
cmakeAutolinkPathRelativeToHarmony: string;
excludedNpmPackageNames: Set<string>;
includedNpmPackageNames: Set<string>;
};
* ohPackageName 数组配置项类型
* 用于将 HAR 文件名映射到指定的 ohPackage 名
*/
type OhPackageNameMapping = {
harName: string;
packageName: string;
* 可选版本号,指定后使用远程依赖(版本号)而非本地依赖(file: 路径)
*/
version?: string;
};
* ohPackageName 支持的类型
* - string: 所有 HAR 共用同一个包名(兼容旧格式)
* - OhPackageNameMapping[]: 每个 HAR 可以有独立的包名
*/
type OhPackageNameConfig = string | OhPackageNameMapping[];
* 解析后的 HAR 配置
* 每个 HAR 文件对应一个 ResolvedHar
*/
type ResolvedHar = {
harFilePathRelativeToHarmony: string;
ohPackageName: string;
* 可选版本号,有值时使用远程依赖(版本号)而非本地依赖(file: 路径)
*/
version?: string;
};
* 可链接的库配置
* 支持单个或多个 HAR 文件
*/
type AutolinkableLibrary = {
npmPackageName: string;
resolvedHars: ResolvedHar[];
etsRNOHPackageClassName?: string;
cppRNOHPackageClassName?: string;
cmakeLibraryTargetName?: string;
};
type AutolinkingInput = {
projectRootPath: AbsolutePath;
etsRNOHPackagesFactoryPath: AbsolutePath;
cppRNOHPackagesFactoryPath: AbsolutePath;
cmakeAutolinkingPath: AbsolutePath;
ohPackagePathAndContent: [AbsolutePath, string];
autolinkableLibraries: AutolinkableLibrary[];
nodeModuleHarFilePathsRelativeToHarmony: string[];
skippedLibraryNpmPackageNames: string[];
};
type AutolinkingOutput = {
linkedLibraryNpmPackageNames: string[];
skippedLibraryNpmPackageNames: string[];
projectRootPath: AbsolutePath;
etsRNOHPackagesFactoryPathAndContent: [AbsolutePath, string];
cppRNOHPackagesFactoryPathAndContent: [AbsolutePath, string];
cmakeAutolinkingPathAndContent: [AbsolutePath, string];
ohPackagePathAndContent: [AbsolutePath, string];
};
const ETS_RNOH_PACKAGES_FACTORY_TEMPLATE = `
/*
* 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';
{{#libraries}}
import {{etsRNOHPackageClassName}} from '{{{ohPackageName}}}';
{{/libraries}}
export function createRNOHPackages(ctx: RNPackageContext): RNOHPackage[] {
return [
{{#libraries}}
new {{etsRNOHPackageClassName}}(ctx),
{{/libraries}}
];
}
`.trimStart();
const CPP_RNOH_PACKAGES_FACTORY_TEMPLATE = `
/*
* 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"
{{#libraries}}
#include "{{cppRNOHPackageClassName}}.h"
{{/libraries}}
std::vector<rnoh::Package::Shared> createRNOHPackages(const rnoh::Package::Context &ctx) {
return {
{{#libraries}}
std::make_shared<rnoh::{{cppRNOHPackageClassName}}>(ctx),
{{/libraries}}
};
}
`.trimStart();
const CMAKE_AUTOLINKING_TEMPLATE = `
# 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)
{{#libraries}}
add_subdirectory("\${OH_MODULES_DIR}/{{{ohPackageName}}}/src/main/cpp" ./{{cmakeLibraryTargetName}})
{{/libraries}}
set(AUTOLINKED_LIBRARIES
{{#libraries}}
{{cmakeLibraryTargetName}}
{{/libraries}}
)
foreach(lib \${AUTOLINKED_LIBRARIES})
target_link_libraries(\${target} PUBLIC \${lib})
endforeach()
endfunction()
`.trimStart();
export class Autolinking {
constructor(
private fs: FS,
private logger: Logger
) { }
async prepareInput(config: AutolinkingConfig): Promise<AutolinkingInput> {
if (
config.includedNpmPackageNames.size > 0 &&
config.excludedNpmPackageNames.size > 0
) {
throw new DescriptiveError({
whatHappened: 'Tried to exclude and include npm packages.',
whatCanUserDo: ['Include or exclude npm packages, but not both.'],
extraData: {
includedNpmPackageNamesCount: config.includedNpmPackageNames.size,
excludedNpmPackageNamesCount: config.excludedNpmPackageNames.size,
includedNpmPackageNames: config.includedNpmPackageNames,
excludedNpmPackageNames: config.excludedNpmPackageNames,
},
});
}
const harmonyProjectPath =
config.harmonyProjectPath ?? new AbsolutePath('./');
const nodeModuleHarPaths: AbsolutePath[] = [];
const autolinkableLibraries: AutolinkableLibrary[] = [];
const projectRootPath = config.nodeModulesPath.copyWithNewSegment('..');
const skippedLibraryNpmPackageNames: string[] = [];
await new ProjectDependenciesManager(this.fs, projectRootPath).forEachAsync(
(dependency) => {
const packageJson = dependency.readPackageJSON();
const providedAutolinkingConfig = packageJson.harmony?.autolinking;
const autolinkingConfig =
providedAutolinkingConfig === true ? {} : providedAutolinkingConfig;
const mainHarPath = autolinkingConfig?.mainHarPath;
const harFilePaths = dependency.getHarFilePaths(mainHarPath);
if (harFilePaths.length === 0) {
return;
}
nodeModuleHarPaths.push(...harFilePaths);
if (
providedAutolinkingConfig === undefined ||
providedAutolinkingConfig === null ||
(config.excludedNpmPackageNames.has(packageJson.name) &&
config.excludedNpmPackageNames.size > 0) ||
(!config.includedNpmPackageNames.has(packageJson.name) &&
config.includedNpmPackageNames.size > 0)
) {
skippedLibraryNpmPackageNames.push(packageJson.name);
return;
}
const ohPackageNameConfig = autolinkingConfig?.ohPackageName as OhPackageNameConfig | undefined;
const resolvedHars = this.resolveHarPackageNames(
harFilePaths,
ohPackageNameConfig,
packageJson.name,
harmonyProjectPath
);
autolinkableLibraries.push({
npmPackageName: packageJson.name,
resolvedHars,
etsRNOHPackageClassName: autolinkingConfig?.etsPackageClassName,
cppRNOHPackageClassName: autolinkingConfig?.cppPackageClassName,
cmakeLibraryTargetName: autolinkingConfig?.cmakeLibraryTargetName,
});
}
);
const ohPackagePath = harmonyProjectPath.copyWithNewSegment(
config.ohPackagePathRelativeToHarmony
);
const ohPackageContent = await this.fs.readText(ohPackagePath);
const cppRNOHPackagesFactoryPath = harmonyProjectPath.copyWithNewSegment(
config.cppRNOHPackagesFactoryPathRelativeToHarmony
);
const cmakeAutolinkingPath = harmonyProjectPath.copyWithNewSegment(
config.cmakeAutolinkPathRelativeToHarmony
);
return {
projectRootPath,
skippedLibraryNpmPackageNames,
etsRNOHPackagesFactoryPath: harmonyProjectPath.copyWithNewSegment(
config.etsRNOHPackagesFactoryPathRelativeToHarmony
),
cppRNOHPackagesFactoryPath,
cmakeAutolinkingPath: cmakeAutolinkingPath,
ohPackagePathAndContent: [ohPackagePath, ohPackageContent],
autolinkableLibraries,
nodeModuleHarFilePathsRelativeToHarmony: nodeModuleHarPaths.map((p) =>
p.relativeTo(harmonyProjectPath).toString()
),
};
}
* 解析 HAR 文件的 ohPackage 名称
* @param harFilePaths HAR 文件路径列表
* @param ohPackageNameConfig ohPackageName 配置(字符串或数组)
* @param npmPackageName npm 包名
* @param harmonyProjectPath Harmony 项目路径
*/
private resolveHarPackageNames(
harFilePaths: AbsolutePath[],
ohPackageNameConfig: OhPackageNameConfig | undefined,
npmPackageName: string,
harmonyProjectPath: AbsolutePath
): ResolvedHar[] {
const defaultOhPackageName = this.npmPackageNameToOHPackageName(npmPackageName);
const harPathMap = new Map<string, AbsolutePath>();
for (const harPath of harFilePaths) {
const harFileName = pathUtils.basename(harPath.toString());
harPathMap.set(harFileName, harPath);
}
const createResolvedHar = (harPath: AbsolutePath, ohPkgName: string, version?: string): ResolvedHar => ({
harFilePathRelativeToHarmony: harPath.relativeTo(harmonyProjectPath).toString(),
ohPackageName: ohPkgName,
version,
});
const result: ResolvedHar[] = [];
const processedHarNames = new Set<string>();
if (Array.isArray(ohPackageNameConfig)) {
for (const mapping of ohPackageNameConfig) {
const harPath = harPathMap.get(mapping.harName);
if (harPath) {
result.push(createResolvedHar(harPath, mapping.packageName, mapping.version));
processedHarNames.add(mapping.harName);
}
}
}
for (const harPath of harFilePaths) {
const harFileName = pathUtils.basename(harPath.toString());
if (processedHarNames.has(harFileName)) {
continue;
}
if (typeof ohPackageNameConfig === 'string') {
const suffix = harFilePaths.length > 1
? '--' + pathUtils.basename(harFileName, '.har')
: '';
result.push(createResolvedHar(harPath, ohPackageNameConfig + suffix));
continue;
}
const suffix = harFilePaths.length > 1
? '--' + pathUtils.basename(harFileName, '.har')
: '';
result.push(createResolvedHar(harPath, defaultOhPackageName + suffix));
}
return result;
}
evaluate(input: AutolinkingInput): AutolinkingOutput {
const autolinkableLibraries: (Required<Omit<AutolinkableLibrary, 'resolvedHars'>> & { resolvedHars: ResolvedHar[] })[] =
input.autolinkableLibraries.map((lib) => {
return {
...lib,
etsRNOHPackageClassName:
lib.etsRNOHPackageClassName ??
this.npmPackageNameToRNOHPackageClassName(lib.npmPackageName),
cppRNOHPackageClassName:
lib.cppRNOHPackageClassName ??
this.npmPackageNameToRNOHPackageClassName(lib.npmPackageName),
cmakeLibraryTargetName:
lib.cmakeLibraryTargetName ??
this.npmPackageNameToCMakeLibraryTargetName(lib.npmPackageName),
resolvedHars: lib.resolvedHars,
};
});
autolinkableLibraries.sort((a, b) => {
if (a.npmPackageName < b.npmPackageName) return -1;
if (a.npmPackageName > b.npmPackageName) return 1;
return 0;
});
const ohPackage = JSON5.parse(input.ohPackagePathAndContent[1]);
const unmanagedNativeDependencySpecifierByName: Record<
string,
DependencySpecifier
> = {};
const autolinkedNpmPackageNames = new Set(
autolinkableLibraries.map(lib => lib.npmPackageName)
);
Object.entries<DependencySpecifier>(ohPackage.dependencies).forEach(
([name, dependencySpecifier]) => {
if (
!(
dependencySpecifier.includes('file:') &&
dependencySpecifier.includes('node_modules')
)
) {
unmanagedNativeDependencySpecifierByName[name] = dependencySpecifier;
} else {
const npmPackageName = this.extractNpmPackageNameFromPath(dependencySpecifier);
if (!npmPackageName || !autolinkedNpmPackageNames.has(npmPackageName)) {
const harPath = dependencySpecifier.replace('file:', '');
const normalizedHarPath = pathUtils.normalize(harPath);
const harmonyProjectPath = input.ohPackagePathAndContent[0].getDirectoryPath();
const absoluteHarPath = harmonyProjectPath.copyWithNewSegment(normalizedHarPath);
if (this.fs.existsSync(absoluteHarPath)) {
unmanagedNativeDependencySpecifierByName[name] = dependencySpecifier;
}
}
}
}
);
const managedNativeDependencySpecifierByName: Record<string, string> = {};
for (const library of autolinkableLibraries) {
for (const har of library.resolvedHars) {
managedNativeDependencySpecifierByName[har.ohPackageName] =
har.version ??
`file:${har.harFilePathRelativeToHarmony}`.split(pathUtils.sep).join('/');
}
}
const templateLibraries = autolinkableLibraries.map((lib) => {
const primaryOhPackageName = lib.resolvedHars[0]?.ohPackageName ?? '';
return {
...lib,
ohPackageName: primaryOhPackageName,
};
});
return {
skippedLibraryNpmPackageNames: input.skippedLibraryNpmPackageNames,
linkedLibraryNpmPackageNames: autolinkableLibraries.map(
(lib) => lib.npmPackageName
),
projectRootPath: input.projectRootPath,
cppRNOHPackagesFactoryPathAndContent: [
input.cppRNOHPackagesFactoryPath,
Mustache.render(CPP_RNOH_PACKAGES_FACTORY_TEMPLATE, {
libraries: templateLibraries,
}),
],
etsRNOHPackagesFactoryPathAndContent: [
input.etsRNOHPackagesFactoryPath,
Mustache.render(ETS_RNOH_PACKAGES_FACTORY_TEMPLATE, {
libraries: templateLibraries,
}),
],
cmakeAutolinkingPathAndContent: [
input.cmakeAutolinkingPath,
Mustache.render(CMAKE_AUTOLINKING_TEMPLATE, {
libraries: templateLibraries,
}),
],
ohPackagePathAndContent: [
input.ohPackagePathAndContent[0],
JSON5Writer.updateDependenciesWithFallback(
input.ohPackagePathAndContent[1],
{
...unmanagedNativeDependencySpecifierByName,
...managedNativeDependencySpecifierByName,
}
),
],
};
}
private npmPackageNameToRNOHPackageClassName(fullNpmPackageName: string) {
if (fullNpmPackageName.startsWith('@')) {
const [scopeName, packageName] = fullNpmPackageName
.replace('@', '')
.split('/');
return Case.pascal(scopeName) + Case.pascal(packageName) + 'Package';
}
return Case.pascal(fullNpmPackageName) + 'Package';
}
private npmPackageNameToCMakeLibraryTargetName(fullNpmPackageName: string) {
if (fullNpmPackageName.startsWith('@')) {
const [scopeName, packageName] = fullNpmPackageName
.replace('@', '')
.split('/');
return 'rnoh__' + Case.snake(scopeName) + '__' + Case.snake(packageName);
}
return 'rnoh__' + Case.snake(fullNpmPackageName);
}
private npmPackageNameToOHPackageName(fullNpmPackageName: string): string {
if (fullNpmPackageName.startsWith('@')) {
const [scopeName, packageName] = fullNpmPackageName
.replace('@', '')
.split('/');
return '@rnoh/' + Case.kebab(scopeName) + '--' + Case.kebab(packageName);
}
return '@rnoh/' + Case.kebab(fullNpmPackageName);
}
* 从依赖路径中提取 npm 包名
* @param dependencySpecifier 依赖路径,如 "file:../node_modules/@scope/package/harmony/xxx.har"
* @returns npm 包名,如 "@scope/package";如果无法解析则返回 null
*/
private extractNpmPackageNameFromPath(dependencySpecifier: string): string | null {
const path = dependencySpecifier.replace(/^file:/, '');
const nodeModulesIndex = path.indexOf('node_modules/');
if (nodeModulesIndex === -1) {
return null;
}
const afterNodeModules = path.substring(nodeModulesIndex + 'node_modules/'.length);
if (afterNodeModules.startsWith('@')) {
const parts = afterNodeModules.split('/');
if (parts.length >= 2) {
return `${parts[0]}/${parts[1]}`;
}
} else {
const firstSlash = afterNodeModules.indexOf('/');
if (firstSlash !== -1) {
return afterNodeModules.substring(0, firstSlash);
}
return afterNodeModules || null;
}
return null;
}
saveAndLogOutput(output: AutolinkingOutput) {
const pathAndContentPairs: [AbsolutePath, string][] = [];
pathAndContentPairs.push(output.etsRNOHPackagesFactoryPathAndContent);
pathAndContentPairs.push(output.cppRNOHPackagesFactoryPathAndContent);
pathAndContentPairs.push(output.cmakeAutolinkingPathAndContent);
pathAndContentPairs.push(output.ohPackagePathAndContent);
pathAndContentPairs.forEach(([path, content]) => {
this.fs.writeTextSync(path, content);
});
output.linkedLibraryNpmPackageNames.forEach((npmPackageName) => {
this.logger.debug(
(styles) => `[${styles.green(styles.bold('link'))}] ${npmPackageName}`
);
});
output.skippedLibraryNpmPackageNames.forEach((npmPackageName) => {
this.logger.debug(
(styles) => `[${styles.yellow(styles.bold('skip'))}] ${npmPackageName}`
);
});
this.logger.debug(() => '');
const sortedPathsRelativeToRoot = pathAndContentPairs
.map(([path]) => path.relativeTo(output.projectRootPath).toString())
.sort();
sortedPathsRelativeToRoot.forEach((pathStr) => {
this.logger.debug((styles) => styles.gray(`• ${pathStr}`));
});
this.logger.debug(() => '');
this.logger.info(
(styles) =>
`updated ${styles.green(
styles.bold(sortedPathsRelativeToRoot.length)
)} file(s), linked ${styles.green(
styles.bold(output.linkedLibraryNpmPackageNames.length)
)} libraries, skipped ${styles.yellow(
styles.bold(output.skippedLibraryNpmPackageNames.length)
)} libraries`
);
this.logger.debug(() => '');
}
}