diff --git a/OAT.xml b/OAT.xml index 1f95471cda7fa07fd76817a76422b7ab26f3612a..0641c6980ad9000b0f92bed40703e73e921c007c 100644 --- a/OAT.xml +++ b/OAT.xml @@ -52,8 +52,11 @@ - + + + + diff --git a/example/.eslintrc b/example/.eslintrc new file mode 100644 index 0000000000000000000000000000000000000000..8f60824513bdc26c2b115f2441d2d9726562e0ca --- /dev/null +++ b/example/.eslintrc @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +{ + "extends": "@react-native", + "rules": { + "react-native/no-inline-styles": "off", + "react/no-unstable-nested-components": "off", + "react/react-in-jsx-scope": "off", + "@typescript-eslint/no-unused-vars": "warn", + "react-hooks/exhaustive-deps": "off", + "radix": "off", + "prettier/prettier": "warn", + "max-lines": "off" + } +} \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..7613cdedd309866629ce9ae75eb3855ea0849f0a --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,71 @@ +.husky +bundle.harmony.js +package-lock.json +*.hbc +lintCppResult.txt + +# --- + +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore +BuildProfile.ets + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +# Bundle artifact +*.jsbundle + +# Ruby / CocoaPods +/vendor/bundle/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* +*.htrace \ No newline at end of file diff --git a/example/.node-version b/example/.node-version new file mode 100644 index 0000000000000000000000000000000000000000..0b340785231a7dd0a43f008cbcf785d91d33b253 --- /dev/null +++ b/example/.node-version @@ -0,0 +1,6 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +18 diff --git a/example/.prettierrc.js b/example/.prettierrc.js new file mode 100644 index 0000000000000000000000000000000000000000..c24038f10c7cda3754df319c07fa01ac9511b186 --- /dev/null +++ b/example/.prettierrc.js @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +module.exports = { + arrowParens: 'avoid', + bracketSameLine: true, + bracketSpacing: true, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/example/.watchmanconfig b/example/.watchmanconfig new file mode 100644 index 0000000000000000000000000000000000000000..c8817eae530b549f6ec94218880ce380e5e766ac --- /dev/null +++ b/example/.watchmanconfig @@ -0,0 +1,6 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +{} \ No newline at end of file diff --git a/example/app.json b/example/app.json new file mode 100644 index 0000000000000000000000000000000000000000..7586f6fa99a6680a35a27fac7255ba721880d461 --- /dev/null +++ b/example/app.json @@ -0,0 +1,4 @@ +{ + "name": "app_name", + "displayName": "tester" +} \ No newline at end of file diff --git a/example/babel.config.js b/example/babel.config.js new file mode 100644 index 0000000000000000000000000000000000000000..e4b7261456052a5c3ce488bbe9116941473394e7 --- /dev/null +++ b/example/babel.config.js @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], + plugins: [ + ], +}; diff --git a/example/contexts.ts b/example/contexts.ts new file mode 100644 index 0000000000000000000000000000000000000000..3caf29e5e119971c4813fba75201758cf1c17fe1 --- /dev/null +++ b/example/contexts.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +import React from 'react'; + +export const AppParamsContext = React.createContext(undefined); diff --git a/example/harmony/.gitignore b/example/harmony/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..72d73fb4b6240ccd907882c931ce08ee45ca62ba --- /dev/null +++ b/example/harmony/.gitignore @@ -0,0 +1,24 @@ +# it may cause some issues when building the project when switching branches +package-lock.json +# we add this because we want to keep the signing configs out of git +/build-profile.json5 + +rnoh_modules + +**/oh-package-lock.json5 +# --- + +/node_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +/oh_modules +hvigorw +hvigorw.bat +hvigor/hvigor-wrapper.js + diff --git a/example/harmony/AppScope/app.json5 b/example/harmony/AppScope/app.json5 new file mode 100644 index 0000000000000000000000000000000000000000..058bd391dac60928a1857aa31e4b0f712a7bc743 --- /dev/null +++ b/example/harmony/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.harmony.wechat.lib.demo", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} diff --git a/example/harmony/AppScope/resources/base/element/string.json b/example/harmony/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..698a720065342ae0dadad63eb87d45fec8725f36 --- /dev/null +++ b/example/harmony/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "RN Tester" + } + ] +} diff --git a/example/harmony/AppScope/resources/base/media/app_icon.png b/example/harmony/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce307a8827bd75456441ceb57d530e4c8d45d36c Binary files /dev/null and b/example/harmony/AppScope/resources/base/media/app_icon.png differ diff --git a/example/harmony/build-profile.template.json5 b/example/harmony/build-profile.template.json5 new file mode 100644 index 0000000000000000000000000000000000000000..9d7ded5d587232b1496cff5aeb442dee0dc2cd14 --- /dev/null +++ b/example/harmony/build-profile.template.json5 @@ -0,0 +1,40 @@ +{ + "app": { + "products": [ + { + "name": 'default', + "signingConfig": 'default', + "compileSdkVersion": '5.0.1(13)', + "compatibleSdkVersion": '5.0.0(12)', + "runtimeOS": 'HarmonyOS' + }, + ], + "buildModeSet": [ + { + "name": 'debug', + }, + { + "name": 'release', + }, + ], + "signingConfigs": [] + }, + "modules": [ + { + "name": 'entry', + "srcPath": './entry', + "targets": [ + { + "name": 'default', + "applyToProducts": [ + 'default' + ], + }, + ], + }, + { + "name": "toolbar_android", + "srcPath": '../../harmony/toolbar_android' + } + ], +} \ No newline at end of file diff --git a/example/harmony/codelinter.json b/example/harmony/codelinter.json new file mode 100644 index 0000000000000000000000000000000000000000..e7f91acb037aeda939967a042f16033f4c6e21b9 --- /dev/null +++ b/example/harmony/codelinter.json @@ -0,0 +1,32 @@ +{ + "files": ["**/*.ts", "**/*.ets"], + "ignore": [ + "**/ohosTest/**/*", + "**/node_modules/**/*", + "**/hvigorfile.ts", + "**/node_modules/**/*", + "**/oh_modules/**/*", + "**/build/**/*", + "**/.preview/**/*" + ], + "plugins": ["@typescript-eslint"], + "ruleSet": [], + "rules": { + "@typescript-eslint/await-thenable": "warn", + "@typescript-eslint/consistent-type-imports": "warn", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/explicit-module-boundary-types": "warn", + "@typescript-eslint/no-dynamic-delete": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-for-in-array": "warn", + "@typescript-eslint/no-this-alias": "warn", + "@typescript-eslint/no-unnecessary-type-constraint": "warn", + "@typescript-eslint/no-unsafe-argument": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/no-unsafe-return": "warn", + "@typescript-eslint/prefer-literal-enum-member": "warn" + }, + "overrides": [] +} diff --git a/example/harmony/entry/.gitignore b/example/harmony/entry/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..bbc049cfeb689c61d6c2922810f652cbb19aad21 --- /dev/null +++ b/example/harmony/entry/.gitignore @@ -0,0 +1,7 @@ +/node_modules +/.preview +/build +/.cxx +package-lock.json +/oh_modules +/assets diff --git a/example/harmony/entry/build-profile.json5 b/example/harmony/entry/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..2408c163287e825139a36f35cbec83e9772c49db --- /dev/null +++ b/example/harmony/entry/build-profile.json5 @@ -0,0 +1,19 @@ +{ + "apiType": 'stageMode', + "buildOption": { + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "cppFlags": "-s", + }, + }, + "targets": [ + { + "name": "default", + "runtimeOS": "HarmonyOS" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/example/harmony/entry/hvigorfile.ts b/example/harmony/entry/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..b56e312a0678edd7005549245f0c148b446b750b --- /dev/null +++ b/example/harmony/entry/hvigorfile.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +export { hapTasks } from '@ohos/hvigor-ohos-plugin'; diff --git a/example/harmony/entry/oh-package.json5 b/example/harmony/entry/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..a4f6974dd9939b313e19033a8a9b2548b5f3e9bf --- /dev/null +++ b/example/harmony/entry/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "license": "ISC", + "devDependencies": {}, + "name": "entry", + "description": "example description", + "version": "1.0.0", + "dependencies": { + "@rnoh/react-native-openharmony": "0.72.38", + "@react-native-ohos/toolbar-android": "file:../../../harmony/toolbar_android" + } +} \ No newline at end of file diff --git a/example/harmony/entry/src/main/cpp/.gitignore b/example/harmony/entry/src/main/cpp/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..881dde5479f322de9db2b2bb98fd94a95f73715e --- /dev/null +++ b/example/harmony/entry/src/main/cpp/.gitignore @@ -0,0 +1,2 @@ +jsbundle.h +generated/ diff --git a/example/harmony/entry/src/main/cpp/CMakeLists.txt b/example/harmony/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..c22d5bfa542a9f6bdf89c6e3a519433856c9cca0 --- /dev/null +++ b/example/harmony/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,34 @@ +project(rnapp) +cmake_minimum_required(VERSION 3.4.1) +set(CMAKE_SKIP_BUILD_RPATH TRUE) +set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules") +set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules") +set(RNOH_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules/@rnoh/react-native-openharmony/src/main/cpp") +set(RNOH_GENERATED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/generated") +set(LOG_VERBOSITY_LEVEL 1) +set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments") +set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie") +set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules") + +set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use +add_compile_definitions(WITH_HITRACE_SYSTRACE) + +add_subdirectory("${RNOH_CPP_DIR}" ./rn) + +# RNOH_BEGIN: manual_package_linking_1 +add_subdirectory("${OH_MODULES}/@react-native-ohos/toolbar-android/src/main/cpp" ./toolbar-android) +# RNOH_END: manual_package_linking_1 + +file(GLOB GENERATED_CPP_FILES "${CMAKE_CURRENT_SOURCE_DIR}/generated/*.cpp") # this line is needed by codegen v1 + +add_library(rnoh_app SHARED + ${GENERATED_CPP_FILES} + "./PackageProvider.cpp" + "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp" +) +target_link_libraries(rnoh_app PUBLIC rnoh) + +# RNOH_BEGIN: manual_package_linking_2 +target_link_libraries(rnoh_app PUBLIC rnoh_toolbar_android) +# RNOH_END: manual_package_linking_2 diff --git a/example/harmony/entry/src/main/cpp/PackageProvider.cpp b/example/harmony/entry/src/main/cpp/PackageProvider.cpp new file mode 100644 index 0000000000000000000000000000000000000000..98be431a60c47a79d00d97d7159a3947621bf96c --- /dev/null +++ b/example/harmony/entry/src/main/cpp/PackageProvider.cpp @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +#include "RNOH/PackageProvider.h" +#include "generated/RNOHGeneratedPackage.h" +#include "ToolbarAndroidPackage.h" + +using namespace rnoh; + +std::vector> PackageProvider::getPackages(Package::Context ctx) +{ + return { + std::make_shared(ctx), // generated by codegen v1 + std::make_shared(ctx), + }; +} \ No newline at end of file diff --git a/example/harmony/entry/src/main/ets/RNPackagesFactory.ets b/example/harmony/entry/src/main/ets/RNPackagesFactory.ets new file mode 100644 index 0000000000000000000000000000000000000000..950ceea7d055329d3179c457628adff942609155 --- /dev/null +++ b/example/harmony/entry/src/main/ets/RNPackagesFactory.ets @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +import type {RNPackageContext, RNPackage} from '@rnoh/react-native-openharmony/ts'; +import {ToolbarAndroidPackage} from '@react-native-ohos/toolbar-android/src/main/ets/ToolbarAndroidPackage'; + +export function createRNPackages(ctx: RNPackageContext): RNPackage[] { + return [ + new ToolbarAndroidPackage(ctx) + ]; +} diff --git a/example/harmony/entry/src/main/ets/assets/fonts/Pacifico-Regular.ttf b/example/harmony/entry/src/main/ets/assets/fonts/Pacifico-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e7def95d3f44c82086f6e74d93fc0aadac7c454a Binary files /dev/null and b/example/harmony/entry/src/main/ets/assets/fonts/Pacifico-Regular.ttf differ diff --git a/example/harmony/entry/src/main/ets/assets/fonts/StintUltraCondensed-Regular.ttf b/example/harmony/entry/src/main/ets/assets/fonts/StintUltraCondensed-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..25c749e167bd5beef18360b13ae660f1c10ebc62 Binary files /dev/null and b/example/harmony/entry/src/main/ets/assets/fonts/StintUltraCondensed-Regular.ttf differ diff --git a/example/harmony/entry/src/main/ets/entryability/EntryAbility.ets b/example/harmony/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..35746494ac1dda87b830c77681493aed4f389b61 --- /dev/null +++ b/example/harmony/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +import {RNAbility} from '@rnoh/react-native-openharmony'; +import { AbilityConstant, Want } from '@kit.AbilityKit'; + +export default class EntryAbility extends RNAbility { + + onCreate(want: Want) { + super.onCreate(want) + } + + getPagePath() { + return 'pages/Index'; + } +} diff --git a/example/harmony/entry/src/main/ets/pages/Index.ets b/example/harmony/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..2b505979957ca67c58501f83697e09196a88e35f --- /dev/null +++ b/example/harmony/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +import { + AnyJSBundleProvider, + ComponentBuilderContext, + FileJSBundleProvider, + MetroJSBundleProvider, + ResourceJSBundleProvider, + RNApp, + RNOHErrorDialog, + RNOHLogger, + TraceJSBundleProviderDecorator, + RNOHCoreContext +} from '@rnoh/react-native-openharmony'; +import font from '@ohos.font'; +import { createRNPackages } from '../RNPackagesFactory'; +import { RNCToolbarAndroid } from '@react-native-ohos/toolbar-android/src/main/ets/RNCToolbarAndroid' + +const arkTsComponentNames: Array = [RNCToolbarAndroid.NAME]; + +@Builder +export function buildCustomRNComponent(ctx: ComponentBuilderContext) { + // There seems to be a problem with the placement of ArkTS components in mixed mode. Nested Stack temporarily avoided. + Stack() { + if (ctx.componentName === RNCToolbarAndroid.NAME) { + RNCToolbarAndroid({ + ctx: ctx.rnComponentContext, + tag: ctx.tag + }) + } + } + .position({ x: 0, y: 0 }) + +} + +const wrappedCustomRNComponentBuilder = wrapBuilder(buildCustomRNComponent) + +/** + * If you want to use custom fonts, you need to register them here. + * We should support react-native-asset to handle registering fonts automatically. + */ +const fonts: font.FontOptions[] = [ + { + familyName: 'Pacifico-Regular', + familySrc: '/assets/fonts/Pacifico-Regular.ttf' + }, + { + familyName: 'StintUltraCondensed-Regular', + familySrc: '/assets/fonts/StintUltraCondensed-Regular.ttf' + } +] + +@Entry +@Component +struct Index { + @StorageLink('RNOHCoreContext') private rnohCoreContext: RNOHCoreContext | undefined = undefined + @State shouldShow: boolean = false + private logger!: RNOHLogger + bundlePath: string = 'bunlde.harmony.js' + @State hasBundle: boolean = false + + aboutToAppear() { + this.logger = this.rnohCoreContext!.logger.clone("Index") + const stopTracing = this.logger.clone("aboutToAppear").startTracing() + for (const customFont of fonts) { + font.registerFont(customFont) + } + + this.shouldShow = true + stopTracing() + } + + onBackPress(): boolean | undefined { + // NOTE: this is required since `Ability`'s `onBackPressed` function always + // terminates or puts the app in the background, but we want Ark to ignore it completely + // when handled by RN + this.rnohCoreContext!.dispatchBackPress() + return true + } + + build() { + Column() { + if (this.rnohCoreContext && this.shouldShow) { + if (this.rnohCoreContext?.isDebugModeEnabled) { + RNOHErrorDialog({ ctx: this.rnohCoreContext }) + } + RNApp({ + rnInstanceConfig: { + createRNPackages, + enableNDKTextMeasuring: true, + enableBackgroundExecutor: false, + enableCAPIArchitecture: true, + arkTsComponentNames: arkTsComponentNames, + }, + initialProps: { "foo": "bar" } as Record, + appKey: "app_name", + wrappedCustomRNComponentBuilder: wrappedCustomRNComponentBuilder, + onSetUp: (rnInstance) => { + rnInstance.enableFeatureFlag("ENABLE_RN_INSTANCE_CLEAN_UP") + }, + jsBundleProvider: new TraceJSBundleProviderDecorator( + new AnyJSBundleProvider([ + new MetroJSBundleProvider(), + // NOTE: to load the bundle from file, place it in + // `/data/app/el2/100/base/com.rnoh.tester/files/bundle.harmony.js` + // on your device. The path mismatch is due to app sandboxing on HarmonyOS + new FileJSBundleProvider('/data/storage/el2/base/files/bundle.harmony.js'), + // new FileJSBundleProvider(context.filesDir + '/' + this.bundlePath), + new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'hermes_bundle.hbc'), + new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'bundle.harmony.js') + ]), + this.rnohCoreContext.logger), + }) + } + Text("1233333333122 1212323") + } + .height('100%') + .width('100%') + } +} diff --git a/example/harmony/entry/src/main/ets/pages/SurfaceDeadlockTest.ets b/example/harmony/entry/src/main/ets/pages/SurfaceDeadlockTest.ets new file mode 100644 index 0000000000000000000000000000000000000000..88e4439681c4981d7ab7e1b87a46653a9e5a9c35 --- /dev/null +++ b/example/harmony/entry/src/main/ets/pages/SurfaceDeadlockTest.ets @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +import { RNInstance, JSBundleProvider, RNAbility, RNSurface } from '@rnoh/react-native-openharmony' +import { CustomComponentBuilder } from "@rnoh/react-native-openharmony/src/main/ets/RNOHCorePackage" +import { SurfaceConfig2 } from '@rnoh/react-native-openharmony/src/main/ets/RNSurface' + + +@Component +export struct SurfaceDeadlockTest { + public jsBundleProvider: JSBundleProvider | undefined = undefined + public appKeys: string[] = [] + public numberOfIterations: number = 1 + @BuilderParam public buildCustomComponent!: CustomComponentBuilder + // ------------------------------------------------------------------------------------------------------------------- + @StorageLink('RNAbility') private rnAbility: RNAbility = {} as RNAbility + private rnInstance!: RNInstance + @State private shouldShow: boolean = false + private shouldDestroyRNInstance: boolean = false + private cleanUpCallbacks: (() => void)[] = [] + + aboutToAppear() { + this.getOrCreateRNInstance().then(rnInstance => { + this.rnInstance = rnInstance + const jsBundleExecutionStatus = this.rnInstance.getBundleExecutionStatus(this.jsBundleProvider?.getURL()) + if (this.jsBundleProvider && jsBundleExecutionStatus === undefined) { + this.rnInstance.runJSBundle(this.jsBundleProvider).then(() => { + this.shouldShow = true + }) + return; + } + }).catch((reason: string | Error) => { + if (typeof reason === "string") + this.rnAbility.getLogger().error(reason) + else if (reason instanceof Error) { + this.rnAbility.getLogger().error(reason.message) + } else { + this.rnAbility.getLogger().error("Fatal exception") + } + }) + } + + aboutToDisappear() { + if (this.shouldDestroyRNInstance) + this.rnAbility.destroyAndUnregisterRNInstance(this.rnInstance) + this.cleanUpCallbacks.forEach(cleanUp => cleanUp()) + } + + private getOrCreateRNInstance(): Promise { + return this.rnAbility.createAndRegisterRNInstance({ createRNPackages: () => [] }) + } + + build() { + Stack() { + if (this.shouldShow) { + ForEach(this.appKeys, (appKey: string, idx) => { + Stack() { + Blinker({ + minDelayInMs: 1000, + maxDelayInMs: 2000, + blinksCount: this.numberOfIterations, + randomnessPrecisionInMs: 500 + }) { + RNSurface({ + ctx: this.rnAbility.createRNOHContext({ rnInstance: this.rnInstance }), + surfaceConfig: { + initialProps: {}, + appKey: appKey, + } as SurfaceConfig2, + buildCustomComponent: this.buildCustomComponent, + }) + } + }.height(`${100 / this.appKeys.length}%`) + .position({ x: 0, y: `${(idx / this.appKeys.length) * 100}%` }) + }) + } + }.width("100%") + .height("100%") + } +} + + +@Component +struct Blinker { + public minDelayInMs: number = 0 + public maxDelayInMs: number = 1000 + public blinksCount: number = 0 + public randomnessPrecisionInMs: number = 250 + @BuilderParam public renderChildren: () => void + private currentBlinksCount = 0 + @State private isVisible: boolean = false + private timeout: number = 0 + + aboutToAppear() { + this.blink(this.minDelayInMs) + } + + aboutToDisappear() { + clearTimeout(this.timeout) + } + + private blink(ms: number) { + this.isVisible = !this.isVisible + this.currentBlinksCount += 1 + if (this.currentBlinksCount >= this.blinksCount) { + if (this.timeout) { + clearTimeout(this.timeout) + } + this.isVisible = true + return; + } + this.timeout = setTimeout(() => { + this.blink(this.getNextDelay()) + }, ms) + } + + private getNextDelay(): number { + return ((Math.floor(Math.random() * (Number.MAX_VALUE / this.randomnessPrecisionInMs)) * this.randomnessPrecisionInMs) % this.maxDelayInMs) + this.minDelayInMs + } + + build() { + Stack() { + if (this.isVisible) { + this.renderChildren() + } + } + } +} + + + + + diff --git a/example/harmony/entry/src/main/ets/pages/TouchDisplayer.ets b/example/harmony/entry/src/main/ets/pages/TouchDisplayer.ets new file mode 100644 index 0000000000000000000000000000000000000000..1db115688b6933f71717001bd6307403d31cfcf0 --- /dev/null +++ b/example/harmony/entry/src/main/ets/pages/TouchDisplayer.ets @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +@Component +export struct TouchDisplayer { + @State currentTouches: TouchObject[] = [] + @State touchIndicatorOpacity: number = 0 + @BuilderParam buildChildren: () => void + build() { + Stack() { + this.buildChildren() + ForEach(this.currentTouches, (activeTouch: TouchObject) => { + Stack() { + } + .width(64) + .height(64) + .backgroundColor("blue") + .borderWidth(2) + .borderColor("white") + .opacity(this.touchIndicatorOpacity) + .position({ x: activeTouch.x - 32, y: activeTouch.y - 32 }) + .borderRadius(1000) + .hitTestBehavior(HitTestMode.Transparent) + }) + } + .width("100%") + .height("100%") + .hitTestBehavior(HitTestMode.Transparent) + .onTouch(e => { + this.currentTouches = e.touches + this.touchIndicatorOpacity = 0.5 + animateTo({ + duration: 500, + curve: Curve.Linear, + }, () => { + this.touchIndicatorOpacity = 0 + }) + }) + } +} \ No newline at end of file diff --git a/example/harmony/entry/src/main/module.json5 b/example/harmony/entry/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..c7700c2c5f348cd7d73ed4cf3b8d7112a46295b9 --- /dev/null +++ b/example/harmony/entry/src/main/module.json5 @@ -0,0 +1,52 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "default" + ], + "querySchemes": [ + "weixin", + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + // below property is supported from 5.0.0 - it is needed by bundleManager.canOpenLink to check if the app can open some url + // "querySchemes": ["maps", "http", "https", "customDomain"], + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET" + }, + ], + "metadata": [ + { + "name": "OPTLazyForEach", + "value": "true", + } + ], + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:icon", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "visible": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/example/harmony/entry/src/main/resources/.gitignore b/example/harmony/entry/src/main/resources/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..037cea9e70bef8c906560a871a394f748ec2325d --- /dev/null +++ b/example/harmony/entry/src/main/resources/.gitignore @@ -0,0 +1 @@ +rawfile/assets diff --git a/example/harmony/entry/src/main/resources/base/element/color.json b/example/harmony/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..3c712962da3c2751c2b9ddb53559afcbd2b54a02 --- /dev/null +++ b/example/harmony/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/example/harmony/entry/src/main/resources/base/element/string.json b/example/harmony/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..074b2173f51352f71629252715526554911d44bb --- /dev/null +++ b/example/harmony/entry/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "RN Tester" + } + ] +} \ No newline at end of file diff --git a/example/harmony/entry/src/main/resources/base/media/icon.png b/example/harmony/entry/src/main/resources/base/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c0f05abe7eb110a3c4958a48f71ea15b28bae5d3 Binary files /dev/null and b/example/harmony/entry/src/main/resources/base/media/icon.png differ diff --git a/example/harmony/entry/src/main/resources/base/profile/main_pages.json b/example/harmony/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..1898d94f58d6128ab712be2c68acc7c98e9ab9ce --- /dev/null +++ b/example/harmony/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/example/harmony/entry/src/main/resources/rawfile/1.txt b/example/harmony/entry/src/main/resources/rawfile/1.txt new file mode 100644 index 0000000000000000000000000000000000000000..71f6ee3dfcd13fad21fc73e697c7c5a3c31ec039 --- /dev/null +++ b/example/harmony/entry/src/main/resources/rawfile/1.txt @@ -0,0 +1 @@ +text test \ No newline at end of file diff --git a/example/harmony/format.ps1 b/example/harmony/format.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..91e4a497c05c602cb9cc478420df1537c9231359 --- /dev/null +++ b/example/harmony/format.ps1 @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +$directoryPath = Split-Path -Parent $MyInvocation.MyCommand.Path +$filePaths = Get-ChildItem $directoryPath -Recurse -Include *.h, *.cpp | +Where-Object { + $_.DirectoryName -notmatch 'third-party' -and + $_.DirectoryName -notmatch 'patches' -and + $_.DirectoryName -notmatch 'node_modules' -and + $_.DirectoryName -notmatch '.cxx' -and + $_.DirectoryName -notmatch 'build' +} +foreach ($filePath in $filePaths) { + & "clang-format.exe" -style=file -i $filePath.FullName +} \ No newline at end of file diff --git a/example/harmony/hvigor/.gitignore b/example/harmony/hvigor/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..aa1ec1ea0618124672d14a63d00d943240f2db2a --- /dev/null +++ b/example/harmony/hvigor/.gitignore @@ -0,0 +1 @@ +*.tgz diff --git a/example/harmony/hvigor/hvigor-config.json5 b/example/harmony/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..c8ba182673e80fbc3da9e614580e9453b718cd3b --- /dev/null +++ b/example/harmony/hvigor/hvigor-config.json5 @@ -0,0 +1,21 @@ +{ + "modelVersion": "5.0.0", + "dependencies": { + }, + "execution": { + // "analyze": "default", /* Define the build analyze mode. Value: [ "default" | "verbose" | false ]. Default: "default" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 4096 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process */ + } +} \ No newline at end of file diff --git a/example/harmony/hvigorfile.ts b/example/harmony/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..82487ef3d53f08f2dc584ed5c8f149c884f0d0a9 --- /dev/null +++ b/example/harmony/hvigorfile.ts @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +export { appTasks } from '@ohos/hvigor-ohos-plugin'; \ No newline at end of file diff --git a/example/harmony/oh-package.json5 b/example/harmony/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..fe6ab68c076ba72f6fc67f919699ff0862be86b2 --- /dev/null +++ b/example/harmony/oh-package.json5 @@ -0,0 +1,12 @@ +{ + "modelVersion": "5.0.0", + "license": "ISC", + "name": "rnoh", + "description": "example description", + "repository": {}, + "version": "1.0.0", + "overrides": { + "@rnoh/react-native-openharmony": "0.72.38" + }, + "dynamicDependencies": {} +} \ No newline at end of file diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000000000000000000000000000000000000..55bc3951b26389c19c85bc6f4d146e91ceaba5a4 --- /dev/null +++ b/example/index.js @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +import {AppRegistry, View, Text} from 'react-native'; +import {name as appName} from './app.json'; +import App from './src/ToolbarAndroidDemo'; + +AppRegistry.registerComponent(appName, () => App); \ No newline at end of file diff --git a/example/jest.config.js b/example/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..96cf4a6510311b69ccfeff435d04129c1ce5a06f --- /dev/null +++ b/example/jest.config.js @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/example/metro.config.js b/example/metro.config.js new file mode 100644 index 0000000000000000000000000000000000000000..41347609efbd0402c61ea2212f8d8a01ef2c7761 --- /dev/null +++ b/example/metro.config.js @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +const {mergeConfig, getDefaultConfig} = require('@react-native/metro-config'); +const {createHarmonyMetroConfig} = require('react-native-harmony/metro.config'); + +/** + * @type {import("metro-config").ConfigT} + */ +const config = { + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + }, +}; + +module.exports = mergeConfig( + getDefaultConfig(__dirname), + createHarmonyMetroConfig({ + reactNativeHarmonyPackageName: 'react-native-harmony', + }), + config, +); diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000000000000000000000000000000000000..d1a88dcf5abe3abf1bc8d3c9b9209ac95122e5ec --- /dev/null +++ b/example/package.json @@ -0,0 +1,66 @@ +{ + "name": "react-native-harmony-tester", + "version": "1.0.0", + "private": true, + "scripts": { + "i": "cd ../ && npm i && npm run pack && cd ./example && npm i", + "reStart": "npm run install:pkg && npm run codegen && hdc rport tcp:8081 tcp:8081 && react-native start", + "start": "hdc rport tcp:8081 tcp:8081 && react-native start", + "codegen": "react-native codegen-harmony --rnoh-module-path ./harmony/entry/oh_modules/@rnoh/react-native-openharmony", + "pack:pkg": "cd ../ && npm pack && cd ./example", + "install:pkg": "npm uninstall @react-native-ohos/toolbar-android && npm run pack:pkg && npm i @react-native-ohos/toolbar-android@file:../react-native-ohos-toolbar-android-0.2.2-rc.1.tgz", + "dev": "npm run codegen && react-native bundle-harmony --dev --minify=false", + "prod": "npm run codegen && react-native bundle-harmony --dev=false --minify=true", + "postinstall": "node ./scripts/create-build-profile", + "fast:pkg": "cd ../ && npm i --legacy-peer-deps && npm pack && cd ./example && npm i --legacy-peer-deps && npm run dev" + }, + "dependencies": { + "@gorhom/portal": "^1.0.14", + "@react-native-ohos/toolbar-android": "file:../react-native-ohos-toolbar-android-0.2.2-rc.1.tgz", + "react": "18.2.0", + "react-native": "0.72.5", + "react-native-harmony": "npm:@react-native-oh/react-native-harmony@^0.72.32" + }, + "devDependencies": { + "@babel/core": "^7.20.0", + "@babel/preset-env": "^7.20.0", + "@babel/runtime": "^7.20.0", + "@react-native-community/eslint-config": "^3.2.0", + "@react-native/eslint-config": "^0.74.0", + "@react-native/metro-config": "^0.72.6", + "@tsconfig/react-native": "^2.0.2", + "@types/chai": "^4.3.4", + "@types/d3-scale-chromatic": "^3.0.0", + "@types/fs-extra": "^11.0.1", + "@types/jest": "^29.5.5", + "@types/metro-config": "^0.76.2", + "@types/react": "17.0.14", + "@types/react-dom": "17.0.14", + "@types/react-test-renderer": "^18.0.0", + "babel-jest": "^29.2.1", + "csv-parser": "^3.0.0", + "eslint": "^8.19.0", + "eslint-plugin-prettier": "^5.0.1", + "fs-extra": "^11.1.1", + "husky": "^8.0.3", + "jest": "^29.7.0", + "json5": "^2.2.3", + "metro": "^0.76.3", + "metro-config": "^0.76.3", + "metro-react-native-babel-preset": "0.73.9", + "prettier": "3.2.4", + "react-test-renderer": "18.2.0", + "simple-statistics": "^7.8.3", + "ts-jest": "^29.1.1", + "typescript": "^5.3.2", + "yargs": "^17.7.2", + "@rnoh/react-native-harmony-cli": "npm:@react-native-oh/react-native-harmony-cli@^0.0.27" + }, + "overrides": { + "@react-native-community/cli": "11.4.1", + "@react-native/codegen": "0.74.0" + }, + "resolutions": { + "@react-native-community/cli": "11.4.1" + } +} diff --git a/example/react-native.config.js b/example/react-native.config.js new file mode 100644 index 0000000000000000000000000000000000000000..25bde474d0a85b6d00842811b3af016f5a355ab0 --- /dev/null +++ b/example/react-native.config.js @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +module.exports = { + project: { + }, + assets: ['./assets/fonts/'], +}; diff --git a/example/scripts/create-build-profile.js b/example/scripts/create-build-profile.js new file mode 100644 index 0000000000000000000000000000000000000000..82433609508e37bbc0e44a0bcbeefa2a41b5b4e6 --- /dev/null +++ b/example/scripts/create-build-profile.js @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +const fs = require('fs'); +const JSON5 = require('json5'); +const path = require('path'); + +const templatePath = path.join( + __dirname, + '..', + 'harmony', + 'build-profile.template.json5', +); +const existingProfilePath = path.join( + __dirname, + '..', + 'harmony', + 'build-profile.json5', +); + +if (fs.existsSync(existingProfilePath)) { + let existingProfile = JSON5.parse( + fs.readFileSync(existingProfilePath, 'utf-8'), + ); + let template = JSON5.parse(fs.readFileSync(templatePath, 'utf-8')); + let signingConfigs = + existingProfile.app && existingProfile.app.signingConfigs; + + existingProfile = {...template}; + + if (signingConfigs) { + existingProfile.app.signingConfigs = signingConfigs; + } + + fs.writeFileSync( + existingProfilePath, + JSON5.stringify(existingProfile, null, 2), + ); +} else { + // File doesn't exist, create a copy from the template + fs.copyFileSync(templatePath, existingProfilePath); +} \ No newline at end of file diff --git a/example/src/ToolbarAndroidDemo.tsx b/example/src/ToolbarAndroidDemo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a649bd3596651806f4c028dec254be3181ac0fa0 --- /dev/null +++ b/example/src/ToolbarAndroidDemo.tsx @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +import React, { useState } from "react"; +import { StyleSheet, View, Text } from "react-native"; +import ToolbarAndroid from "@react-native-community/toolbar-android"; + +function ToolbarAndroidDemo({}): JSX.Element { + const [state, setState] = useState<{ + message?: string; + }>({ + message: undefined, + }); + + const { message } = state; + + return ( + + setState({ message: "Clicked nav icon" })} + onActionSelected={(position: number) => + setState({ message: `Clicked Menu-${position}` }) + } + /> + + + Click nav or action icon will trigger some events! + + {message} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + toolbar: { + backgroundColor: "#E9EAED", + height: 56, + }, + centerContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#F5FCFF", + }, + headText: { + fontSize: 16, + }, + contentText: { + fontSize: 18, + fontWeight: "bold", + color: "#ff681f", + }, +}); + +export default ToolbarAndroidDemo; \ No newline at end of file diff --git a/example/src/assets/app_icon.png b/example/src/assets/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce307a8827bd75456441ceb57d530e4c8d45d36c Binary files /dev/null and b/example/src/assets/app_icon.png differ diff --git a/example/src/assets/icon.png b/example/src/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c0f05abe7eb110a3c4958a48f71ea15b28bae5d3 Binary files /dev/null and b/example/src/assets/icon.png differ diff --git a/example/tsconfig.json b/example/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..9a2c8c222cb885dde1a7a015f7dfe57de4828c85 --- /dev/null +++ b/example/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@tsconfig/react-native/tsconfig.json", + "compilerOptions": { + "jsx": "react-native", + "paths": { + "react-native": [ + "./node_modules/react-native-harmony" + ], + }, + }, + "exclude": [ + "harmony" + ] +} \ No newline at end of file diff --git a/harmony/toolbar_android/oh-package.json5 b/harmony/toolbar_android/oh-package.json5 index d64dbe16c091234c0ed44174e0030dc860a1d146..ae4f4255148d759ba4cb60460e2cc2db854c37b1 100644 --- a/harmony/toolbar_android/oh-package.json5 +++ b/harmony/toolbar_android/oh-package.json5 @@ -6,5 +6,7 @@ "main": "Index.ets", "author": "", "license": "ISC", - "dependencies": {} + "dependencies": { + "@rnoh/react-native-openharmony": "^0.72.38", + } } \ No newline at end of file