@expo-harmony/template 55.0.0-harmony.0

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/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @expo-harmony/template
2
+
3
+ [**GitHub 仓库**](https://github.com/renbaoshuo/expo-harmony/tree/master/packages/template)
4
+
5
+ 为 Expo 项目提供 HarmonyOS 原生工程模板,包含 AppScope、Entry 模块、Hvigor、CMake 和 RNOH 宿主代码,适配 Expo SDK 55、React Native 0.84.1 和 RNOH 0.84.1。
6
+
7
+ 该模板由 `@expo-harmony/prebuild-config` 和 `@expo-harmony/cli` 管理,通常无需在应用中直接使用。配置好预构建插件后,运行以下命令生成或更新 `harmony` 原生工程:
8
+
9
+ ```sh
10
+ npx expo-harmony prebuild
11
+ ```
12
+
13
+ ## 说明
14
+
15
+ 模板只包含 Expo Modules Core 和 HarmonyOS 构建所需的基础依赖。应用使用其他 Expo 功能时,需要同时安装对应的官方 JavaScript 包和 `@expo-harmony/*` 原生包,预构建阶段会自动发现并链接这些模块。
16
+
17
+ 模板中的应用名称、包名和 Ability 等内容均为占位值,最终结果由 `@expo-harmony/prebuild-config` 根据 Expo 配置生成。签名密钥、密码、绝对路径、OHPM 缓存、Bundle 和 HAP 等本地或构建产物不会包含在模板中。
18
+
19
+ 原生构建需要 Node.js 20 或更高版本,以及可用的 HarmonyOS SDK/NDK、OHPM 和 Hvigor。Release 构建会通过项目本地的 `@expo-harmony/cli` 生成 Hermes Bundle;如需指定 Node.js 可执行文件,可设置 `EXPO_HARMONY_NODE`。
20
+
21
+ ## Author
22
+
23
+ **expo-harmony** © [Baoshuo](https://github.com/renbaoshuo), Released under the MIT License.<br>
24
+ Authored and maintained by Baoshuo with help from [contributors](https://github.com/renbaoshuo/expo-harmony/contributors).
25
+
26
+ > [Personal Website](https://baoshuo.ren) · [Blog](https://blog.baoshuo.ren) · GitHub [@renbaoshuo](https://github.com/renbaoshuo) · Twitter [@baoshuo](https://twitter.com/baoshuo)
@@ -0,0 +1,8 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "placeholders": {
4
+ "abilityName": "__EXPO_HARMONY_ABILITY_NAME__",
5
+ "appLabel": "__EXPO_HARMONY_APP_LABEL__",
6
+ "bundlePath": "__EXPO_HARMONY_BUNDLE_PATH__"
7
+ }
8
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "app": {
3
+ "bundleName": "com.placeholder.app",
4
+ "vendor": "expo-harmony",
5
+ "versionCode": 1,
6
+ "versionName": "1.0.0",
7
+ "icon": "$media:app_icon",
8
+ "label": "$string:app_name"
9
+ }
10
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "string": [
3
+ {
4
+ "name": "app_name",
5
+ "value": "expo-harmony"
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
2
+ <rect width="512" height="512" rx="112" fill="#111827"/>
3
+ <path d="M139 342 229 151c11-24 43-24 54 0l90 191h-56l-19-43h-84l-19 43h-56Zm96-92h42l-21-50-21 50Z" fill="#F9FAFB"/>
4
+ </svg>
@@ -0,0 +1,32 @@
1
+ {
2
+ "app": {
3
+ "products": [
4
+ {
5
+ "name": "default",
6
+ "compatibleSdkVersion": "6.1.0(23)",
7
+ "targetSdkVersion": "6.1.1(24)",
8
+ "runtimeOS": "HarmonyOS",
9
+ "buildOption": {
10
+ "nativeCompiler": "BiSheng"
11
+ }
12
+ }
13
+ ],
14
+ "buildModeSet": [
15
+ { "name": "debug" },
16
+ { "name": "release" }
17
+ ],
18
+ "signingConfigs": []
19
+ },
20
+ "modules": [
21
+ {
22
+ "name": "entry",
23
+ "srcPath": "./entry",
24
+ "targets": [
25
+ {
26
+ "name": "default",
27
+ "applyToProducts": ["default"]
28
+ }
29
+ ]
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "apiType": "stageMode",
3
+ "buildOption": {
4
+ "externalNativeOptions": {
5
+ "path": "./src/main/cpp/CMakeLists.txt",
6
+ "arguments": "",
7
+ "abiFilters": ["arm64-v8a", "x86_64"]
8
+ }
9
+ },
10
+ "targets": [
11
+ {
12
+ "name": "default",
13
+ "runtimeOS": "HarmonyOS"
14
+ },
15
+ {
16
+ "name": "ohosTest"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { hapTasks } from '@ohos/hvigor-ohos-plugin';
4
+ import { createRNOHModulePlugin } from '@rnoh/hvigor-plugin';
5
+
6
+ const projectRoot = path.resolve('..');
7
+ function resolveNodeModulesPath() {
8
+ let directory = projectRoot;
9
+ while (true) {
10
+ const candidate = path.join(directory, 'node_modules');
11
+ if (fs.existsSync(candidate)) return candidate;
12
+ const parent = path.dirname(directory);
13
+ if (parent === directory) throw new Error('Unable to find node_modules for the Harmony project.');
14
+ directory = parent;
15
+ }
16
+ }
17
+ const nodeModulesPath = resolveNodeModulesPath();
18
+
19
+ export default {
20
+ system: hapTasks,
21
+ plugins: [
22
+ createRNOHModulePlugin({
23
+ nodeModulesPath,
24
+ codegen: {
25
+ rnohModulePath: './oh_modules/@rnoh/react-native-openharmony',
26
+ cppOutputPath: './entry/src/main/cpp/rnoh_codegen/generated',
27
+ },
28
+ autolinking: null,
29
+ }),
30
+ ],
31
+ };
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "entry",
3
+ "version": "1.0.0",
4
+ "description": "expo-harmony entry module",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Baoshuo",
8
+ "email": "i@baoshuo.ren"
9
+ },
10
+ "homepage": "https://github.com/renbaoshuo/expo-harmony",
11
+ "repository": "https://github.com/renbaoshuo/expo-harmony.git",
12
+ "dependencies": {}
13
+ }
@@ -0,0 +1,43 @@
1
+ cmake_minimum_required(VERSION 3.13)
2
+ project(expo_harmony_app LANGUAGES CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 20)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
7
+ set(OH_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../oh_modules")
8
+ set(RNOH_CPP_DIR "${OH_MODULES_DIR}/@rnoh/react-native-openharmony/src/main/cpp")
9
+ set(RNOH_CODEGEN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/rnoh_codegen")
10
+ set(RNOH_GENERATED_DIR "${RNOH_CODEGEN_ROOT}/generated")
11
+ set(LOG_VERBOSITY_LEVEL 1)
12
+ set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments")
13
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack")
14
+
15
+ # Hvigor rewrites this ignored stamp only when the bytes of a linked HAR change.
16
+ # The digest is part of every native compile command, so deterministic archives
17
+ # with normalized old mtimes cannot make Ninja reuse stale objects.
18
+ set(EXPO_HARMONY_NATIVE_INPUTS_STAMP
19
+ "${CMAKE_CURRENT_SOURCE_DIR}/../../../../.hvigor/expo-harmony/native-inputs.cmake")
20
+ if(NOT EXISTS "${EXPO_HARMONY_NATIVE_INPUTS_STAMP}")
21
+ message(FATAL_ERROR "Harmony native input fingerprint is missing; build through the root Hvigor project.")
22
+ endif()
23
+ include("${EXPO_HARMONY_NATIVE_INPUTS_STAMP}")
24
+
25
+ if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin" AND DEFINED OHOS_SDK_NATIVE)
26
+ set(CMAKE_CXX_FLAGS
27
+ "${CMAKE_CXX_FLAGS} -include ${OHOS_SDK_NATIVE}/sysroot/usr/include/assert.h")
28
+ endif()
29
+
30
+ add_subdirectory("${RNOH_CPP_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/rnoh")
31
+ include("${CMAKE_CURRENT_SOURCE_DIR}/autolinking.cmake")
32
+
33
+ file(GLOB GENERATED_CPP_FILES "${RNOH_GENERATED_DIR}/*.cpp")
34
+ add_library(rnoh_app SHARED
35
+ ${GENERATED_CPP_FILES}
36
+ "./PackageProvider.cpp"
37
+ "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp")
38
+
39
+ target_link_libraries(rnoh_app PUBLIC rnoh)
40
+ target_include_directories(rnoh_app PRIVATE "${RNOH_CODEGEN_ROOT}")
41
+ target_include_directories(rnoh_app PRIVATE
42
+ "${OH_MODULES_DIR}/@expo-harmony/expo-modules-core/src/main/cpp/package")
43
+ autolink_libraries(rnoh_app)
@@ -0,0 +1,19 @@
1
+ #include <RNOH/PackageProvider.h>
2
+
3
+ #include <package/ExpoModulesCorePackage.h>
4
+
5
+ namespace rnoh {
6
+ using ExpoModulesCorePackage = expo::harmony::ExpoModulesCorePackage;
7
+ }
8
+
9
+ #include <memory>
10
+ #include <vector>
11
+
12
+ #include "RNOHPackagesFactory.h"
13
+ #include "rnoh_codegen/generated/RNOHGeneratedPackage.h"
14
+
15
+ std::vector<std::shared_ptr<rnoh::Package>> rnoh::PackageProvider::getPackages(rnoh::Package::Context context) {
16
+ auto packages = createRNOHPackages(context);
17
+ packages.push_back(std::make_shared<rnoh::RNOHGeneratedPackage>(context));
18
+ return packages;
19
+ }
@@ -0,0 +1,6 @@
1
+ import type { RNPackage, RNPackageContext } from '@rnoh/react-native-openharmony';
2
+ import { createRNOHPackages } from './RNOHPackagesFactory';
3
+
4
+ export function getRNOHPackages(context: RNPackageContext): RNPackage[] {
5
+ return createRNOHPackages(context);
6
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Automatically generated by @expo-harmony/prebuild-config.
3
+ */
4
+
5
+ import type { ExpoHarmonyHostProvider } from '@expo-harmony/expo-modules-core/Autolinking';
6
+ import { ExpoRNAbility } from '@expo-harmony/expo-modules-core/Autolinking';
7
+ import { expoHarmonyHostProvider } from '../generated/ExpoHarmonyHostProvider';
8
+
9
+ export default class __EXPO_HARMONY_ABILITY_NAME__ extends ExpoRNAbility {
10
+ override getPagePath(): string {
11
+ return 'pages/Index';
12
+ }
13
+
14
+ protected override getExpoHarmonyHostProvider(): ExpoHarmonyHostProvider {
15
+ return expoHarmonyHostProvider;
16
+ }
17
+
18
+ protected override getExpoHarmonyWorkerScriptUrl(): string {
19
+ return 'entry/ets/workers/RNOHWorker.ets';
20
+ }
21
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Automatically generated by @expo-harmony/prebuild-config.
3
+ */
4
+
5
+ import type resmgr from '@ohos.resourceManager';
6
+ import {
7
+ MetroJSBundleProvider,
8
+ RNOHCoreContext,
9
+ RNOHErrorDialog,
10
+ ResourceJSBundleProvider,
11
+ } from '@rnoh/react-native-openharmony';
12
+ import { ExpoRNApp } from '@expo-harmony/expo-modules-core';
13
+ import { getRNOHPackages } from '../PackageProvider';
14
+ import { ExpoHarmonyRootView } from '../generated/ExpoHarmonyHostProvider';
15
+
16
+ class InMemoryResourceJSBundleProvider extends ResourceJSBundleProvider {
17
+ private readonly ownedResourceManager: resmgr.ResourceManager;
18
+ private readonly ownedResourcePath: string;
19
+
20
+ constructor(resourceManager: resmgr.ResourceManager, resourcePath: string) {
21
+ super(resourceManager, resourcePath);
22
+ this.ownedResourceManager = resourceManager;
23
+ this.ownedResourcePath = resourcePath;
24
+ }
25
+
26
+ override async getBundle(): Promise<ArrayBuffer> {
27
+ const bytes = await this.ownedResourceManager.getRawFileContent(this.ownedResourcePath);
28
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
29
+ }
30
+ }
31
+
32
+ @Entry
33
+ @Component
34
+ struct Index {
35
+ @StorageLink('RNOHCoreContext') private rnohCoreContext: RNOHCoreContext | undefined = undefined;
36
+
37
+ build(): void {
38
+ Stack() {
39
+ ExpoHarmonyRootView()
40
+ Column() {
41
+ if (this.rnohCoreContext !== undefined) {
42
+ if (this.rnohCoreContext.isDebugModeEnabled) {
43
+ RNOHErrorDialog({ ctx: this.rnohCoreContext });
44
+ }
45
+ ExpoRNApp({
46
+ rnInstanceConfig: {
47
+ name: "__EXPO_HARMONY_APP_LABEL__",
48
+ createRNPackages: getRNOHPackages,
49
+ fontResourceByFontFamily: {},
50
+ enableDebugger: this.rnohCoreContext.isDebugModeEnabled,
51
+ },
52
+ appKey: 'main',
53
+ jsBundleProvider: this.rnohCoreContext.isDebugModeEnabled
54
+ ? new MetroJSBundleProvider()
55
+ : new InMemoryResourceJSBundleProvider(
56
+ this.rnohCoreContext.uiAbilityContext.resourceManager,
57
+ 'hermes_bundle.hbc',
58
+ ),
59
+ });
60
+ }
61
+ }
62
+ .height('100%')
63
+ .width('100%');
64
+ }
65
+ .height('100%')
66
+ .width('100%');
67
+ }
68
+ }
@@ -0,0 +1,8 @@
1
+ import { setupRNOHWorker } from '@rnoh/react-native-openharmony/src/main/ets/setupRNOHWorker';
2
+ import { getRNOHPackages } from '../PackageProvider';
3
+
4
+ setupRNOHWorker({
5
+ createWorkerRNInstanceConfig: () => ({
6
+ thirdPartyPackagesFactory: getRNOHPackages,
7
+ }),
8
+ });
@@ -0,0 +1,43 @@
1
+ {
2
+ "module": {
3
+ "name": "entry",
4
+ "type": "entry",
5
+ "description": "$string:expo_harmony_module_desc",
6
+ "mainElement": "EntryAbility",
7
+ "deviceTypes": ["phone", "tablet"],
8
+ "deliveryWithInstall": true,
9
+ "installationFree": false,
10
+ "pages": "$profile:main_pages",
11
+ "requestPermissions": [
12
+ {
13
+ "name": "ohos.permission.INTERNET"
14
+ }
15
+ ],
16
+ "metadata": [
17
+ {
18
+ "name": "expo.harmony.userInterfaceStyle",
19
+ "value": "light"
20
+ }
21
+ ],
22
+ "abilities": [
23
+ {
24
+ "name": "EntryAbility",
25
+ "srcEntry": "./ets/entryability/EntryAbility.ets",
26
+ "description": "$string:expo_harmony_ability_desc",
27
+ "icon": "$media:app_icon",
28
+ "label": "$string:expo_harmony_ability_label",
29
+ "startWindowIcon": "$media:app_icon",
30
+ "startWindowBackground": "$color:expo_harmony_start_window_background",
31
+ "exported": true,
32
+ "visible": true,
33
+ "orientation": "unspecified",
34
+ "skills": [
35
+ {
36
+ "entities": ["entity.system.home"],
37
+ "actions": ["action.system.home"]
38
+ }
39
+ ]
40
+ }
41
+ ]
42
+ }
43
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "color": [
3
+ {
4
+ "name": "expo_harmony_start_window_background",
5
+ "value": "#FFFFFF"
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "string": [
3
+ { "name": "expo_harmony_ability_desc", "value": "Expo Harmony main ability" },
4
+ { "name": "expo_harmony_ability_label", "value": "Expo Harmony App" },
5
+ { "name": "expo_harmony_module_desc", "value": "Expo Harmony entry module" }
6
+ ]
7
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
2
+ <rect width="512" height="512" rx="112" fill="#111827"/>
3
+ <path d="M139 342 229 151c11-24 43-24 54 0l90 191h-56l-19-43h-84l-19 43h-56Zm96-92h42l-21-50-21 50Z" fill="#F9FAFB"/>
4
+ </svg>
@@ -0,0 +1,3 @@
1
+ {
2
+ "src": ["pages/Index"]
3
+ }
@@ -0,0 +1,12 @@
1
+ oh_modules/
2
+ .hvigor/
3
+ build/
4
+ **/build/
5
+ local.properties
6
+ oh-package-lock.json5
7
+ *.hap
8
+ *.app
9
+ *.hsp
10
+ *.har
11
+ entry/src/main/resources/rawfile/*.hbc
12
+ entry/src/main/resources/rawfile/*.map
@@ -0,0 +1,8 @@
1
+ {
2
+ "modelVersion": "5.0.0",
3
+ "dependencies": {},
4
+ "execution": {},
5
+ "logging": {},
6
+ "debugging": {},
7
+ "nodeOptions": {}
8
+ }
@@ -0,0 +1,240 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { hvigor } from '@ohos/hvigor';
6
+ import { appTasks, OhosPluginId } from '@ohos/hvigor-ohos-plugin';
7
+ import { createRNOHProjectPlugin } from '@rnoh/hvigor-plugin';
8
+
9
+ import { writeStamp } from './native-inputs-stamp';
10
+
11
+ process.env.HERMES_V1_ENABLED = 'true';
12
+
13
+ const ProjectRoot = path.resolve('..');
14
+ const HarmonyRoot = path.join(ProjectRoot, 'harmony');
15
+ const NodeModules = findNodeModules();
16
+ const ProjectRequire = createRequire(path.join(ProjectRoot, 'package.json'));
17
+ const EmbeddedBundle = path.join(ProjectRoot, '__EXPO_HARMONY_BUNDLE_PATH__');
18
+ const HermesMagic = 'c61fbc03c103191f';
19
+
20
+ let restoreBundle: (() => void) | null = null;
21
+
22
+ writeStamp(ProjectRoot, HarmonyRoot);
23
+ hvigor.nodesEvaluated(async () => {
24
+ await refreshExpoConstantsResource();
25
+ });
26
+
27
+ async function refreshExpoConstantsResource(): Promise<void> {
28
+ let plugin: { refreshExpoConstantsResourceAsync?: (projectRoot: string, harmonyRoot: string) => Promise<string> };
29
+ try {
30
+ plugin = ProjectRequire('@expo-harmony/expo-constants/plugin/withConstants');
31
+ } catch (error: unknown) {
32
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'MODULE_NOT_FOUND') return;
33
+ throw error;
34
+ }
35
+
36
+ await plugin.refreshExpoConstantsResourceAsync?.(ProjectRoot, HarmonyRoot);
37
+ }
38
+
39
+ function findNodeModules(): string {
40
+ let directory = ProjectRoot;
41
+
42
+ while (true) {
43
+ const candidate = path.join(directory, 'node_modules');
44
+ if (fs.existsSync(candidate)) return candidate;
45
+
46
+ const parent = path.dirname(directory);
47
+ if (parent === directory) throw new Error('Unable to find node_modules for the Harmony project.');
48
+ directory = parent;
49
+ }
50
+ }
51
+
52
+ function stashBundle() {
53
+ if (!fs.existsSync(EmbeddedBundle) || restoreBundle) return;
54
+
55
+ const backupRoot = path.join(ProjectRoot, '.expo', 'harmony');
56
+ fs.mkdirSync(backupRoot, { recursive: true });
57
+
58
+ const backupDir = fs.mkdtempSync(path.join(backupRoot, 'debug-build-'));
59
+ const backup = path.join(backupDir, path.basename(EmbeddedBundle));
60
+ fs.renameSync(EmbeddedBundle, backup);
61
+
62
+ restoreBundle = () => {
63
+ if (!fs.existsSync(backup)) {
64
+ fs.rmSync(backupDir, { recursive: true, force: true });
65
+ restoreBundle = null;
66
+ throw new Error('Cannot restore the release Harmony bundle because its debug-build backup is missing.');
67
+ }
68
+
69
+ if (fs.existsSync(EmbeddedBundle)) {
70
+ throw new Error('Cannot restore the release Harmony bundle because its destination changed during the debug build.');
71
+ }
72
+
73
+ fs.renameSync(backup, EmbeddedBundle);
74
+ fs.rmSync(backupDir, { recursive: true, force: true });
75
+ restoreBundle = null;
76
+ };
77
+
78
+ const restoreOnExit = () => {
79
+ try {
80
+ restoreBundle?.();
81
+ } catch (error) {
82
+ process.stderr.write(`[expo-harmony] Failed to restore the release bundle: ${String(error)}\n`);
83
+ }
84
+ };
85
+
86
+ process.once('exit', restoreOnExit);
87
+ hvigor.buildFinished(() => {
88
+ try {
89
+ restoreBundle?.();
90
+ } finally {
91
+ if (!restoreBundle) process.removeListener('exit', restoreOnExit);
92
+ }
93
+ });
94
+ }
95
+
96
+ function readNodeVersion(executable: string): string | null {
97
+ const result = spawnSync(executable, ['-p', 'process.versions.node'], { encoding: 'utf8' });
98
+ if (result.error || result.status !== 0) return null;
99
+ return String(result.stdout).trim();
100
+ }
101
+
102
+ function supportsMetro(executable: string): boolean {
103
+ const version = readNodeVersion(executable);
104
+ return version !== null && Number.parseInt(version, 10) >= 20;
105
+ }
106
+
107
+ function resolveMetroNode(): string {
108
+ const configured = process.env.EXPO_HARMONY_NODE;
109
+ if (configured) {
110
+ if (supportsMetro(configured)) return configured;
111
+
112
+ throw new Error(
113
+ `EXPO_HARMONY_NODE must point to Node.js 20 or newer; received ${readNodeVersion(configured) || 'an unusable executable'}.`
114
+ );
115
+ }
116
+
117
+ const executable = process.platform === 'win32' ? 'node.exe' : 'node';
118
+ const candidates = (process.env.PATH || '')
119
+ .split(path.delimiter)
120
+ .filter(Boolean)
121
+ .map(directory => path.join(directory, executable));
122
+ const seen = new Set();
123
+
124
+ for (const candidate of [...candidates, process.execPath]) {
125
+ if (seen.has(candidate)) continue;
126
+
127
+ seen.add(candidate);
128
+ if (supportsMetro(candidate)) return candidate;
129
+ }
130
+
131
+ throw new Error(
132
+ 'Release bundling requires Node.js 20 or newer. Set EXPO_HARMONY_NODE to its executable.'
133
+ );
134
+ }
135
+
136
+ function isHermesBytecode(file: string): boolean {
137
+ if (!fs.existsSync(file) || fs.statSync(file).size <= 12) return false;
138
+
139
+ const descriptor = fs.openSync(file, 'r');
140
+
141
+ try {
142
+ const header = Buffer.alloc(8);
143
+ const bytesRead = fs.readSync(descriptor, header, 0, header.length, 0);
144
+
145
+ return bytesRead === header.length && header.toString('hex') === HermesMagic;
146
+ } finally {
147
+ fs.closeSync(descriptor);
148
+ }
149
+ }
150
+
151
+ function assertBundle() {
152
+ if (!isHermesBytecode(EmbeddedBundle)) {
153
+ throw new Error('The prebuilt Harmony bundle is missing or is not Hermes bytecode.');
154
+ }
155
+ }
156
+
157
+ function resolveCliPath(): string {
158
+ try {
159
+ return ProjectRequire.resolve('@expo-harmony/cli/bin/expo-harmony');
160
+ } catch (cause) {
161
+ throw new Error('Release bundling requires the project-local @expo-harmony/cli package.', {
162
+ cause,
163
+ });
164
+ }
165
+ }
166
+
167
+ function bundleRelease() {
168
+ if (process.env.EXPO_HARMONY_BUNDLE_PREBUILT === '1') {
169
+ assertBundle();
170
+ return;
171
+ }
172
+
173
+ const executable = resolveMetroNode();
174
+ const cliPath = resolveCliPath();
175
+
176
+ const result = spawnSync(executable, [cliPath, 'export:embed', ProjectRoot, '--json'], {
177
+ cwd: ProjectRoot,
178
+ encoding: 'utf8',
179
+ env: {
180
+ ...process.env,
181
+ EXPO_METRO_TARGET: 'harmony',
182
+ HERMES_V1_ENABLED: 'true',
183
+ },
184
+ maxBuffer: 64 * 1024 * 1024,
185
+ });
186
+
187
+ if (result.error) {
188
+ throw new Error(result.error.message, { cause: result.error });
189
+ }
190
+
191
+ if (result.status !== 0) {
192
+ const diagnostics = (result.stderr || result.stdout || '').slice(-4000);
193
+
194
+ throw new Error(
195
+ `Harmony bundle failed with exit code ${result.status ?? 'unknown'}.\n${diagnostics}`
196
+ );
197
+ }
198
+
199
+ assertBundle();
200
+ }
201
+
202
+ const BundlerPlugin = {
203
+ pluginId: 'expo-harmony-bundler',
204
+ apply(node) {
205
+ hvigor.nodesEvaluated(() => {
206
+ const appContext = node.getContext(OhosPluginId.OHOS_APP_PLUGIN);
207
+ const buildMode = appContext.getBuildMode();
208
+
209
+ if (buildMode !== 'release') stashBundle();
210
+
211
+ node.subNodes((moduleNode) => {
212
+ const hapContext = moduleNode.getContext(OhosPluginId.OHOS_HAP_PLUGIN);
213
+
214
+ hapContext?.targets((target) => {
215
+ const targetName = target.getTargetName();
216
+
217
+ moduleNode.registerTask({
218
+ name: `${targetName}@ExpoHarmonyBundle`,
219
+ run: () => {
220
+ if (buildMode === 'release') bundleRelease();
221
+ },
222
+ dependencies: [`${targetName}@ProcessResource`],
223
+ postDependencies: [`${targetName}@CompileResource`],
224
+ });
225
+ });
226
+ });
227
+ });
228
+ },
229
+ };
230
+
231
+ export default {
232
+ system: appTasks,
233
+ plugins: [
234
+ createRNOHProjectPlugin({
235
+ nodeModulesPath: NodeModules,
236
+ bundler: { enabled: false },
237
+ }),
238
+ BundlerPlugin,
239
+ ],
240
+ };
@@ -0,0 +1,45 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import { fingerprintHarmonyNativeInputsSync } from '@expo-harmony/config-plugins/native-inputs';
6
+
7
+ const StampPath = '.hvigor/expo-harmony/native-inputs.cmake';
8
+
9
+ function readFileIfPresent(file: string): Buffer | null {
10
+ try {
11
+ return fs.readFileSync(file);
12
+ } catch (error: unknown) {
13
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') return null;
14
+
15
+ throw new Error(error instanceof Error ? error.message : String(error), { cause: error });
16
+ }
17
+ }
18
+
19
+ export function writeStamp(projectRoot: string, harmonyRoot: string): void {
20
+ const { artifactCount, fingerprint } = fingerprintHarmonyNativeInputsSync({
21
+ lockfile: path.join(harmonyRoot, 'oh-package-lock.json5'),
22
+ manifest: path.join(harmonyRoot, 'oh-package.json5'),
23
+ projectRoot,
24
+ });
25
+ const file = path.join(harmonyRoot, ...StampPath.split('/'));
26
+ const content = [
27
+ '# Generated by @expo-harmony/template. Do not edit.',
28
+ `# Native HAR artifacts: ${artifactCount}`,
29
+ `add_compile_definitions(EXPO_HARMONY_NATIVE_INPUTS_SHA256_${fingerprint}=1)`,
30
+ '',
31
+ ].join('\n');
32
+
33
+ const current = readFileIfPresent(file);
34
+ if (current?.toString('utf8') === content) return;
35
+
36
+ fs.mkdirSync(path.dirname(file), { recursive: true });
37
+ const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
38
+
39
+ try {
40
+ fs.writeFileSync(temp, content);
41
+ fs.renameSync(temp, file);
42
+ } finally {
43
+ if (fs.existsSync(temp)) fs.rmSync(temp, { force: true });
44
+ }
45
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "modelVersion": "5.0.0",
3
+ "name": "com.placeholder.app",
4
+ "version": "1.0.0",
5
+ "license": "MIT",
6
+ "dependencies": {
7
+ "@rnoh/react-native-openharmony": "0.84.1"
8
+ },
9
+ "overrides": {
10
+ "@rnoh/react-native-openharmony": "0.84.1"
11
+ }
12
+ }
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ module.exports = path.join(__dirname, 'harmony');
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@expo-harmony/template",
3
+ "version": "55.0.0-harmony.0",
4
+ "keywords": [
5
+ "react-native",
6
+ "expo",
7
+ "expo-harmony",
8
+ "harmonyos"
9
+ ],
10
+ "homepage": "https://github.com/renbaoshuo/expo-harmony/tree/master/packages/template#readme",
11
+ "bugs": "https://github.com/renbaoshuo/expo-harmony/issues",
12
+ "author": "Baoshuo <i@baoshuo.ren>",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/renbaoshuo/expo-harmony.git",
17
+ "directory": "packages/template"
18
+ },
19
+ "main": "index.js",
20
+ "scripts": {
21
+ "start": "expo start --dev-client",
22
+ "harmony": "expo-harmony run",
23
+ "harmony:doctor": "expo-harmony doctor",
24
+ "harmony:export": "expo-harmony export:embed",
25
+ "harmony:prebuild": "expo-harmony prebuild"
26
+ },
27
+ "dependencies": {
28
+ "@babel/core": "7.29.7",
29
+ "@expo-harmony/config-plugins": "55.0.10-harmony.1",
30
+ "@expo-harmony/expo-modules-core": "55.0.25-harmony.1",
31
+ "@expo/metro-config": "55.0.26",
32
+ "@expo/metro-runtime": "55.0.12",
33
+ "@react-native-oh/react-native-harmony": "0.84.1",
34
+ "@react-native-oh/react-native-harmony-cli": "0.84.1",
35
+ "expo": "55.0.26",
36
+ "expo-modules-core": "55.0.25",
37
+ "metro": "0.83.3",
38
+ "metro-config": "0.83.7",
39
+ "react": "19.2.3",
40
+ "react-dom": "19.2.3",
41
+ "react-native": "0.84.1"
42
+ },
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }