diff --git a/.changeset/free-squids-walk.md b/.changeset/free-squids-walk.md new file mode 100644 index 000000000..45cbf9052 --- /dev/null +++ b/.changeset/free-squids-walk.md @@ -0,0 +1,10 @@ +--- +'@rock-js/platform-android': patch +'@rock-js/platform-harmony': patch +'create-rock': patch +'@rock-js/tools': patch +'rock': patch +'rock-docs': patch +--- + +feat: add experimental support for HarmonyOS platform diff --git a/packages/cli/src/lib/plugins/fingerprint.ts b/packages/cli/src/lib/plugins/fingerprint.ts index 40a01760c..ab6d20873 100644 --- a/packages/cli/src/lib/plugins/fingerprint.ts +++ b/packages/cli/src/lib/plugins/fingerprint.ts @@ -33,7 +33,7 @@ function redactSensitiveSources(sources: FingerprintInputHash[]) { } type NativeFingerprintCommandOptions = { - platform: 'ios' | 'android'; + platform: 'ios' | 'android' | 'harmony'; raw?: boolean; }; @@ -44,7 +44,12 @@ export async function nativeFingerprintCommand( ) { validateOptions(options); const platform = options.platform; - const readablePlatformName = platform === 'ios' ? 'iOS' : 'Android'; + const readablePlatformName = + platform === 'ios' + ? 'iOS' + : platform === 'android' + ? 'Android' + : 'HarmonyOS'; if (options.raw || !isInteractive()) { const fingerprint = await nativeFingerprint(path, { @@ -106,12 +111,16 @@ export async function nativeFingerprintCommand( function validateOptions(options: NativeFingerprintCommandOptions) { if (!options.platform) { throw new RockError( - 'The --platform flag is required. Please specify either "ios" or "android".', + 'The --platform flag is required. Please specify either "ios", "android" or "harmony".', ); } - if (options.platform !== 'ios' && options.platform !== 'android') { + if ( + options.platform !== 'ios' && + options.platform !== 'android' && + options.platform !== 'harmony' + ) { throw new RockError( - `Unsupported platform "${options.platform}". Please specify either "ios" or "android".`, + `Unsupported platform "${options.platform}". Please specify either "ios", "android" or "harmony".`, ); } } diff --git a/packages/cli/src/lib/plugins/logConfig.ts b/packages/cli/src/lib/plugins/logConfig.ts index 5a6c5534b..c5b3d30c5 100644 --- a/packages/cli/src/lib/plugins/logConfig.ts +++ b/packages/cli/src/lib/plugins/logConfig.ts @@ -74,7 +74,7 @@ export const logConfigPlugin = options: [ { name: '-p, --platform ', - description: 'Select platform, e.g. ios or android', + description: 'Select platform, e.g. ios, android, or harmony', }, ], }); diff --git a/packages/cli/src/lib/plugins/remoteCache.ts b/packages/cli/src/lib/plugins/remoteCache.ts index 4fc27cb3d..4a0950e61 100644 --- a/packages/cli/src/lib/plugins/remoteCache.ts +++ b/packages/cli/src/lib/plugins/remoteCache.ts @@ -426,13 +426,15 @@ export const remoteCachePlugin = }, { name: '-p, --platform ', - description: 'Select platform, e.g. ios or android', + description: + 'Select platform, e.g. ios, android, or harmony (experimental)', }, { name: '-t, --traits ', description: `Comma-separated traits that construct final artifact name. Traits for Android are: variant; for iOS: destination and configuration. Example iOS: --traits simulator,Release -Example Android: --traits debug`, +Example Android: --traits debug +Example Harmony: --traits debug`, parse: (val: string) => val.split(','), }, { diff --git a/packages/create-app/src/lib/__tests__/bin.test.ts b/packages/create-app/src/lib/__tests__/bin.test.ts index 6dcdefd0d..db767212c 100644 --- a/packages/create-app/src/lib/__tests__/bin.test.ts +++ b/packages/create-app/src/lib/__tests__/bin.test.ts @@ -7,6 +7,7 @@ test('should format config without plugins', () => { .toMatchInlineSnapshot(` "import { platformIOS } from '@rock-js/platform-ios'; import { platformAndroid } from '@rock-js/platform-android'; + import { platformHarmony } from '@rock-js/platform-harmony'; import { pluginMetro } from '@rock-js/plugin-metro'; export default { @@ -14,6 +15,7 @@ test('should format config without plugins', () => { platforms: { ios: platformIOS(), android: platformAndroid(), + harmony: platformHarmony(), }, }; " @@ -25,6 +27,7 @@ test('should format config with plugins', () => { { type: 'npm', name: 'test', + displayName: 'test', packageName: '@rock-js/plugin-test', version: 'latest', directory: 'template', diff --git a/packages/create-app/src/lib/__tests__/templates.test.ts b/packages/create-app/src/lib/__tests__/templates.test.ts index 0fefd4cdd..8348ec906 100644 --- a/packages/create-app/src/lib/__tests__/templates.test.ts +++ b/packages/create-app/src/lib/__tests__/templates.test.ts @@ -6,6 +6,7 @@ test('resolveTemplateName with built-in templates', () => { expect(resolveTemplate(TEMPLATES, 'default')).toEqual({ type: 'npm', name: 'default', + displayName: 'default', packageName: '@rock-js/template-default', version: 'latest', directory: '.', @@ -16,6 +17,7 @@ test('resolveTemplateName with local paths', () => { expect(resolveTemplate(TEMPLATES, './directory/template-1')).toEqual({ type: 'local', name: 'template-1', + displayName: 'template-1', localPath: path.resolve('./directory/template-1'), directory: '.', packageName: 'template-1', @@ -24,6 +26,7 @@ test('resolveTemplateName with local paths', () => { expect(resolveTemplate(TEMPLATES, '../../up/up/away/template-2')).toEqual({ type: 'local', name: 'template-2', + displayName: 'template-2', localPath: path.resolve('../../up/up/away/template-2'), directory: '.', packageName: 'template-2', @@ -32,6 +35,7 @@ test('resolveTemplateName with local paths', () => { expect(resolveTemplate(TEMPLATES, '/absolute/path/template-3')).toEqual({ type: 'local', name: 'template-3', + displayName: 'template-3', localPath: '/absolute/path/template-3', directory: '.', packageName: 'template-3', @@ -42,6 +46,7 @@ test('resolveTemplateName with local paths', () => { ).toEqual({ type: 'local', name: 'template-4', + displayName: 'template-4', localPath: '/url-based/path/template-4', directory: '.', packageName: 'template-4', @@ -50,6 +55,7 @@ test('resolveTemplateName with local paths', () => { expect(resolveTemplate(TEMPLATES, './directory/template-5.tgz')).toEqual({ type: 'local', name: 'template-5', + displayName: 'template-5', localPath: path.resolve('./directory/template-5.tgz'), directory: '.', packageName: 'template-5', @@ -58,6 +64,7 @@ test('resolveTemplateName with local paths', () => { expect(resolveTemplate(TEMPLATES, '../up/template-6.tar')).toEqual({ type: 'local', name: 'template-6', + displayName: 'template-6', localPath: path.resolve('../up/template-6.tar'), directory: '.', packageName: 'template-6', @@ -66,6 +73,7 @@ test('resolveTemplateName with local paths', () => { expect(resolveTemplate(TEMPLATES, '/root/directory/template-7.tgz')).toEqual({ type: 'local', name: 'template-7', + displayName: 'template-7', localPath: '/root/directory/template-7.tgz', directory: '.', packageName: 'template-7', @@ -76,6 +84,7 @@ test('resolveTemplateName with npm packages', () => { expect(resolveTemplate(TEMPLATES, 'package-name')).toEqual({ type: 'npm', name: 'package-name', + displayName: 'package-name', directory: '.', packageName: 'package-name', version: 'latest', @@ -83,6 +92,7 @@ test('resolveTemplateName with npm packages', () => { expect(resolveTemplate(TEMPLATES, 'package-name@1.2.3')).toEqual({ type: 'npm', name: 'package-name', + displayName: 'package-name', directory: '.', packageName: 'package-name', version: '1.2.3', @@ -90,6 +100,7 @@ test('resolveTemplateName with npm packages', () => { expect(resolveTemplate(TEMPLATES, '@scoped/package-name@1.2.3')).toEqual({ type: 'npm', name: '@scoped/package-name', + displayName: '@scoped/package-name', directory: '.', packageName: '@scoped/package-name', version: '1.2.3', diff --git a/packages/create-app/src/lib/bin.ts b/packages/create-app/src/lib/bin.ts index e3bcedbd8..ad54f44d3 100644 --- a/packages/create-app/src/lib/bin.ts +++ b/packages/create-app/src/lib/bin.ts @@ -165,7 +165,13 @@ export async function run() { for (const platform of platforms) { await extractPackage(absoluteTargetDir, platform); } - await extractPackage(absoluteTargetDir, bundler); + // Skip metro.config.js for Harmony platform as it's included in the platform template + const skipFiles = platforms + .map((platform) => platform.name) + .includes('harmony') + ? ['metro.config.js'] + : undefined; + await extractPackage(absoluteTargetDir, bundler, skipFiles); for (const plugin of plugins ?? []) { await extractPackage(absoluteTargetDir, plugin); } @@ -252,7 +258,11 @@ async function installDependencies( loader.stop(`Installed dependencies with ${pkgManager}`); } -async function extractPackage(absoluteTargetDir: string, pkg: TemplateInfo) { +async function extractPackage( + absoluteTargetDir: string, + pkg: TemplateInfo, + skipFiles?: string[], +) { let tarballPath: string | null = null; // NPM package: download tarball file if (pkg.type === 'npm') { @@ -282,7 +292,9 @@ async function extractPackage(absoluteTargetDir: string, pkg: TemplateInfo) { fs.unlinkSync(tarballPath); } - copyDirSync(path.join(localPath, pkg.directory ?? ''), absoluteTargetDir); + copyDirSync(path.join(localPath, pkg.directory ?? ''), absoluteTargetDir, { + skipFiles, + }); removeDirSync(localPath); return; @@ -292,6 +304,7 @@ async function extractPackage(absoluteTargetDir: string, pkg: TemplateInfo) { copyDirSync( path.join(pkg.localPath, pkg.directory ?? ''), absoluteTargetDir, + { skipFiles }, ); return; diff --git a/packages/create-app/src/lib/templates.ts b/packages/create-app/src/lib/templates.ts index eef987780..42acc2b7e 100644 --- a/packages/create-app/src/lib/templates.ts +++ b/packages/create-app/src/lib/templates.ts @@ -9,6 +9,7 @@ export type TemplateInfo = NpmTemplateInfo | LocalTemplateInfo; export type NpmTemplateInfo = { type: 'npm'; name: string; + displayName: string; version: string; packageName: string; /** Directory inside package that contains the template */ @@ -21,6 +22,7 @@ export type LocalTemplateInfo = { type: 'local'; name: string; localPath: string; + displayName: string | undefined; packageName: string; directory: string | undefined; importName?: string; @@ -31,6 +33,7 @@ export const TEMPLATES: TemplateInfo[] = [ { type: 'npm', name: 'default', + displayName: 'default', packageName: '@rock-js/template-default', version: 'latest', directory: '.', @@ -41,6 +44,7 @@ export const PLUGINS: TemplateInfo[] = [ { type: 'npm', name: 'brownfield-ios', + displayName: 'Brownfield iOS', packageName: '@rock-js/plugin-brownfield-ios', hint: 'Setup packaging React Native app as a XCFramework', version: 'latest', @@ -50,6 +54,7 @@ export const PLUGINS: TemplateInfo[] = [ { type: 'npm', name: 'brownfield-android', + displayName: 'Brownfield Android', packageName: '@rock-js/plugin-brownfield-android', hint: 'Setup packaging React Native app as an AAR', version: 'latest', @@ -62,6 +67,7 @@ export const BUNDLERS: TemplateInfo[] = [ { type: 'npm', name: 'metro', + displayName: 'Metro', packageName: '@rock-js/plugin-metro', version: 'latest', directory: 'template', @@ -70,6 +76,7 @@ export const BUNDLERS: TemplateInfo[] = [ { type: 'npm', name: 'repack', + displayName: 'Re.Pack', packageName: '@rock-js/plugin-repack', version: 'latest', directory: 'template', @@ -81,6 +88,7 @@ export const PLATFORMS: TemplateInfo[] = [ { type: 'npm', name: 'ios', + displayName: 'iOS', packageName: '@rock-js/platform-ios', version: 'latest', directory: 'template', @@ -89,11 +97,21 @@ export const PLATFORMS: TemplateInfo[] = [ { type: 'npm', name: 'android', + displayName: 'Android', packageName: '@rock-js/platform-android', version: 'latest', directory: 'template', importName: 'platformAndroid', }, + { + type: 'npm', + name: 'harmony', + displayName: 'Harmony (experimental)', + packageName: '@rock-js/platform-harmony', + version: 'latest', + directory: 'template', + importName: 'platformHarmony', + }, ]; export function remoteCacheProviderToImportTemplate( @@ -175,6 +193,7 @@ export function resolveTemplate( return { type: 'local', name: basename.slice(0, basename.length - ext.length), + displayName: basename.slice(0, basename.length - ext.length), localPath: resolveAbsolutePath(name), directory: '.', packageName: basename.slice(0, basename.length - ext.length), @@ -187,6 +206,7 @@ export function resolveTemplate( return { type: 'npm', name: getNpmLibraryName(name), + displayName: getNpmLibraryName(name), packageName: getNpmLibraryName(name), directory: '.', version: getNpmLibraryVersion(name) ?? 'latest', diff --git a/packages/create-app/src/lib/utils/edit-template.ts b/packages/create-app/src/lib/utils/edit-template.ts index 623b29ee1..f408b8353 100644 --- a/packages/create-app/src/lib/utils/edit-template.ts +++ b/packages/create-app/src/lib/utils/edit-template.ts @@ -12,11 +12,18 @@ const PLACEHOLDER_NAME = 'HelloWorld'; */ export function renameCommonFiles(projectPath: string) { const sourceGitIgnorePath = path.join(projectPath, 'gitignore'); - if (!fs.existsSync(sourceGitIgnorePath)) { - return; + if (fs.existsSync(sourceGitIgnorePath)) { + fs.renameSync(sourceGitIgnorePath, path.join(projectPath, '.gitignore')); } - fs.renameSync(sourceGitIgnorePath, path.join(projectPath, '.gitignore')); + // Harmony platform has a separate gitignore file. + const harmonyGitIgnorePath = path.join(projectPath, 'harmony', 'gitignore'); + if (fs.existsSync(harmonyGitIgnorePath)) { + fs.renameSync( + harmonyGitIgnorePath, + path.join(projectPath, 'harmony', '.gitignore'), + ); + } } /** diff --git a/packages/create-app/src/lib/utils/fs.ts b/packages/create-app/src/lib/utils/fs.ts index 2a520dea3..fdda0e009 100644 --- a/packages/create-app/src/lib/utils/fs.ts +++ b/packages/create-app/src/lib/utils/fs.ts @@ -29,7 +29,7 @@ export function copyDirSync( } else { if (nodePath.basename(srcFile) === 'package.json') { mergePackageJsons(srcFile, distFile); - } else { + } else if (!skipFiles?.includes(nodePath.basename(srcFile))) { fs.copyFileSync(srcFile, distFile); } } diff --git a/packages/create-app/src/lib/utils/prompts.ts b/packages/create-app/src/lib/utils/prompts.ts index 83c0156cd..4ec31cec3 100644 --- a/packages/create-app/src/lib/utils/prompts.ts +++ b/packages/create-app/src/lib/utils/prompts.ts @@ -96,7 +96,7 @@ export async function promptTemplate( // @ts-expect-error todo options: templates.map((template) => ({ value: template, - label: template.name, + label: template.displayName, })), }); } @@ -118,7 +118,7 @@ export function promptPlatforms( // @ts-expect-error todo options: platforms.map((platform) => ({ value: platform, - label: platform.name, + label: platform.displayName, })), }); } @@ -135,7 +135,7 @@ export function promptPlugins( // @ts-expect-error todo fixup type options: plugins.map((plugin) => ({ value: plugin, - label: plugin.name, + label: plugin.displayName, hint: plugin.hint, })), required: false, @@ -155,7 +155,7 @@ export function promptBundlers( // @ts-expect-error todo fixup type options: bundlers.map((bundler) => ({ value: bundler, - label: bundler.name, + label: bundler.displayName, })), }); } diff --git a/packages/platform-android/src/lib/commands/buildAndroid/__tests__/buildAndroid.test.ts b/packages/platform-android/src/lib/commands/buildAndroid/__tests__/buildAndroid.test.ts index e14b9d918..f179847ec 100644 --- a/packages/platform-android/src/lib/commands/buildAndroid/__tests__/buildAndroid.test.ts +++ b/packages/platform-android/src/lib/commands/buildAndroid/__tests__/buildAndroid.test.ts @@ -28,6 +28,7 @@ const androidProject: AndroidProjectConfig = { const fingerprintOptions = { extraSources: [], ignorePaths: [], + env: [], }; const spinnerMock = vi.hoisted(() => ({ diff --git a/packages/platform-android/src/lib/commands/runAndroid/__tests__/runAndroid.test.ts b/packages/platform-android/src/lib/commands/runAndroid/__tests__/runAndroid.test.ts index a2134aa83..d846306b0 100644 --- a/packages/platform-android/src/lib/commands/runAndroid/__tests__/runAndroid.test.ts +++ b/packages/platform-android/src/lib/commands/runAndroid/__tests__/runAndroid.test.ts @@ -311,7 +311,7 @@ test.each([['release'], ['debug'], ['staging']])( { ...args, variant }, '/', undefined, - { extraSources: [], ignorePaths: [] }, + { extraSources: [], ignorePaths: [], env: [] }, ); expect(tools.outro).toBeCalledWith('Success 🎉.'); @@ -360,7 +360,7 @@ test('runAndroid runs gradle build with custom --appId, --appIdSuffix and --main }, '/', undefined, - { extraSources: [], ignorePaths: [] }, + { extraSources: [], ignorePaths: [], env: [] }, ); expect(tools.outro).toBeCalledWith('Success 🎉.'); @@ -387,7 +387,7 @@ test('runAndroid fails to launch an app on not-connected device when specified w { ...args, device: 'emulator-5554' }, '/', undefined, - { extraSources: [], ignorePaths: [] }, + { extraSources: [], ignorePaths: [], env: [] }, ); expect(logWarnSpy).toBeCalledWith( 'No devices or emulators found matching "emulator-5554". Using available one instead.', @@ -456,7 +456,7 @@ test.each([['release'], ['debug']])( { ...args, device: 'emulator-5554', variant }, '/', undefined, - { extraSources: [], ignorePaths: [] }, + { extraSources: [], ignorePaths: [], env: [] }, ); // we don't want to run installDebug when a device is selected, because gradle will install the app on all connected devices @@ -516,6 +516,7 @@ test('runAndroid launches an app on all connected devices', async () => { await runAndroid({ ...androidProject }, { ...args }, '/', undefined, { extraSources: [], ignorePaths: [], + env: [], }); // Runs assemble debug task with active architectures arm64-v8a, armeabi-v7a @@ -582,7 +583,7 @@ test('runAndroid skips building when --binary-path is passed', async () => { }, '/root', undefined, - { extraSources: [], ignorePaths: [] }, + { extraSources: [], ignorePaths: [], env: [] }, ); // Skips gradle diff --git a/packages/platform-android/src/lib/commands/runAndroid/tryInstallAppOnDevice.ts b/packages/platform-android/src/lib/commands/runAndroid/tryInstallAppOnDevice.ts index 5104c2085..bc5556eef 100644 --- a/packages/platform-android/src/lib/commands/runAndroid/tryInstallAppOnDevice.ts +++ b/packages/platform-android/src/lib/commands/runAndroid/tryInstallAppOnDevice.ts @@ -21,7 +21,7 @@ export async function tryInstallAppOnDevice( let deviceId: string; if (!device.deviceId) { logger.debug( - `No device with id "${device.deviceId}", skipping launching the app.`, + `No "deviceId" for ${device}, skipping launching the app`, ); return; } else { diff --git a/packages/platform-android/src/lib/commands/runAndroid/tryLaunchAppOnDevice.ts b/packages/platform-android/src/lib/commands/runAndroid/tryLaunchAppOnDevice.ts index c2c2db79d..9d71b6b17 100644 --- a/packages/platform-android/src/lib/commands/runAndroid/tryLaunchAppOnDevice.ts +++ b/packages/platform-android/src/lib/commands/runAndroid/tryLaunchAppOnDevice.ts @@ -12,7 +12,7 @@ export async function tryLaunchAppOnDevice( let deviceId; if (!device.deviceId) { logger.debug( - `No device with id "${device.deviceId}", skipping launching the app.`, + `No "deviceId" for ${device}, skipping launching the app`, ); return {}; } else { diff --git a/packages/platform-harmony/.npmrc b/packages/platform-harmony/.npmrc new file mode 100644 index 000000000..cc8df9de0 --- /dev/null +++ b/packages/platform-harmony/.npmrc @@ -0,0 +1 @@ +node-linker=hoisted \ No newline at end of file diff --git a/packages/platform-harmony/CHANGELOG.md b/packages/platform-harmony/CHANGELOG.md new file mode 100644 index 000000000..9581f8eb7 --- /dev/null +++ b/packages/platform-harmony/CHANGELOG.md @@ -0,0 +1 @@ +# @rock-js/platform-harmony diff --git a/packages/platform-harmony/README.md b/packages/platform-harmony/README.md new file mode 100644 index 000000000..b5be86f78 --- /dev/null +++ b/packages/platform-harmony/README.md @@ -0,0 +1,7 @@ +# @rock-js/platform-harmony + +HarmonyOS Next platform integration for Rock. This package is part of the Rock ecosystem and provides HarmonyOS-specific build and development tools. + +## Documentation + +For detailed documentation about Rock and its tools, visit [Rock Documentation](https://rockjs.dev) diff --git a/packages/platform-harmony/package.json b/packages/platform-harmony/package.json new file mode 100644 index 000000000..b94c5918d --- /dev/null +++ b/packages/platform-harmony/package.json @@ -0,0 +1,42 @@ +{ + "name": "@rock-js/platform-harmony", + "version": "0.11.5", + "type": "module", + "types": "./dist/src/index.d.ts", + "exports": { + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + }, + "files": [ + "dist", + "template", + "react-native.config.ts" + ], + "scripts": { + "build": "tsc -p tsconfig.lib.json", + "dev": "tsc -p tsconfig.lib.json --watch", + "publish:npm": "npm publish --access public", + "publish:verdaccio": "npm publish --registry http://localhost:4873 --userconfig ../../.npmrc" + }, + "dependencies": { + "@rock-js/tools": "^0.11.5", + "json5": "^2.2.3", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@react-native-community/cli-types": "^20.0.0", + "@rock-js/config": "^0.11.5" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/callstack/rock.git", + "directory": "packages/platform-harmony" + }, + "homepage": "https://rockjs.dev", + "bugs": "https://github.com/callstack/rock/issues", + "license": "MIT", + "author": "Callstack " +} diff --git a/packages/platform-harmony/react-native.config.ts b/packages/platform-harmony/react-native.config.ts new file mode 100644 index 000000000..670daad08 --- /dev/null +++ b/packages/platform-harmony/react-native.config.ts @@ -0,0 +1,31 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { getValidProjectConfig } from './dist/src/lib/commands/getValidProjectConfig.js'; + +/** + * Get the dependency config for the Harmony platform. It's currently very bare bones + * and only supports aliasing, but that should be enough for now to list dependencies, + * that we use as input for fingerprinting. + * @param folder - The folder to get the dependency config for. + * @returns The dependency config. + */ +function getDependencyConfig(folder: string) { + const packageJsonPath = path.join(folder, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (packageJson.harmony?.alias) { + return { alias: packageJson.harmony.alias }; + } + } + return null; +} + +export default { + platforms: { + harmony: { + npmPackageName: '@react-native-oh/react-native-harmony', + projectConfig: getValidProjectConfig, + dependencyConfig: getDependencyConfig, + }, + }, +}; diff --git a/packages/platform-harmony/src/index.ts b/packages/platform-harmony/src/index.ts new file mode 100644 index 000000000..91ed5743c --- /dev/null +++ b/packages/platform-harmony/src/index.ts @@ -0,0 +1 @@ +export * from './lib/platformHarmony.js'; diff --git a/packages/platform-harmony/src/lib/commands/build/__tests__/buildHarmony.test.ts b/packages/platform-harmony/src/lib/commands/build/__tests__/buildHarmony.test.ts new file mode 100644 index 000000000..40dbfa43b --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/build/__tests__/buildHarmony.test.ts @@ -0,0 +1,117 @@ +import * as tools from '@rock-js/tools'; +import { RockError, spawn } from '@rock-js/tools'; +import type { Mock } from 'vitest'; +import { test, vi } from 'vitest'; +import type { HarmonyProjectConfig } from '../../getValidProjectConfig.js'; +import { type BuildFlags, buildHarmony } from '../buildHarmony.js'; + +const args: BuildFlags = { + buildMode: 'debug', + module: 'entry', + product: 'default', + local: true, +}; +const harmonyProject: HarmonyProjectConfig = { + sourceDir: '/harmony', + bundleName: 'test', + signingConfigs: true, +}; + +const fingerprintOptions = { + extraSources: [], + ignorePaths: [], + env: [], +}; + +const spinnerMock = vi.hoisted(() => ({ + start: vi.fn(), + stop: vi.fn(), + message: vi.fn(), +})); + +vi.spyOn(tools, 'spinner').mockImplementation(() => spinnerMock); + +const OLD_ENV = process.env; + +beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + process.env = { ...OLD_ENV, DEVECO_SDK_HOME: '/mock/deveco/sdk' }; +}); + +function mockOhpmCall(file: string, args: string[]) { + return file.includes('ohpm') && args?.[0] === 'install'; +} + +function spawnMockImplementation(file: string, args: string[]) { + if (mockOhpmCall(file, args)) { + return { output: 'install completed in 0s 22ms' }; + } + return { output: '...' }; +} + +test('buildAndroid runs gradle build with correct configuration for debug and outputs build path', async () => { + (spawn as Mock).mockImplementation((file, args) => + spawnMockImplementation(file, args), + ); + + await buildHarmony( + harmonyProject, + { ...args }, + '/root', + null, + fingerprintOptions, + ); + + expect(spawn).toBeCalledWith( + 'node', + [ + expect.stringContaining('hvigorw.js'), + '-p', + 'module=entry@default', + '-p', + 'product=default', + '-p', + 'buildMode=debug', + '-p', + 'requiredDeviceType=phone', + 'assembleHap', + ], + { + cwd: '/harmony', + }, + ); + expect(spinnerMock.stop).toBeCalledWith('Built the app'); +}); + +test('buildHarmony fails gracefully when hvigor errors', async () => { + (spawn as Mock).mockImplementation((file, args) => { + if (file === 'node' && args?.[0]?.includes('hvigorw.js')) { + throw new RockError('hvigor error'); + } + return spawnMockImplementation(file, args); + }); + + await expect( + buildHarmony(harmonyProject, args, '/root', null, fingerprintOptions), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[RockError: Failed to build the app with Hvigor]`, + ); + + expect(spawn).toBeCalledWith( + 'node', + [ + expect.stringContaining('hvigorw.js'), + '-p', + 'module=entry@default', + '-p', + 'product=default', + '-p', + 'buildMode=debug', + '-p', + 'requiredDeviceType=phone', + 'assembleHap', + ], + { cwd: '/harmony' }, + ); +}); diff --git a/packages/platform-harmony/src/lib/commands/build/buildHarmony.ts b/packages/platform-harmony/src/lib/commands/build/buildHarmony.ts new file mode 100644 index 000000000..ec0df0b33 --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/build/buildHarmony.ts @@ -0,0 +1,99 @@ +import type { RemoteBuildCache } from '@rock-js/tools'; +import { + colorLink, + type FingerprintSources, + formatArtifactName, + getBinaryPath, + logger, + outro, + relativeToCwd, +} from '@rock-js/tools'; +import { findOutputFile } from '../run/findOutputFile.js'; +import { runHvigor } from '../runHvigor.js'; + +export interface BuildFlags { + buildMode: string; + module: string; + product: string; + local?: boolean; +} + +export async function buildHarmony( + harmonyConfig: { + sourceDir: string; + bundleName: string; + }, + args: BuildFlags, + projectRoot: string, + remoteCacheProvider: null | (() => RemoteBuildCache) | undefined, + fingerprintOptions: FingerprintSources, +) { + const { sourceDir, bundleName } = harmonyConfig; + const artifactName = await formatArtifactName({ + platform: 'harmony', + traits: [args.buildMode], + root: projectRoot, + fingerprintOptions, + }); + const binaryPath = await getBinaryPath({ + platformName: 'harmony', + artifactName, + localFlag: args.local, + remoteCacheProvider, + fingerprintOptions, + sourceDir, + }); + if (!binaryPath) { + await runHvigor({ sourceDir, args, artifactName, bundleName }); + } + + if (binaryPath) { + logger.log(`Build available at: ${colorLink(relativeToCwd(binaryPath))}`); + } else { + const signedHapPath = await findOutputFile(sourceDir, args.module, { + deviceId: undefined, + readableName: undefined, + type: 'phone', + connected: false, + }); + if (signedHapPath) { + logger.log( + `Signed build available at: ${colorLink(relativeToCwd(signedHapPath))}`, + ); + } + const unsignedHapPath = await findOutputFile(sourceDir, args.module, { + deviceId: undefined, + readableName: undefined, + type: 'emulator', + connected: false, + }); + if (unsignedHapPath) { + logger.log( + `Unsigned build available at: ${colorLink(relativeToCwd(unsignedHapPath))}`, + ); + } + } + outro('Success 🎉.'); +} + +export const options = [ + { + name: '--local', + description: 'Force local build with Gradle wrapper.', + }, + { + name: '--module ', + description: 'Name of the OH module to run.', + default: 'entry', + }, + { + name: '--build-mode ', + description: `Specify your app's build mode, e.g. "debug" or "release".`, + default: 'debug', + }, + { + name: '--product ', + description: 'OpenHarmony product defined in build-profile.json5.', + default: 'default', + }, +]; diff --git a/packages/platform-harmony/src/lib/commands/build/command.ts b/packages/platform-harmony/src/lib/commands/build/command.ts new file mode 100644 index 000000000..fd96ba487 --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/build/command.ts @@ -0,0 +1,29 @@ +import type { PluginApi } from '@rock-js/config'; +import { + getValidProjectConfig, + type HarmonyProjectConfig, +} from '../getValidProjectConfig.js'; +import type { BuildFlags } from './buildHarmony.js'; +import { buildHarmony, options } from './buildHarmony.js'; + +export function registerBuildCommand( + api: PluginApi, + pluginConfig: Partial | undefined, +) { + api.registerCommand({ + name: 'build:harmony', + description: 'Builds your app for HarmonyOS Next platform.', + action: async (args) => { + const projectRoot = api.getProjectRoot(); + const harmonyConfig = getValidProjectConfig(projectRoot, pluginConfig); + await buildHarmony( + harmonyConfig, + args as BuildFlags, + projectRoot, + await api.getRemoteCacheProvider(), + api.getFingerprintOptions(), + ); + }, + options: options, + }); +} diff --git a/packages/platform-harmony/src/lib/commands/getValidProjectConfig.ts b/packages/platform-harmony/src/lib/commands/getValidProjectConfig.ts new file mode 100644 index 000000000..6a1ca8ae7 --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/getValidProjectConfig.ts @@ -0,0 +1,54 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { logger, RockError } from '@rock-js/tools'; +import json5 from 'json5'; + +export type HarmonyProjectConfig = { + sourceDir: string; + bundleName: string; + signingConfigs: boolean | undefined; +}; + +export function getValidProjectConfig( + projectRoot: string, + pluginConfig?: Partial, +) { + const sourceDir = pluginConfig?.sourceDir + ? path.isAbsolute(pluginConfig?.sourceDir) + ? pluginConfig?.sourceDir + : path.join(projectRoot, pluginConfig?.sourceDir) + : path.join(projectRoot, 'harmony'); + + if (!fs.existsSync(sourceDir)) { + throw new RockError(`Harmony project not found under ${sourceDir}.`); + } + + let bundleName: string; + try { + bundleName = json5.parse( + fs.readFileSync(path.join(sourceDir, 'AppScope', 'app.json5'), 'utf8'), + ).app.bundleName; + } catch (error) { + throw new RockError('Error reading app.json5 file.', { + cause: error, + }); + } + + let signingConfigs; + try { + const buildProfile = json5.parse( + fs.readFileSync(path.join(sourceDir, 'build-profile.json5'), 'utf8'), + ); + signingConfigs = Boolean(buildProfile.app.signingConfigs); + } catch (error) { + logger.debug('Error reading build-profile.json5 file.', { + cause: (error as Error).message, + }); + } + + return { + sourceDir, + bundleName, + signingConfigs, + }; +} diff --git a/packages/platform-harmony/src/lib/commands/run/__tests__/tryLaunchAppOnDevice.test.ts b/packages/platform-harmony/src/lib/commands/run/__tests__/tryLaunchAppOnDevice.test.ts new file mode 100644 index 000000000..69e8bf3a5 --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/run/__tests__/tryLaunchAppOnDevice.test.ts @@ -0,0 +1,155 @@ +import { spawn } from '@rock-js/tools'; +import { test, vi } from 'vitest'; +import type { DeviceData } from '../listHarmonyDevices.js'; +import type { Flags } from '../runHarmony.js'; +import { tryLaunchAppOnDevice } from '../tryLaunchAppOnDevice.js'; + +vi.mock('@rock-js/tools', async () => { + return { + ...(await vi.importActual('@rock-js/tools')), + spawn: vi.fn(() => Promise.resolve({ stdout: '', stderr: '' })), + }; +}); + +const OLD_ENV = process.env; + +beforeEach(() => { + vi.clearAllMocks(); + vi.resetModules(); + process.env = { ...OLD_ENV, DEVECO_SDK_HOME: '/mock/deveco/sdk' }; +}); + +afterAll(() => { + process.env = OLD_ENV; +}); + +const device: DeviceData = { + deviceId: '4UQ9K25710015363', + readableName: 'Emulator 5554', + connected: true, + type: 'emulator', +}; + +const args: Flags = { + port: '8081', + buildMode: 'debug', + module: 'entry', + product: 'default', + ability: 'EntryAbility', + local: true, +}; + +const bundleName = 'com.example.app'; + +test('launches hdc shell to force stop and start app on device', async () => { + await tryLaunchAppOnDevice(device, bundleName, args); + + expect(spawn).toHaveBeenCalledWith( + '/mock/deveco/sdk/default/openharmony/toolchains/hdc', + ['-t', '4UQ9K25710015363', 'shell', 'aa', 'force-stop', bundleName], + ); + + expect(spawn).toHaveBeenCalledWith( + '/mock/deveco/sdk/default/openharmony/toolchains/hdc', + [ + '-t', + '4UQ9K25710015363', + 'shell', + 'aa', + 'start', + '-a', + 'EntryAbility', + '-b', + bundleName, + ], + ); +}); + +test('launches hdc shell with different ability name', async () => { + const customArgs = { ...args, ability: 'CustomAbility' }; + await tryLaunchAppOnDevice(device, bundleName, customArgs); + + expect(spawn).toHaveBeenCalledWith( + '/mock/deveco/sdk/default/openharmony/toolchains/hdc', + ['-t', '4UQ9K25710015363', 'shell', 'aa', 'force-stop', bundleName], + ); + + expect(spawn).toHaveBeenCalledWith( + '/mock/deveco/sdk/default/openharmony/toolchains/hdc', + [ + '-t', + '4UQ9K25710015363', + 'shell', + 'aa', + 'start', + '-a', + 'CustomAbility', + '-b', + bundleName, + ], + ); +}); + +test('launches hdc shell with different bundle name', async () => { + const customBundleName = 'com.custom.app'; + await tryLaunchAppOnDevice(device, customBundleName, args); + + expect(spawn).toHaveBeenCalledWith( + '/mock/deveco/sdk/default/openharmony/toolchains/hdc', + ['-t', '4UQ9K25710015363', 'shell', 'aa', 'force-stop', customBundleName], + ); + + expect(spawn).toHaveBeenCalledWith( + '/mock/deveco/sdk/default/openharmony/toolchains/hdc', + [ + '-t', + '4UQ9K25710015363', + 'shell', + 'aa', + 'start', + '-a', + 'EntryAbility', + '-b', + customBundleName, + ], + ); +}); + +test('launches hdc shell with different device id', async () => { + const customDevice = { ...device, deviceId: 'device-123' }; + await tryLaunchAppOnDevice(customDevice, bundleName, args); + + expect(spawn).toHaveBeenCalledWith( + '/mock/deveco/sdk/default/openharmony/toolchains/hdc', + ['-t', 'device-123', 'shell', 'aa', 'force-stop', bundleName], + ); + + expect(spawn).toHaveBeenCalledWith( + '/mock/deveco/sdk/default/openharmony/toolchains/hdc', + [ + '-t', + 'device-123', + 'shell', + 'aa', + 'start', + '-a', + 'EntryAbility', + '-b', + bundleName, + ], + ); +}); + +test('returns applicationIdWithSuffix when successful', async () => { + const result = await tryLaunchAppOnDevice(device, bundleName, args); + + expect(result).toEqual({ applicationIdWithSuffix: bundleName }); +}); + +test('handles device without deviceId gracefully', async () => { + const deviceWithoutId = { ...device, deviceId: undefined }; + const result = await tryLaunchAppOnDevice(deviceWithoutId, bundleName, args); + + expect(result).toEqual({}); + expect(spawn).not.toHaveBeenCalled(); +}); diff --git a/packages/platform-harmony/src/lib/commands/run/command.ts b/packages/platform-harmony/src/lib/commands/run/command.ts new file mode 100644 index 000000000..3af277a30 --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/run/command.ts @@ -0,0 +1,30 @@ +import type { PluginApi } from '@rock-js/config'; +import { + getValidProjectConfig, + type HarmonyProjectConfig, +} from '../getValidProjectConfig.js'; +import type { Flags } from './runHarmony.js'; +import { runHarmony, runOptions } from './runHarmony.js'; + +export function registerRunCommand( + api: PluginApi, + pluginConfig: Partial | undefined, +) { + api.registerCommand({ + name: 'run:harmony', + description: + 'Builds your app and starts it on a connected HarmonyOS Next device.', + action: async (args) => { + const projectRoot = api.getProjectRoot(); + const harmonyConfig = getValidProjectConfig(projectRoot, pluginConfig); + await runHarmony( + harmonyConfig, + args as Flags, + projectRoot, + await api.getRemoteCacheProvider(), + api.getFingerprintOptions(), + ); + }, + options: runOptions, + }); +} diff --git a/packages/platform-harmony/src/lib/commands/run/findOutputFile.ts b/packages/platform-harmony/src/lib/commands/run/findOutputFile.ts new file mode 100644 index 000000000..4f8f56c8e --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/run/findOutputFile.ts @@ -0,0 +1,26 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type { DeviceData } from './listHarmonyDevices.js'; + +export async function findOutputFile( + sourceDir: string, + module: string, + device?: DeviceData, +) { + let hapName: string; + if (device?.type === 'emulator') { + hapName = `${module}-default-unsigned.hap`; + } else { + hapName = `${module}-default-signed.hap`; + } + const pathToHap = path.join( + sourceDir, + module, + 'build', + 'default', + 'outputs', + 'default', + hapName, + ); + return fs.existsSync(pathToHap) ? pathToHap : undefined; +} diff --git a/packages/platform-harmony/src/lib/commands/run/hdc.ts b/packages/platform-harmony/src/lib/commands/run/hdc.ts new file mode 100644 index 000000000..0818ba2df --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/run/hdc.ts @@ -0,0 +1,43 @@ +import path from 'node:path'; +import { spawn } from '@rock-js/tools'; +import { getDevEcoSdkPath } from '../../paths.js'; + +export function getHdcPath() { + return path.join( + getDevEcoSdkPath(), + 'default', + 'openharmony', + 'toolchains', + 'hdc', + ); +} + +/** + * Executes the commands needed to get a list of devices from ADB + */ +export async function getDevices() { + const hdcPath = getHdcPath(); + try { + const { output } = await spawn(hdcPath, ['list', 'targets', '-v'], { + stdio: 'pipe', + }); + const lines = output.trim().split('\n'); + return ( + lines + .map((line) => { + const parts = line.split(/\s+/); + return { + name: parts[0], + method: parts[1], // USB + state: parts[2], // Connected, Offline + locate: parts[3], // localhost + connectTool: parts[4], + }; + }) + // hdc will report no devices as [Empty] sometimes + .filter((line) => line.state != undefined) + ); + } catch { + return []; + } +} diff --git a/packages/platform-harmony/src/lib/commands/run/listHarmonyDevices.ts b/packages/platform-harmony/src/lib/commands/run/listHarmonyDevices.ts new file mode 100644 index 000000000..6062db4e3 --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/run/listHarmonyDevices.ts @@ -0,0 +1,27 @@ +import { getDevices } from './hdc.js'; + +export type DeviceData = { + deviceId: string | undefined; + readableName: string | undefined; + connected: boolean; + type: 'emulator' | 'phone'; +}; + +export async function listHarmonyDevices() { + const devices = await getDevices(); + + const allDevices: Array = []; + + for (const device of devices) { + const phoneData: DeviceData = { + deviceId: device.name, + // @todo get readable name + readableName: device.name, + type: 'phone', + connected: device.state === 'Connected', + }; + allDevices.push(phoneData); + } + + return allDevices; +} diff --git a/packages/platform-harmony/src/lib/commands/run/runHarmony.ts b/packages/platform-harmony/src/lib/commands/run/runHarmony.ts new file mode 100644 index 000000000..f49b68dfe --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/run/runHarmony.ts @@ -0,0 +1,228 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type { FingerprintSources, RemoteBuildCache } from '@rock-js/tools'; +import { + color, + formatArtifactName, + intro, + isInteractive, + logger, + outro, + promptSelect, + RockError, + spinner, +} from '@rock-js/tools'; +import { getBinaryPath } from '@rock-js/tools'; +import type { BuildFlags } from '../build/buildHarmony.js'; +import { options } from '../build/buildHarmony.js'; +import { runHvigor } from '../runHvigor.js'; +import { getDevices } from './hdc.js'; +import type { DeviceData } from './listHarmonyDevices.js'; +import { listHarmonyDevices } from './listHarmonyDevices.js'; +import { tryInstallAppOnDevice } from './tryInstallAppOnDevice.js'; +import { tryLaunchAppOnDevice } from './tryLaunchAppOnDevice.js'; + +export interface Flags extends BuildFlags { + ability: string; + port: string; + device?: string; + binaryPath?: string; +} + +/** + * Starts the app on a connected HarmonyOS emulator or device. + */ +export async function runHarmony( + harmonyConfig: { + sourceDir: string; + bundleName: string; + }, + args: Flags, + projectRoot: string, + remoteCacheProvider: null | (() => RemoteBuildCache) | undefined, + fingerprintOptions: FingerprintSources, +) { + intro('Running HarmonyOS Next app'); + + normalizeArgs(args, projectRoot); + const { sourceDir, bundleName } = harmonyConfig; + const devices = await listHarmonyDevices(); + const device = await selectDevice(devices, args); + + const artifactName = await formatArtifactName({ + platform: 'harmony', + traits: [args.buildMode], + root: projectRoot, + fingerprintOptions, + }); + const binaryPath = await getBinaryPath({ + platformName: 'harmony', + artifactName, + binaryPathFlag: args.binaryPath, + localFlag: args.local, + remoteCacheProvider, + fingerprintOptions, + sourceDir: sourceDir, + }); + + if (device) { + if (device.deviceId) { + if (!binaryPath) { + await runHvigor({ sourceDir, args, artifactName, device, bundleName }); + } + await runOnDevice({ device, sourceDir, args, binaryPath, bundleName }); + } + } else { + if ((await getDevices()).length === 0) { + if (isInteractive()) { + await selectAndLaunchDevice(); + } else { + logger.warn( + 'No booted devices or emulators found. Launching first available emulator.', + ); + // @todo add emulators + } + } + + if (!binaryPath) { + await runHvigor({ sourceDir, args, artifactName, bundleName }); + } + + for (const device of await listHarmonyDevices()) { + if (device.connected) { + await runOnDevice({ device, sourceDir, args, binaryPath, bundleName }); + } + } + } + + outro('Success 🎉.'); +} + +async function selectAndLaunchDevice() { + const allDevices = await listHarmonyDevices(); + const device = await promptForDeviceSelection(allDevices); + + if (!device.connected) { + // @todo add emulators + // list devices once again when emulator is booted + const allDevices = await listHarmonyDevices(); + const newDevice = + allDevices.find((d) => d.readableName === device.readableName) ?? device; + return newDevice; + } + return device; +} + +async function selectDevice(devices: DeviceData[], args: Flags) { + const device = args.device ? matchingDevice(devices, args.device) : undefined; + if (!device && args.device) { + logger.warn( + `No devices or emulators found matching "${args.device}". Using available one instead.`, + ); + } + return device; +} + +function matchingDevice(devices: Array, deviceArg: string) { + const deviceByName = devices.find( + (device) => device.readableName === deviceArg, + ); + const deviceById = devices.find((d) => d.deviceId === deviceArg); + return deviceByName || deviceById; +} + +function normalizeArgs(args: Flags, projectRoot: string) { + if (args.binaryPath) { + args.binaryPath = path.isAbsolute(args.binaryPath) + ? args.binaryPath + : path.join(projectRoot, args.binaryPath); + + if (args.binaryPath && !fs.existsSync(args.binaryPath)) { + throw new RockError( + `"--binary-path" was specified, but the file was not found at "${args.binaryPath}".`, + ); + } + } +} + +async function promptForDeviceSelection( + allDevices: Array, +): Promise { + if (!allDevices.length) { + throw new RockError( + // @todo add emulators + 'No devices connected. Please create connect HarmonyOS device.', + ); + } + const selected = await promptSelect({ + // @todo add emulators + message: 'Select the device you want to use', + options: allDevices.map((d) => ({ + label: `${d.readableName}${ + d.type === 'phone' ? ' - (physical device)' : '' + }${d.connected ? ' (connected)' : ''}`, + value: d, + })), + }); + + return selected; +} + +async function runOnDevice({ + device, + sourceDir, + args, + binaryPath, + bundleName, +}: { + device: DeviceData; + sourceDir: string; + args: Flags; + binaryPath: string | undefined; + bundleName: string; +}) { + const loader = spinner(); + loader.start('Installing the app'); + await tryInstallAppOnDevice(device, sourceDir, args, binaryPath); + loader.message('Launching the app'); + const { applicationIdWithSuffix } = await tryLaunchAppOnDevice( + device, + bundleName, + args, + ); + if (applicationIdWithSuffix) { + loader.stop( + `Installed and launched the app on ${color.bold(device.readableName)}`, + ); + } else { + loader.stop( + `Failed: installing and launching the app on ${color.bold( + device.readableName, + )}`, + ); + } +} + +export const runOptions = [ + ...options, + { + name: '--port ', + description: 'Part for packager.', + default: process.env['RCT_METRO_PORT'] || '8081', + }, + { + name: '--ability ', + description: 'Name of the ability to start.', + default: 'EntryAbility', + }, + { + name: '--device ', + description: + 'Explicitly set the device or emulator to use by name or ID (if launched).', + }, + { + name: '--binary-path ', + description: + 'Path relative to project root where pre-built .apk binary lives.', + }, +]; diff --git a/packages/platform-harmony/src/lib/commands/run/tryInstallAppOnDevice.ts b/packages/platform-harmony/src/lib/commands/run/tryInstallAppOnDevice.ts new file mode 100644 index 000000000..037cd7a44 --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/run/tryInstallAppOnDevice.ts @@ -0,0 +1,66 @@ +import path from 'node:path'; +import { + color, + colorLink, + logger, + RockError, + spawn, + type SubprocessError, +} from '@rock-js/tools'; +import { findOutputFile } from './findOutputFile.js'; +import { getHdcPath } from './hdc.js'; +import type { DeviceData } from './listHarmonyDevices.js'; +import type { Flags } from './runHarmony.js'; + +export async function tryInstallAppOnDevice( + device: DeviceData, + sourceDir: string, + args: Flags, + binaryPath: string | undefined, +) { + if (!device.deviceId) { + logger.debug( + `No "deviceId" for ${device}, skipping launching the app`, + ); + return; + } + logger.debug(`Connected to device ${color.bold(device.readableName)}`); + let pathToHap: string; + if (!binaryPath) { + const outputFilePath = await findOutputFile(sourceDir, args.module, device); + if (!outputFilePath) { + if (device.type === 'phone') { + throw new RockError( + `There was no signed build output file for the physical device. +This usually means you're missing signing config in your ${colorLink( + path.join(sourceDir, 'build-profile.json5'), + )} file. +Please open DevEco Studio, proceed to: ${color.bold('File > Project Structure... > Signing Configs')} +and log in to your Huawei account to fill the signing information.`, + ); + } else { + logger.warn( + "Skipping installation because there's no build output file.", + ); + return; + } + } + pathToHap = outputFilePath; + } else { + pathToHap = binaryPath; + } + const hdcPath = getHdcPath(); + + try { + await spawn(hdcPath, ['-t', device.deviceId, 'install', '-r', pathToHap]); + } catch (error) { + const errorMessage = + (error as SubprocessError).stderr || (error as SubprocessError).stdout; + if (errorMessage.includes('failed to install')) { + throw new RockError( + `Installation failed. If an application with the same bundle name is already installed, try uninstalling it`, + { cause: error }, + ); + } + } +} diff --git a/packages/platform-harmony/src/lib/commands/run/tryLaunchAppOnDevice.ts b/packages/platform-harmony/src/lib/commands/run/tryLaunchAppOnDevice.ts new file mode 100644 index 000000000..cc304070e --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/run/tryLaunchAppOnDevice.ts @@ -0,0 +1,52 @@ +import { logger, RockError, spawn, type SubprocessError } from '@rock-js/tools'; +import { getHdcPath } from './hdc.js'; +import type { DeviceData } from './listHarmonyDevices.js'; +import type { Flags } from './runHarmony.js'; +import { tryRunHdcReverse } from './tryRunHdcReverse.js'; + +export async function tryLaunchAppOnDevice( + device: DeviceData, + bundleName: string, + args: Flags, +) { + let deviceId; + if (!device.deviceId) { + logger.debug( + `No "deviceId" for ${device}, skipping launching the app`, + ); + return {}; + } else { + deviceId = device.deviceId; + } + await tryRunHdcReverse(args.port, deviceId); + + const hdcPath = getHdcPath(); + + try { + await spawn(hdcPath, [ + '-t', + device.deviceId, + 'shell', + 'aa', + 'force-stop', + bundleName, + ]); + await spawn(hdcPath, [ + '-t', + device.deviceId, + 'shell', + 'aa', + 'start', + '-a', + args.ability, + '-b', + bundleName, + ]); + } catch (error) { + throw new RockError(`Failed to launch the app on ${device.readableName}`, { + cause: (error as SubprocessError).stderr, + }); + } + + return { applicationIdWithSuffix: bundleName }; +} diff --git a/packages/platform-harmony/src/lib/commands/run/tryRunHdcReverse.ts b/packages/platform-harmony/src/lib/commands/run/tryRunHdcReverse.ts new file mode 100644 index 000000000..01806b61f --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/run/tryRunHdcReverse.ts @@ -0,0 +1,28 @@ +import type { SubprocessError } from '@rock-js/tools'; +import { logger, RockError, spawn } from '@rock-js/tools'; +import { getHdcPath } from './hdc.js'; + +// Runs hdc rport tcp:8081 tcp:8081 to allow loading the jsbundle from the packager +export async function tryRunHdcReverse( + packagerPort: number | string, + device: string, +) { + try { + const hdcPath = getHdcPath(); + const hdcArgs = [ + '-t', + device, + 'rport', + `tcp:${packagerPort}`, + `tcp:${packagerPort}`, + ]; + + logger.debug(`Connecting "${device}" to the development server`); + await spawn(hdcPath, hdcArgs); + } catch (error) { + throw new RockError( + `Failed to connect "${device}" to development server using "hdb rport"`, + { cause: (error as SubprocessError).stderr }, + ); + } +} diff --git a/packages/platform-harmony/src/lib/commands/runHvigor.ts b/packages/platform-harmony/src/lib/commands/runHvigor.ts new file mode 100644 index 000000000..0e444c098 --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/runHvigor.ts @@ -0,0 +1,103 @@ +import path from 'node:path'; +import { + color, + logger, + RockError, + saveLocalBuildCache, + spawn, + spinner, + type SubprocessError, +} from '@rock-js/tools'; +import { getDevEcoBuildToolsPath } from '../paths.js'; +import type { BuildFlags } from './build/buildHarmony.js'; +import { findOutputFile } from './run/findOutputFile.js'; +import type { DeviceData } from './run/listHarmonyDevices.js'; +import type { Flags } from './run/runHarmony.js'; + +export type RunHvigorArgs = { + sourceDir: string; + bundleName: string; + args: BuildFlags | Flags; + artifactName: string; + device?: DeviceData; +}; + +async function runOhpm(sourceDir: string, loader: ReturnType) { + loader.message('Installing dependencies with ohpm'); + + const ohpmPath = path.join( + getDevEcoBuildToolsPath(), + 'ohpm', + 'bin', + process.platform === 'win32' ? 'ohpm.bat' : 'ohpm', + ); + + try { + await spawn(ohpmPath, ['install', '--all', '--strict_ssl', 'true'], { + cwd: sourceDir, + }); + } catch (error) { + loader.stop('Failed to install dependencies with ohpm', 1); + throw new RockError('Failed to install native dependencies with ohpm', { + cause: (error as SubprocessError).output, + }); + } +} + +export async function runHvigor({ + sourceDir, + bundleName, + args, + artifactName, + device, +}: RunHvigorArgs) { + logger.log(`Build Settings: +Bundle Name ${color.bold(bundleName)} +Build Mode ${color.bold(args.buildMode)}`); + + const loader = spinner({ indicator: 'timer' }); + const message = `Building the app`; + + loader.start(message); + + await runOhpm(sourceDir, loader); + + const hvigorPath = path.join( + getDevEcoBuildToolsPath(), + 'hvigor', + 'bin', + 'hvigorw.js', + ); + + try { + loader.message('Building the app with Hvigor'); + await spawn( + 'node', + [ + hvigorPath, + `-p`, + `module=${args.module}@default`, + `-p`, + `product=${args.product}`, + `-p`, + `buildMode=${args.buildMode}`, + `-p`, + `requiredDeviceType=phone`, + `assembleHap`, + ], + { cwd: sourceDir }, + ); + loader.stop(`Built the app`); + } catch (error) { + loader.stop('Failed to build the app', 1); + + throw new RockError('Failed to build the app with Hvigor', { + cause: (error as SubprocessError).output, + }); + } + + const outputFilePath = await findOutputFile(sourceDir, args.module, device); + if (outputFilePath) { + saveLocalBuildCache(artifactName, outputFilePath); + } +} diff --git a/packages/platform-harmony/src/lib/commands/toPascalCase.ts b/packages/platform-harmony/src/lib/commands/toPascalCase.ts new file mode 100644 index 000000000..ec25560b5 --- /dev/null +++ b/packages/platform-harmony/src/lib/commands/toPascalCase.ts @@ -0,0 +1,3 @@ +export function toPascalCase(value: string) { + return value !== '' ? value[0].toUpperCase() + value.slice(1) : value; +} diff --git a/packages/platform-harmony/src/lib/paths.ts b/packages/platform-harmony/src/lib/paths.ts new file mode 100644 index 000000000..7c4cbd763 --- /dev/null +++ b/packages/platform-harmony/src/lib/paths.ts @@ -0,0 +1,16 @@ +import path from 'node:path'; + +export function getDevEcoSdkPath() { + const sdkRoot = process.env['DEVECO_SDK_HOME']; + if (!sdkRoot) { + throw new Error( + 'DEVECO_SDK_HOME environment variable is not set. Please set it and run again', + ); + } + return sdkRoot; +} + +export function getDevEcoBuildToolsPath() { + return path.join(getDevEcoSdkPath(), '..', 'tools'); +} + diff --git a/packages/platform-harmony/src/lib/platformHarmony.ts b/packages/platform-harmony/src/lib/platformHarmony.ts new file mode 100644 index 000000000..d6d5f2160 --- /dev/null +++ b/packages/platform-harmony/src/lib/platformHarmony.ts @@ -0,0 +1,32 @@ +import type { PlatformOutput, PluginApi } from '@rock-js/config'; +import { registerBuildCommand } from './commands/build/command.js'; +import { + getValidProjectConfig, + type HarmonyProjectConfig, +} from './commands/getValidProjectConfig.js'; +import { registerRunCommand } from './commands/run/command.js'; + +type PluginConfig = HarmonyProjectConfig; + +export const platformHarmony = + (pluginConfig?: Partial) => + (api: PluginApi): PlatformOutput => { + registerBuildCommand(api, pluginConfig); + registerRunCommand(api, pluginConfig); + + return { + name: '@rock-js/platform-harmony', + description: 'Rock plugin for HarmonyOS Next.', + autolinkingConfig: { + get project() { + const harmonyConfig = getValidProjectConfig( + api.getProjectRoot(), + pluginConfig, + ); + return harmonyConfig; + }, + }, + }; + }; + +export default platformHarmony; diff --git a/packages/platform-harmony/template/harmony/.npmrc b/packages/platform-harmony/template/harmony/.npmrc new file mode 100644 index 000000000..0e39ea17c --- /dev/null +++ b/packages/platform-harmony/template/harmony/.npmrc @@ -0,0 +1 @@ +@ohos:registry=https://repo.harmonyos.com/npm/ diff --git a/packages/platform-harmony/template/harmony/AppScope/app.json5 b/packages/platform-harmony/template/harmony/AppScope/app.json5 new file mode 100644 index 000000000..bbebc1ab9 --- /dev/null +++ b/packages/platform-harmony/template/harmony/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.helloworld", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name", + } +} diff --git a/packages/platform-harmony/template/harmony/AppScope/resources/base/element/string.json b/packages/platform-harmony/template/harmony/AppScope/resources/base/element/string.json new file mode 100644 index 000000000..a804dbe3a --- /dev/null +++ b/packages/platform-harmony/template/harmony/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "HelloWorld" + } + ] +} diff --git a/packages/platform-harmony/template/harmony/AppScope/resources/base/media/app_icon.png b/packages/platform-harmony/template/harmony/AppScope/resources/base/media/app_icon.png new file mode 100644 index 000000000..ce307a882 Binary files /dev/null and b/packages/platform-harmony/template/harmony/AppScope/resources/base/media/app_icon.png differ diff --git a/packages/platform-harmony/template/harmony/build-profile.json5 b/packages/platform-harmony/template/harmony/build-profile.json5 new file mode 100644 index 000000000..2cc47dfd8 --- /dev/null +++ b/packages/platform-harmony/template/harmony/build-profile.json5 @@ -0,0 +1,36 @@ +{ + app: { + products: [ + { + name: 'default', + signingConfig: 'default', + compatibleSdkVersion: '5.0.0(12)', + runtimeOS: 'HarmonyOS', + buildOption: { + nativeCompiler: 'BiSheng' + } + }, + ], + buildModeSet: [ + { + name: 'debug', + }, + { + name: 'release', + }, + ], + signingConfigs: [], + }, + modules: [ + { + name: 'entry', + srcPath: './entry', + targets: [ + { + name: 'default', + applyToProducts: ['default'], + }, + ], + }, + ], +} diff --git a/packages/platform-harmony/template/harmony/codelinter.json b/packages/platform-harmony/template/harmony/codelinter.json new file mode 100644 index 000000000..e7f91acb0 --- /dev/null +++ b/packages/platform-harmony/template/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/packages/platform-harmony/template/harmony/entry/build-profile.json5 b/packages/platform-harmony/template/harmony/entry/build-profile.json5 new file mode 100644 index 000000000..682438e1d --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/build-profile.json5 @@ -0,0 +1,18 @@ +{ + "apiType": 'stageMode', + "buildOption": { + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + }, + }, + "targets": [ + { + "name": "default", + "runtimeOS": "HarmonyOS" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/packages/platform-harmony/template/harmony/entry/hvigorfile.ts b/packages/platform-harmony/template/harmony/entry/hvigorfile.ts new file mode 100644 index 000000000..250c18820 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/hvigorfile.ts @@ -0,0 +1,13 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; +import { createRNOHModulePlugin } from '@rnoh/hvigor-plugin'; + +export default { + system: hapTasks, + plugins: [ + createRNOHModulePlugin({ + codegen: { + rnohModulePath: './oh_modules/@rnoh/react-native-openharmony', + }, + }), + ], +}; diff --git a/packages/platform-harmony/template/harmony/entry/oh-package.json5 b/packages/platform-harmony/template/harmony/entry/oh-package.json5 new file mode 100644 index 000000000..68ea88cdf --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/oh-package.json5 @@ -0,0 +1,8 @@ +{ + "license": "ISC", + "devDependencies": {}, + "name": "entry", + "description": "", + "version": "1.0.0", + "dependencies": {} +} diff --git a/packages/platform-harmony/template/harmony/entry/src/main/cpp/CMakeLists.txt b/packages/platform-harmony/template/harmony/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..77a80d56a --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.11) +project(rnapp) + +set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_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(WITH_HITRACE_SYSTRACE ON) +set(WITH_HITRACE_REACT_MARKER ON) +set(OH_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../oh_modules") # required by 'autolink_libraries' + +add_subdirectory("${RNOH_CPP_DIR}" ./rn) + +include("${CMAKE_CURRENT_SOURCE_DIR}/autolinking.cmake") + +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) + +autolink_libraries(rnoh_app) diff --git a/packages/platform-harmony/template/harmony/entry/src/main/cpp/PackageProvider.cpp b/packages/platform-harmony/template/harmony/entry/src/main/cpp/PackageProvider.cpp new file mode 100644 index 000000000..ad4d0e267 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/cpp/PackageProvider.cpp @@ -0,0 +1,13 @@ +#include "RNOH/PackageProvider.h" +#include "RNOHPackagesFactory.h" +#include "generated/RNOHGeneratedPackage.h" + +using namespace rnoh; + +std::vector> PackageProvider::getPackages( + Package::Context ctx) { + auto packages = createRNOHPackages(ctx); // <= autolinking + packages.push_back(std::make_shared( + ctx)); // generated by codegen-harmony v1 on app preBuilt stage) + return packages; +} \ No newline at end of file diff --git a/packages/platform-harmony/template/harmony/entry/src/main/ets/PackageProvider.ets b/packages/platform-harmony/template/harmony/entry/src/main/ets/PackageProvider.ets new file mode 100644 index 000000000..346d9a6cc --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/ets/PackageProvider.ets @@ -0,0 +1,8 @@ +import type { RNPackageContext, RNOHPackage } from '@rnoh/react-native-openharmony'; +import { createRNOHPackages as createRNOHPackagesAutolinking } from "./RNOHPackagesFactory" + +export function getRNOHPackages(ctx: RNPackageContext): RNOHPackage[] { + return [ + ...createRNOHPackagesAutolinking(ctx) + ] +} diff --git a/packages/platform-harmony/template/harmony/entry/src/main/ets/entryability/EntryAbility.ets b/packages/platform-harmony/template/harmony/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 000000000..d8905c247 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,11 @@ +import {RNAbility} from '@rnoh/react-native-openharmony'; + +export default class EntryAbility extends RNAbility { + override getPagePath() { + return 'pages/Index'; + } + + override getRNOHWorkerScriptUrl() { + return "entry/ets/workers/RNOHWorker.ets" + } +} diff --git a/packages/platform-harmony/template/harmony/entry/src/main/ets/pages/Index.ets b/packages/platform-harmony/template/harmony/entry/src/main/ets/pages/Index.ets new file mode 100644 index 000000000..f3cdf8420 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,42 @@ +import { + AnyJSBundleProvider, + MetroJSBundleProvider, + RNApp, + RNOHErrorDialog, + ResourceJSBundleProvider, + RNOHCoreContext +} from '@rnoh/react-native-openharmony'; +import { getRNOHPackages } from '../PackageProvider'; + +@Entry +@Component +struct Index { + @StorageLink('RNOHCoreContext') private rnohCoreContext: RNOHCoreContext | undefined = undefined + + build() { + Column() { + if (this.rnohCoreContext) { + if (this.rnohCoreContext?.isDebugModeEnabled) { + RNOHErrorDialog({ ctx: this.rnohCoreContext }) + } + RNApp({ + rnInstanceConfig: { + name: "HelloWorld", + createRNPackages: getRNOHPackages, + fontResourceByFontFamily: {}, + enableDebugger: this.rnohCoreContext?.isDebugModeEnabled, + }, + appKey: "HelloWorld", + jsBundleProvider: this.rnohCoreContext?.isDebugModeEnabled ? + new AnyJSBundleProvider([ + new MetroJSBundleProvider(), + new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'index.jsbundle'), + ]) : + new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'index.jsbundle'), + }) + } + } + .height('100%') + .width('100%') + } +} \ No newline at end of file diff --git a/packages/platform-harmony/template/harmony/entry/src/main/ets/workers/RNOHWorker.ets b/packages/platform-harmony/template/harmony/entry/src/main/ets/workers/RNOHWorker.ets new file mode 100644 index 000000000..85f3e5378 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/ets/workers/RNOHWorker.ets @@ -0,0 +1,16 @@ +/** + * Importing some ets files in the following way results in runtime errors e.g. + * import { setupRNOHWorker } from "@rnoh/react-native-openharmony" + * + * It looks like this is ArkTS problem and it may be fixed in the future. + * For the time being, use direct imports. + */ +import { setupRNOHWorker } from "@rnoh/react-native-openharmony/src/main/ets/setupRNOHWorker" +import { getRNOHPackages } from "../PackageProvider" + +setupRNOHWorker({ + createWorkerRNInstanceConfig: (_rnInstanceName) => { + return { thirdPartyPackagesFactory: getRNOHPackages } + } +}) + diff --git a/packages/platform-harmony/template/harmony/entry/src/main/module.json5 b/packages/platform-harmony/template/harmony/entry/src/main/module.json5 new file mode 100644 index 000000000..2ab81f1fb --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/module.json5 @@ -0,0 +1,65 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "default" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + // This is needed by bundleManager.canOpenLink to check if the app can open some url + "querySchemes": [ + "maps", + "http", + "https", + "customDomain" + ], + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET" + }, + { + "name": "ohos.permission.VIBRATE" + } + ], + "metadata": [ + { + "name": "OPTLazyForEach", + "value": "true", + }, + { + "name": "can_preview_text", + "value": "true", + }, + { + "name": "halfLeading", + "value": "true", + }, + ], + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "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/packages/platform-harmony/template/harmony/entry/src/main/resources/base/element/color.json b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/element/color.json new file mode 100644 index 000000000..162a7b6f4 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} diff --git a/packages/platform-harmony/template/harmony/entry/src/main/resources/base/element/string.json b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/element/string.json new file mode 100644 index 000000000..06d1ad267 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "" + }, + { + "name": "EntryAbility_desc", + "value": "" + }, + { + "name": "EntryAbility_label", + "value": "helloworld" + } + ] +} \ No newline at end of file diff --git a/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/background.png b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/background.png new file mode 100644 index 000000000..f939c9fa8 Binary files /dev/null and b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/background.png differ diff --git a/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/foreground.png b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 000000000..4483ddad1 Binary files /dev/null and b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/foreground.png differ diff --git a/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/layered_image.json b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 000000000..fb4992044 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/startIcon.png b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 000000000..205ad8b5a Binary files /dev/null and b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/media/startIcon.png differ diff --git a/packages/platform-harmony/template/harmony/entry/src/main/resources/base/profile/main_pages.json b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 000000000..1898d94f5 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/packages/platform-harmony/template/harmony/entry/src/ohosTest/ets/test/List.test.ets b/packages/platform-harmony/template/harmony/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 000000000..4fa4dd4c1 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,2 @@ +export default function testsuite() { +} \ No newline at end of file diff --git a/packages/platform-harmony/template/harmony/entry/src/ohosTest/module.json5 b/packages/platform-harmony/template/harmony/entry/src/ohosTest/module.json5 new file mode 100644 index 000000000..55725a929 --- /dev/null +++ b/packages/platform-harmony/template/harmony/entry/src/ohosTest/module.json5 @@ -0,0 +1,13 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/packages/platform-harmony/template/harmony/gitignore b/packages/platform-harmony/template/harmony/gitignore new file mode 100644 index 000000000..682daa320 --- /dev/null +++ b/packages/platform-harmony/template/harmony/gitignore @@ -0,0 +1,19 @@ +local.properties +.idea +.hvigor +.cxx +.clangd +.clang-format +.clang-tidy +oh_modules +hvigorw +hvigorw.bat +hvigor/hvigor-wrapper.js +.preview +**/build +**/cpp/generated/ +**/cpp/RNOHPackagesFactory.h +**/cpp/autolinking.cmake +**/ets/RNOHPackagesFactory.ets +**/ets/codegen +*.bundle \ No newline at end of file diff --git a/packages/platform-harmony/template/harmony/hvigor/hvigor-config.json5 b/packages/platform-harmony/template/harmony/hvigor/hvigor-config.json5 new file mode 100644 index 000000000..9caee6568 --- /dev/null +++ b/packages/platform-harmony/template/harmony/hvigor/hvigor-config.json5 @@ -0,0 +1,23 @@ +{ + modelVersion: '5.0.0', + dependencies: { + /* @rnoh/hvigor-plugin is not available in npm */ + "@rnoh/hvigor-plugin": "file:../../node_modules/@react-native-oh/react-native-harmony-cli/harmony/rnoh-hvigor-plugin-0.77.18.tgz" + }, + 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 */ + }, +} diff --git a/packages/platform-harmony/template/harmony/hvigorfile.ts b/packages/platform-harmony/template/harmony/hvigorfile.ts new file mode 100644 index 000000000..1a6eba511 --- /dev/null +++ b/packages/platform-harmony/template/harmony/hvigorfile.ts @@ -0,0 +1,7 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; +import { createRNOHProjectPlugin } from '@rnoh/hvigor-plugin'; + +export default { + system: appTasks, + plugins: [createRNOHProjectPlugin()], +}; diff --git a/packages/platform-harmony/template/harmony/oh-package.json5 b/packages/platform-harmony/template/harmony/oh-package.json5 new file mode 100644 index 000000000..14ec5a3b5 --- /dev/null +++ b/packages/platform-harmony/template/harmony/oh-package.json5 @@ -0,0 +1,17 @@ +{ + name: "helloworld", + description: "", + version: "1.0.0", + modelVersion: "5.0.0", + license: "ISC", + repository: {}, + dependencies: { + "@rnoh/react-native-openharmony": "file:../node_modules/@react-native-oh/react-native-harmony/react_native_openharmony.har", + }, + devDependencies: { + "@ohos/hypium": "1.0.6", + }, + overrides: { + "@rnoh/react-native-openharmony": "file:../node_modules/@react-native-oh/react-native-harmony/react_native_openharmony.har", + }, +} \ No newline at end of file diff --git a/packages/platform-harmony/template/metro.config.js b/packages/platform-harmony/template/metro.config.js new file mode 100644 index 000000000..60d006926 --- /dev/null +++ b/packages/platform-harmony/template/metro.config.js @@ -0,0 +1,20 @@ +const { + createHarmonyMetroConfig, +} = require('@react-native-oh/react-native-harmony/metro.config'); +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); + +/** + * Metro configuration + * https://reactnative.dev/docs/metro + * + * @type {import('@react-native/metro-config').MetroConfig} + */ +const config = {}; + +module.exports = mergeConfig( + getDefaultConfig(__dirname), + createHarmonyMetroConfig({ + reactNativeHarmonyPackageName: '@react-native-oh/react-native-harmony', + }), + config, +); diff --git a/packages/platform-harmony/template/package.json b/packages/platform-harmony/template/package.json new file mode 100644 index 000000000..d3b9541af --- /dev/null +++ b/packages/platform-harmony/template/package.json @@ -0,0 +1,20 @@ +{ + "name": "rock-platform-harmony-template", + "scripts": { + "harmony": "rock run:harmony", + "bundle:harmony": "rock bundle --platform harmony --dev false --entry-file ./index.js --bundle-output ./harmony/entry/src/main/resources/rawfile/hermes_bundle --hermes && mv ./harmony/entry/src/main/resources/rawfile/hermes_bundle ./harmony/entry/src/main/resources/rawfile/hermes_bundle.hbc" + }, + "dependencies": { + "react": "18.3.1", + "react-native": "^0.77.1" + }, + "devDependencies": { + "@react-native-community/cli": "^15.0.1", + "@rock-js/platform-harmony": "^0.11.5", + "@react-native-oh/react-native-harmony": "0.77.18-1", + "@react-native-oh/react-native-harmony-cli": "0.77.18-1", + "@types/react": "^18.3.1", + "@types/react-test-renderer": "^18.3.1", + "react-test-renderer": "18.3.1" + } +} diff --git a/packages/platform-harmony/template/react-native.config.js b/packages/platform-harmony/template/react-native.config.js new file mode 100644 index 000000000..82cc454d9 --- /dev/null +++ b/packages/platform-harmony/template/react-native.config.js @@ -0,0 +1,11 @@ +// eslint-disable-next-line no-undef +module.exports = { + commands: [ + // eslint-disable-next-line @typescript-eslint/no-require-imports,no-undef + require('@react-native-oh/react-native-harmony-cli/dist/commands/link-harmony.js') + .commandLinkHarmony, + // eslint-disable-next-line @typescript-eslint/no-require-imports,no-undef + require('@react-native-oh/react-native-harmony-cli/dist/commands/codegen-harmony.js') + .commandCodegenHarmony, + ], +}; diff --git a/packages/platform-harmony/tsconfig.json b/packages/platform-harmony/tsconfig.json new file mode 100644 index 000000000..0e8e67d56 --- /dev/null +++ b/packages/platform-harmony/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/platform-harmony/tsconfig.lib.json b/packages/platform-harmony/tsconfig.lib.json new file mode 100644 index 000000000..ee2d31650 --- /dev/null +++ b/packages/platform-harmony/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["vite.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/platform-harmony/tsconfig.spec.json b/packages/platform-harmony/tsconfig.spec.json new file mode 100644 index 000000000..3c002c215 --- /dev/null +++ b/packages/platform-harmony/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vitest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/packages/platform-harmony/vitest-setup.ts b/packages/platform-harmony/vitest-setup.ts new file mode 100644 index 000000000..0d8bd094d --- /dev/null +++ b/packages/platform-harmony/vitest-setup.ts @@ -0,0 +1,67 @@ +import { vi } from 'vitest'; + +vi.mock('node:fs'); + +// Spawn mock for helpers inside @rock-js/tools that don't get mock with following mock +vi.mock('nano-spawn', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal(); + return { + ...actual, + default: vi.fn((file, args) => { + const nodeChildProcess = { + kill: vi.fn(), + finally: vi.fn(), + }; + if (file === 'rock' && args.includes('config')) { + return { + output: '{"dependencies":[]}', + nodeChildProcess, + }; + } + return { + output: '', + nodeChildProcess, + }; + }), + }; +}); + +vi.mock('@rock-js/tools', async (importOriginal) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const actual = await importOriginal(); + return { + ...actual, + // Logger + logger: { + ...actual.logger, + success: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + log: vi.fn(), + }, + // Prompts + intro: vi.fn(), + outro: vi.fn(), + note: vi.fn(), + promptConfirm: vi.fn(), + promptMultiSelect: vi.fn(), + promptSelect: vi.fn(), + promptText: vi.fn(), + formatArtifactName: vi + .fn() + .mockResolvedValue( + 'rock-harmony-debug-089e8a9887099a309e8a7845e697bbf705353f45', + ), + spinner: vi.fn(() => ({ + start: vi.fn(), + stop: vi.fn(), + message: vi.fn(), + })), + // Spawn + spawn: vi.fn(), + getBinaryPath: vi.fn(), + }; +}); diff --git a/packages/platform-harmony/vitest.config.ts b/packages/platform-harmony/vitest.config.ts new file mode 100644 index 000000000..2c00d5f2b --- /dev/null +++ b/packages/platform-harmony/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/platform-harmony', + + test: { + watch: false, + globals: true, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/packages/platform-harmony', + provider: 'v8', + }, + setupFiles: ['./vitest-setup.ts'], + }, +}); diff --git a/packages/tools/package.json b/packages/tools/package.json index b352e86ba..8bef04c6a 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -21,7 +21,7 @@ "@clack/prompts": "^0.11.0", "adm-zip": "^0.5.16", "appdirsjs": "^1.2.7", - "fs-fingerprint": "^0.7.0", + "fs-fingerprint": "^0.11.0", "is-unicode-supported": "^2.1.0", "nano-spawn": "^0.2.0", "picocolors": "^1.1.1", diff --git a/packages/tools/src/lib/build-cache/common.ts b/packages/tools/src/lib/build-cache/common.ts index 34477798c..0c87cdde5 100644 --- a/packages/tools/src/lib/build-cache/common.ts +++ b/packages/tools/src/lib/build-cache/common.ts @@ -108,7 +108,7 @@ export async function formatArtifactName({ raw, type, }: { - platform?: 'ios' | 'android'; + platform?: 'ios' | 'android' | 'harmony'; traits?: string[]; root: string; fingerprintOptions: FingerprintSources; diff --git a/packages/tools/src/lib/build-cache/getBinaryPath.ts b/packages/tools/src/lib/build-cache/getBinaryPath.ts index 0ecad756e..667ee07b7 100644 --- a/packages/tools/src/lib/build-cache/getBinaryPath.ts +++ b/packages/tools/src/lib/build-cache/getBinaryPath.ts @@ -84,8 +84,11 @@ async function warnIgnoredFiles( const projectRoot = getProjectRoot(); const ignorePaths = [ ...(fingerprintOptions?.ignorePaths ?? []), - // git expects relative paths - ...getAllIgnorePaths(platformName, path.relative(projectRoot, sourceDir)), + ...getAllIgnorePaths( + platformName, + path.relative(projectRoot, sourceDir), // git expects relative paths + projectRoot, + ), ]; const { output } = await spawn('git', [ 'clean', diff --git a/packages/tools/src/lib/fingerprint/ignorePaths.ts b/packages/tools/src/lib/fingerprint/ignorePaths.ts index 15ec21209..df0272e51 100644 --- a/packages/tools/src/lib/fingerprint/ignorePaths.ts +++ b/packages/tools/src/lib/fingerprint/ignorePaths.ts @@ -1,4 +1,4 @@ -// export const FINGERPRINT_IGNORE_FILENAME = '.fingerprintignore'. TODO: should we include this? +import { getGitIgnoredPaths } from 'fs-fingerprint'; function getAndroidIgnorePaths(sourceDir: string) { return [ @@ -40,22 +40,51 @@ function getIOSIgnorePaths(sourceDir: string) { ]; } -export function getDefaultIgnorePaths() { +function getHarmonyIgnorePaths(sourceDir: string) { + return [ + `${sourceDir}/.hvigor`, + `${sourceDir}/**/.idea`, + `${sourceDir}/**/oh_modules`, + `${sourceDir}/**/build`, + `${sourceDir}/**/.cxx`, + `${sourceDir}/**/.preview`, + `${sourceDir}/**/.clangd`, + `${sourceDir}/**/.clang-format`, + `${sourceDir}/**/.clang-tidy`, + `${sourceDir}/**/test`, + ]; +} + +function getDefaultIgnorePaths() { return ['**/.DS_Store']; } -export function getPlatformDirIgnorePaths(platform: string, sourceDir: string) { +function getPlatformDirIgnorePaths(platform: string, sourceDir: string) { if (platform === 'android') { return getAndroidIgnorePaths(sourceDir); } else if (platform === 'ios') { return getIOSIgnorePaths(sourceDir); + } else if (platform === 'harmony') { + return getHarmonyIgnorePaths(sourceDir); } return []; } -export function getAllIgnorePaths(platform: string, sourceDir: string) { +/** + * Returns all ignore paths for the given platform, source directory and project root. + * @param platform - The platform to get the ignore paths for. + * @param sourceDir - The relative source directory of that platform to get the ignore paths for. + * @param projectRoot - The project root to get the ignore paths for. + * @returns All ignore paths for the given platform, source directory and project root. + */ +export function getAllIgnorePaths( + platform: string, + sourceDir: string, + projectRoot: string, +) { return [ ...getDefaultIgnorePaths(), + ...getGitIgnoredPaths(projectRoot), ...getPlatformDirIgnorePaths(platform, sourceDir), ]; } diff --git a/packages/tools/src/lib/fingerprint/index.ts b/packages/tools/src/lib/fingerprint/index.ts index 43d46a8a6..e8bc36a2a 100644 --- a/packages/tools/src/lib/fingerprint/index.ts +++ b/packages/tools/src/lib/fingerprint/index.ts @@ -3,10 +3,7 @@ import path from 'node:path'; import { calculateFingerprint, type FingerprintResult } from 'fs-fingerprint'; import { getReactNativeVersion, logger } from '../../index.js'; import { spawn } from '../spawn.js'; -import { - getDefaultIgnorePaths, - getPlatformDirIgnorePaths, -} from './ignorePaths.js'; +import { getAllIgnorePaths } from './ignorePaths.js'; export type { FingerprintInputHash } from 'fs-fingerprint'; export type FingerprintSources = { @@ -16,7 +13,7 @@ export type FingerprintSources = { }; export type FingerprintOptions = { - platform: 'ios' | 'android'; + platform: 'ios' | 'android' | 'harmony'; extraSources: string[]; ignorePaths: string[]; env: string[]; @@ -47,18 +44,9 @@ export async function nativeFingerprint( const packageJSONPath = path.join(projectRoot, 'package.json'); const packageJSON = await readPackageJSON(packageJSONPath); const scripts = packageJSON['scripts']; + const sourceDir = autolinkingConfig.project[options.platform]?.sourceDir; - const platforms = Object.keys(autolinkingConfig.project).map((key) => { - return { - platform: key, - sourceDir: path.relative( - projectRoot, - autolinkingConfig.project[key].sourceDir, - ), - }; - }); - - if (platforms.length === 0) { + if (!options.platform || !sourceDir) { throw new Error('No platforms found in autolinking project config'); } @@ -72,9 +60,8 @@ export async function nativeFingerprint( } const fingerprint = await calculateFingerprint(projectRoot, { - ignoreFilePath: '.gitignore', include: [ - ...platforms.map((platform) => platform.sourceDir), + sourceDir, ...options.extraSources.map((source) => path.isAbsolute(source) ? path.relative(projectRoot, source) : source, ), @@ -92,10 +79,7 @@ export async function nativeFingerprint( ...(env ? [{ key: 'env', json: env }] : []), ], exclude: [ - ...getDefaultIgnorePaths(), - ...platforms.flatMap(({ platform, sourceDir }) => - getPlatformDirIgnorePaths(platform, sourceDir), - ), + ...getAllIgnorePaths(options.platform, sourceDir, projectRoot), ...(options.ignorePaths ?? []), ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2a14f32b..de95639e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,6 +212,25 @@ importers: specifier: ^0.5.7 version: 0.5.7 + packages/platform-harmony: + dependencies: + '@rock-js/tools': + specifier: ^0.11.5 + version: link:../tools + json5: + specifier: ^2.2.3 + version: 2.2.3 + tslib: + specifier: ^2.3.0 + version: 2.8.1 + devDependencies: + '@react-native-community/cli-types': + specifier: ^20.0.0 + version: 20.0.1 + '@rock-js/config': + specifier: ^0.11.5 + version: link:../config + packages/platform-ios: dependencies: '@react-native-community/cli-config-apple': @@ -316,13 +335,13 @@ importers: dependencies: '@callstack/repack': specifier: '>=5' - version: 5.1.3(@babel/core@7.25.2)(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-native@0.81.1(@babel/core@7.25.2)(@react-native/metro-config@0.81.0(@babel/core@7.25.2))(@types/react@19.1.9)(react@19.1.1))(webpack@5.96.1) + version: 5.1.3(@babel/core@7.25.2)(@rspack/core@1.5.2(@swc/helpers@0.5.17))(react-native@0.81.1(@babel/core@7.25.2)(@react-native/metro-config@0.81.0(@babel/core@7.25.2))(@types/react@19.1.9)(react@19.1.1))(webpack@5.96.1) '@rock-js/tools': specifier: ^0.11.5 version: link:../tools '@rspack/core': specifier: '>=1.2.8' - version: 1.4.11(@swc/helpers@0.5.17) + version: 1.5.2(@swc/helpers@0.5.17) '@swc/helpers': specifier: '>=0.5.15' version: 0.5.17 @@ -388,8 +407,8 @@ importers: specifier: ^1.2.7 version: 1.2.7 fs-fingerprint: - specifier: ^0.7.0 - version: 0.7.0 + specifier: ^0.11.0 + version: 0.11.0 is-unicode-supported: specifier: ^2.1.0 version: 2.1.0 @@ -1603,39 +1622,21 @@ packages: '@types/react': '>=16' react: '>=16' - '@module-federation/error-codes@0.17.1': - resolution: {integrity: sha512-n6Elm4qKSjwAPxLUGtwnl7qt4y1dxB8OpSgVvXBIzqI9p27a3ZXshLPLnumlpPg1Qudaj8sLnSnFtt9yGpt5yQ==} - '@module-federation/error-codes@0.18.0': resolution: {integrity: sha512-Woonm8ehyVIUPXChmbu80Zj6uJkC0dD9SJUZ/wOPtO8iiz/m+dkrOugAuKgoiR6qH4F+yorWila954tBz4uKsQ==} - '@module-federation/runtime-core@0.17.1': - resolution: {integrity: sha512-LCtIFuKgWPQ3E+13OyrVpuTPOWBMI/Ggwsq1Q874YeT8Px28b8tJRCj09DjyRFyhpSPyV/uG80T6iXPAUoLIfQ==} - '@module-federation/runtime-core@0.18.0': resolution: {integrity: sha512-ZyYhrDyVAhUzriOsVfgL6vwd+5ebYm595Y13KeMf6TKDRoUHBMTLGQ8WM4TDj8JNsy7LigncK8C03fn97of0QQ==} - '@module-federation/runtime-tools@0.17.1': - resolution: {integrity: sha512-4kr6zTFFwGywJx6whBtxsc84V+COAuuBpEdEbPZN//YLXhNB0iz2IGsy9r9wDl+06h84bD+3dQ05l9euRLgXzQ==} - '@module-federation/runtime-tools@0.18.0': resolution: {integrity: sha512-fSga9o4t1UfXNV/Kh6qFvRyZpPp3EHSPRISNeyT8ZoTpzDNiYzhtw0BPUSSD8m6C6XQh2s/11rI4g80UY+d+hA==} - '@module-federation/runtime@0.17.1': - resolution: {integrity: sha512-vKEN32MvUbpeuB/s6UXfkHDZ9N5jFyDDJnj83UTJ8n4N1jHIJu9VZ6Yi4/Ac8cfdvU8UIK9bIbfVXWbUYZUDsw==} - '@module-federation/runtime@0.18.0': resolution: {integrity: sha512-+C4YtoSztM7nHwNyZl6dQKGUVJdsPrUdaf3HIKReg/GQbrt9uvOlUWo2NXMZ8vDAnf/QRrpSYAwXHmWDn9Obaw==} - '@module-federation/sdk@0.17.1': - resolution: {integrity: sha512-nlUcN6UTEi+3HWF+k8wPy7gH0yUOmCT+xNatihkIVR9REAnr7BUvHFGlPJmx7WEbLPL46+zJUbtQHvLzXwFhng==} - '@module-federation/sdk@0.18.0': resolution: {integrity: sha512-Lo/Feq73tO2unjmpRfyyoUkTVoejhItXOk/h5C+4cistnHbTV8XHrW/13fD5e1Iu60heVdAhhelJd6F898Ve9A==} - '@module-federation/webpack-bundler-runtime@0.17.1': - resolution: {integrity: sha512-Swspdgf4PzcbvS9SNKFlBzfq8h/Qxwqjq/xRSqw1pqAZWondZQzwTTqPXhgrg0bFlz7qWjBS/6a8KuH/gRvGaQ==} - '@module-federation/webpack-bundler-runtime@0.18.0': resolution: {integrity: sha512-TEvErbF+YQ+6IFimhUYKK3a5wapD90d90sLsNpcu2kB3QGT7t4nIluE25duXuZDVUKLz86tEPrza/oaaCWTpvQ==} @@ -1894,6 +1895,7 @@ packages: '@rslib/core@0.13.0': resolution: {integrity: sha512-jRCKUQPBhIXKQT5LVC76pcpAg4H10xU8kjxxLqMW5qKTXka5PlF86zyZ/gSolgkHx+AHaFPYweGZKPxbMaI45A==} engines: {node: '>=18.12.0'} + deprecated: deprecated due to bug, please consider using 0.12.2 or >= 0.13.2 hasBin: true peerDependencies: '@microsoft/api-extractor': ^7 @@ -1904,11 +1906,6 @@ packages: typescript: optional: true - '@rspack/binding-darwin-arm64@1.4.11': - resolution: {integrity: sha512-PrmBVhR8MC269jo6uQ+BMy1uwIDx0HAJYLQRQur8gXiehWabUBCRg/d4U9KR7rLzdaSScRyc5JWXR52T7/4MfA==} - cpu: [arm64] - os: [darwin] - '@rspack/binding-darwin-arm64@1.5.2': resolution: {integrity: sha512-aO76T6VQvAFt1LJNRA5aPOJ+szeTLlzC5wubsnxgWWjG53goP+Te35kFjDIDe+9VhKE/XqRId6iNAymaEsN+Uw==} cpu: [arm64] @@ -1919,11 +1916,6 @@ packages: cpu: [arm64] os: [darwin] - '@rspack/binding-darwin-x64@1.4.11': - resolution: {integrity: sha512-YIV8Wzy+JY0SoSsVtN4wxFXOjzxxVPnVXNswrrfqVUTPr9jqGOFYUWCGpbt8lcCgfuBFm6zN8HpOsKm1xUNsVA==} - cpu: [x64] - os: [darwin] - '@rspack/binding-darwin-x64@1.5.2': resolution: {integrity: sha512-XNSmUOwdGs2PEdCKTFCC0/vu/7U9nMhAlbHJKlmdt0V4iPvFyaNWxkNdFqzLc05jlJOfgDdwbwRb91y9IcIIFQ==} cpu: [x64] @@ -1934,11 +1926,6 @@ packages: cpu: [x64] os: [darwin] - '@rspack/binding-linux-arm64-gnu@1.4.11': - resolution: {integrity: sha512-ms6uwECUIcu+6e82C5HJhRMHnfsI+l33v7XQezntzRPN0+sG3EpikEoT7SGbgt4vDwaWLR7wS20suN4qd5r3GA==} - cpu: [arm64] - os: [linux] - '@rspack/binding-linux-arm64-gnu@1.5.2': resolution: {integrity: sha512-rNxRfgC5khlrhyEP6y93+45uQ4TI7CdtWqh5PKsaR6lPepG1rH4L8VE+etejSdhzXH6wQ76Rw4wzb96Hx+5vuQ==} cpu: [arm64] @@ -1949,11 +1936,6 @@ packages: cpu: [arm64] os: [linux] - '@rspack/binding-linux-arm64-musl@1.4.11': - resolution: {integrity: sha512-9evq0DOdxMN/H8VM8ZmyY9NSuBgILNVV6ydBfVPMHPx4r1E7JZGpWeKDegZcS5Erw3sS9kVSIxyX78L5PDzzKw==} - cpu: [arm64] - os: [linux] - '@rspack/binding-linux-arm64-musl@1.5.2': resolution: {integrity: sha512-kTFX+KsGgArWC5q+jJWz0K/8rfVqZOn1ojv1xpCCcz/ogWRC/qhDGSOva6Wandh157BiR93Vfoe1gMvgjpLe5g==} cpu: [arm64] @@ -1964,11 +1946,6 @@ packages: cpu: [arm64] os: [linux] - '@rspack/binding-linux-x64-gnu@1.4.11': - resolution: {integrity: sha512-bHYFLxPPYBOSaHdQbEoCYGMQ1gOrEWj7Mro/DLfSHZi1a0okcQ2Q1y0i1DczReim3ZhLGNrK7k1IpFXCRbAobQ==} - cpu: [x64] - os: [linux] - '@rspack/binding-linux-x64-gnu@1.5.2': resolution: {integrity: sha512-Lh/6WZGq30lDV6RteQQu7Phw0RH2Z1f4kGR+MsplJ6X4JpnziDow+9oxKdu6FvFHWxHByncpveVeInusQPmL7Q==} cpu: [x64] @@ -1979,11 +1956,6 @@ packages: cpu: [x64] os: [linux] - '@rspack/binding-linux-x64-musl@1.4.11': - resolution: {integrity: sha512-wrm4E7q2k4+cwT6Uhp6hIQ3eUe/YoaUttj6j5TqHYZX6YeLrNPtD9+ne6lQQ17BV8wmm6NZsmoFIJ5xIptpRhQ==} - cpu: [x64] - os: [linux] - '@rspack/binding-linux-x64-musl@1.5.2': resolution: {integrity: sha512-CsLC/SIOIFs6CBmusSAF0FECB62+J36alMdwl7j6TgN6nX3UQQapnL1aVWuQaxU6un/1Vpim0V/EZbUYIdJQ4g==} cpu: [x64] @@ -1994,10 +1966,6 @@ packages: cpu: [x64] os: [linux] - '@rspack/binding-wasm32-wasi@1.4.11': - resolution: {integrity: sha512-hiYxHZjaZ17wQtXyLCK0IdtOvMWreGVTiGsaHCxyeT+SldDG+r16bXNjmlqfZsjlfl1mkAqKz1dg+mMX28OTqw==} - cpu: [wasm32] - '@rspack/binding-wasm32-wasi@1.5.2': resolution: {integrity: sha512-cuVbGr1b4q0Z6AtEraI3becZraPMMgZtZPRaIsVLeDXCmxup/maSAR3T6UaGf4Q2SNcFfjw4neGz5UJxPK8uvA==} cpu: [wasm32] @@ -2006,11 +1974,6 @@ packages: resolution: {integrity: sha512-cfg3niNHeJuxuml1Vy9VvaJrI/5TakzoaZvKX2g5S24wfzR50Eyy4JAsZ+L2voWQQp1yMJbmPYPmnTCTxdJQBQ==} cpu: [wasm32] - '@rspack/binding-win32-arm64-msvc@1.4.11': - resolution: {integrity: sha512-+HF/mnjmTr8PC1dccRt1bkrD2tPDGeqvXC1BBLYd/Klq1VbtIcnrhfmvQM6KaXbiLcY9VWKzcZPOTmnyZ8TaHQ==} - cpu: [arm64] - os: [win32] - '@rspack/binding-win32-arm64-msvc@1.5.2': resolution: {integrity: sha512-4vJQdzRTSuvmvL3vrOPuiA7f9v9frNc2RFWDxqg+GYt0YAjDStssp+lkVbRYyXnTYVJkARSuO6N+BOiI+kLdsQ==} cpu: [arm64] @@ -2021,11 +1984,6 @@ packages: cpu: [arm64] os: [win32] - '@rspack/binding-win32-ia32-msvc@1.4.11': - resolution: {integrity: sha512-EU2fQGwrRfwFd/tcOInlD0jy6gNQE4Q3Ayj0Is+cX77sbhPPyyOz0kZDEaQ4qaN2VU8w4Hu/rrD7c0GAKLFvCw==} - cpu: [ia32] - os: [win32] - '@rspack/binding-win32-ia32-msvc@1.5.2': resolution: {integrity: sha512-zPbu3lx/NrNxdjZzTIjwD0mILUOpfhuPdUdXIFiOAO8RiWSeQpYOvyI061s/+bNOmr4A+Z0uM0dEoOClfkhUFg==} cpu: [ia32] @@ -2036,11 +1994,6 @@ packages: cpu: [ia32] os: [win32] - '@rspack/binding-win32-x64-msvc@1.4.11': - resolution: {integrity: sha512-1Nc5ZzWqfvE+iJc47qtHFzYYnHsC3awavXrCo74GdGip1vxtksM3G30BlvAQHHVtEmULotWqPbjZpflw/Xk9Ag==} - cpu: [x64] - os: [win32] - '@rspack/binding-win32-x64-msvc@1.5.2': resolution: {integrity: sha512-duLNUTshX38xhC10/W9tpkPca7rOifP2begZjdb1ikw7C4AI0I7VnBnYt8qPSxGISoclmhOBxU/LuAhS8jMMlg==} cpu: [x64] @@ -2051,24 +2004,12 @@ packages: cpu: [x64] os: [win32] - '@rspack/binding@1.4.11': - resolution: {integrity: sha512-maGl/zRwnl0QVwkBCkgjn5PH20L9HdlRIdkYhEsfTepy5x2QZ0ti/0T49djjTJQrqb+S1i6wWQymMMMMMsxx6Q==} - '@rspack/binding@1.5.2': resolution: {integrity: sha512-NKiBcsxmAzFDYRnK2ZHWbTtDFVT5/704eK4OfpgsDXPMkaMnBKijMKNgP5pbe18X4rUlz+8HnGm4+Xllo9EESw==} '@rspack/binding@1.5.8': resolution: {integrity: sha512-/91CzhRl9r5BIQCgGsS7jA6MDbw1I2BQpbfcUUdkdKl2P79K3Zo/Mw/TvKzS86catwLaUQEgkGRmYawOfPg7ow==} - '@rspack/core@1.4.11': - resolution: {integrity: sha512-JtKnL6p7Kc/YgWQJF3Woo4OccbgKGyT/4187W4dyex8BMkdQcbqCNIdi6dFk02hwQzxpOOxRSBI4hlGRbz7oYQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - '@swc/helpers': '>=0.5.1' - peerDependenciesMeta: - '@swc/helpers': - optional: true - '@rspack/core@1.5.2': resolution: {integrity: sha512-ifjHqLczC81d1xjXPXCzxTFKNOFsEzuuLN44cMnyzQ/GWi4B48fyX7JHndWE7Lxd54cW1O9Ik7AdBN3Gq891EA==} engines: {node: '>=18.12.0'} @@ -2541,9 +2482,6 @@ packages: '@types/node@20.19.17': resolution: {integrity: sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==} - '@types/node@24.2.1': - resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==} - '@types/react@19.1.9': resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==} @@ -3930,8 +3868,8 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} - fs-fingerprint@0.7.0: - resolution: {integrity: sha512-bEfz/cgk+gU9m9HvgV4oySvUrxgR91/UHNKvSY9rfXy64P4cq6SzS1LibaPySyqaygrdKGHxPn0AvUaTjWZkaA==} + fs-fingerprint@0.11.0: + resolution: {integrity: sha512-EpEtmn1T9bLxxf+506gVdpehs6pAIFAM6UCDtT9/J7tfLXg8FPn+3jmuVnMjjRFshJohR2lb2TZGwuZAhIOcKg==} engines: {node: '>=20.0.0'} fs.realpath@1.0.0: @@ -6247,9 +6185,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.10.0: - resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} - unhead@2.0.17: resolution: {integrity: sha512-xX3PCtxaE80khRZobyWCVxeFF88/Tg9eJDcJWY9us727nsTC7C449B8BUfVBmiF2+3LjPcmqeoB2iuMs0U4oJQ==} @@ -7824,7 +7759,7 @@ snapshots: - supports-color - utf-8-validate - '@callstack/repack@5.1.3(@babel/core@7.25.2)(@rspack/core@1.4.11(@swc/helpers@0.5.17))(react-native@0.81.1(@babel/core@7.25.2)(@react-native/metro-config@0.81.0(@babel/core@7.25.2))(@types/react@19.1.9)(react@19.1.1))(webpack@5.96.1)': + '@callstack/repack@5.1.3(@babel/core@7.25.2)(@rspack/core@1.5.2(@swc/helpers@0.5.17))(react-native@0.81.1(@babel/core@7.25.2)(@react-native/metro-config@0.81.0(@babel/core@7.25.2))(@types/react@19.1.9)(react@19.1.1))(webpack@5.96.1)': dependencies: '@callstack/repack-dev-server': 5.1.3 '@discoveryjs/json-ext': 0.5.7 @@ -7851,7 +7786,7 @@ snapshots: throttleit: 2.1.0 webpack-merge: 6.0.1 optionalDependencies: - '@rspack/core': 1.4.11(@swc/helpers@0.5.17) + '@rspack/core': 1.5.2(@swc/helpers@0.5.17) webpack: 5.96.1 transitivePeerDependencies: - '@babel/core' @@ -8447,51 +8382,26 @@ snapshots: '@types/react': 19.1.9 react: 19.1.1 - '@module-federation/error-codes@0.17.1': {} - '@module-federation/error-codes@0.18.0': {} - '@module-federation/runtime-core@0.17.1': - dependencies: - '@module-federation/error-codes': 0.17.1 - '@module-federation/sdk': 0.17.1 - '@module-federation/runtime-core@0.18.0': dependencies: '@module-federation/error-codes': 0.18.0 '@module-federation/sdk': 0.18.0 - '@module-federation/runtime-tools@0.17.1': - dependencies: - '@module-federation/runtime': 0.17.1 - '@module-federation/webpack-bundler-runtime': 0.17.1 - '@module-federation/runtime-tools@0.18.0': dependencies: '@module-federation/runtime': 0.18.0 '@module-federation/webpack-bundler-runtime': 0.18.0 - '@module-federation/runtime@0.17.1': - dependencies: - '@module-federation/error-codes': 0.17.1 - '@module-federation/runtime-core': 0.17.1 - '@module-federation/sdk': 0.17.1 - '@module-federation/runtime@0.18.0': dependencies: '@module-federation/error-codes': 0.18.0 '@module-federation/runtime-core': 0.18.0 '@module-federation/sdk': 0.18.0 - '@module-federation/sdk@0.17.1': {} - '@module-federation/sdk@0.18.0': {} - '@module-federation/webpack-bundler-runtime@0.17.1': - dependencies: - '@module-federation/runtime': 0.17.1 - '@module-federation/sdk': 0.17.1 - '@module-federation/webpack-bundler-runtime@0.18.0': dependencies: '@module-federation/runtime': 0.18.0 @@ -8872,65 +8782,42 @@ snapshots: transitivePeerDependencies: - '@typescript/native-preview' - '@rspack/binding-darwin-arm64@1.4.11': - optional: true - '@rspack/binding-darwin-arm64@1.5.2': optional: true '@rspack/binding-darwin-arm64@1.5.8': optional: true - '@rspack/binding-darwin-x64@1.4.11': - optional: true - '@rspack/binding-darwin-x64@1.5.2': optional: true '@rspack/binding-darwin-x64@1.5.8': optional: true - '@rspack/binding-linux-arm64-gnu@1.4.11': - optional: true - '@rspack/binding-linux-arm64-gnu@1.5.2': optional: true '@rspack/binding-linux-arm64-gnu@1.5.8': optional: true - '@rspack/binding-linux-arm64-musl@1.4.11': - optional: true - '@rspack/binding-linux-arm64-musl@1.5.2': optional: true '@rspack/binding-linux-arm64-musl@1.5.8': optional: true - '@rspack/binding-linux-x64-gnu@1.4.11': - optional: true - '@rspack/binding-linux-x64-gnu@1.5.2': optional: true '@rspack/binding-linux-x64-gnu@1.5.8': optional: true - '@rspack/binding-linux-x64-musl@1.4.11': - optional: true - '@rspack/binding-linux-x64-musl@1.5.2': optional: true '@rspack/binding-linux-x64-musl@1.5.8': optional: true - '@rspack/binding-wasm32-wasi@1.4.11': - dependencies: - '@napi-rs/wasm-runtime': 1.0.3 - optional: true - '@rspack/binding-wasm32-wasi@1.5.2': dependencies: '@napi-rs/wasm-runtime': 1.0.3 @@ -8941,46 +8828,24 @@ snapshots: '@napi-rs/wasm-runtime': 1.0.5 optional: true - '@rspack/binding-win32-arm64-msvc@1.4.11': - optional: true - '@rspack/binding-win32-arm64-msvc@1.5.2': optional: true '@rspack/binding-win32-arm64-msvc@1.5.8': optional: true - '@rspack/binding-win32-ia32-msvc@1.4.11': - optional: true - '@rspack/binding-win32-ia32-msvc@1.5.2': optional: true '@rspack/binding-win32-ia32-msvc@1.5.8': optional: true - '@rspack/binding-win32-x64-msvc@1.4.11': - optional: true - '@rspack/binding-win32-x64-msvc@1.5.2': optional: true '@rspack/binding-win32-x64-msvc@1.5.8': optional: true - '@rspack/binding@1.4.11': - optionalDependencies: - '@rspack/binding-darwin-arm64': 1.4.11 - '@rspack/binding-darwin-x64': 1.4.11 - '@rspack/binding-linux-arm64-gnu': 1.4.11 - '@rspack/binding-linux-arm64-musl': 1.4.11 - '@rspack/binding-linux-x64-gnu': 1.4.11 - '@rspack/binding-linux-x64-musl': 1.4.11 - '@rspack/binding-wasm32-wasi': 1.4.11 - '@rspack/binding-win32-arm64-msvc': 1.4.11 - '@rspack/binding-win32-ia32-msvc': 1.4.11 - '@rspack/binding-win32-x64-msvc': 1.4.11 - '@rspack/binding@1.5.2': optionalDependencies: '@rspack/binding-darwin-arm64': 1.5.2 @@ -9007,14 +8872,6 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 1.5.8 '@rspack/binding-win32-x64-msvc': 1.5.8 - '@rspack/core@1.4.11(@swc/helpers@0.5.17)': - dependencies: - '@module-federation/runtime-tools': 0.17.1 - '@rspack/binding': 1.4.11 - '@rspack/lite-tapable': 1.0.1 - optionalDependencies: - '@swc/helpers': 0.5.17 - '@rspack/core@1.5.2(@swc/helpers@0.5.17)': dependencies: '@module-federation/runtime-tools': 0.18.0 @@ -9591,7 +9448,7 @@ snapshots: '@types/adm-zip@0.5.7': dependencies: - '@types/node': 24.2.1 + '@types/node': 20.19.17 '@types/babel__code-frame@7.0.6': {} @@ -9690,10 +9547,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.2.1': - dependencies: - undici-types: 7.10.0 - '@types/react@19.1.9': dependencies: csstype: 3.1.3 @@ -11324,11 +11177,10 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 - fs-fingerprint@0.7.0: + fs-fingerprint@0.11.0: dependencies: - ignore: 7.0.5 p-limit: 7.1.1 - picomatch: 4.0.3 + tinyglobby: 0.2.15 fs.realpath@1.0.0: {} @@ -14208,8 +14060,6 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.10.0: {} - unhead@2.0.17: dependencies: hookable: 5.5.3 @@ -14412,7 +14262,7 @@ snapshots: picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.46.2 - tinyglobby: 0.2.14 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 20.19.17 fsevents: 2.3.3 diff --git a/website/src/docs/cli/introduction.md b/website/src/docs/cli/introduction.md index ddcf1cdb7..88427de4b 100644 --- a/website/src/docs/cli/introduction.md +++ b/website/src/docs/cli/introduction.md @@ -18,7 +18,7 @@ npx rock [command] [options] The CLI handles all essential build and deployment tasks: -- Building and running APK/APP files on devices and simulators +- Building and running APK/APP/HAP files on devices and simulators - Creating builds for different variants and configurations - Generating signed IPA and AAB archives for app stores - Re-signing archives with fresh JS bundles @@ -127,57 +127,14 @@ Platform plugins are configured through the [`platform`](/docs/configuration/ind | `run:ios` | Runs iOS app on simulator or device | | `sign:ios` | Signs iOS app with certificate and provisioning profile | -## Command Options +- `@rock-js/platform-harmony` – HarmonyOS platform plugin (experimental) with the following commands: -### `rock start` Options - -The `start` command launches a development server (either Re.Pack or Metro, depending on your bundler plugin) that connects to your apps through port 8081 by default. It provides features like Hot Module Reloading (HMR) and error reporting. + | Command | Description | + | :-------------- | :------------------------------------------ | + | `build:harmony` | Builds HarmonyOS app for emulator or device | + | `run:harmony` | Runs HarmonyOS app on device | -| Option | Description | -| :------------------------------------------------ | :------------------------------------------------------------------------------------------ | -| `--port ` | Port to run the server on (default: 8081) | -| `--host ` | Host to run the server on (default: "") | -| `--project-root `, `--projectRoot ` | Path to a custom project root | -| `--watch-folders `, `--watchFolders ` | Specify any additional folders to be added to the watch list | -| `--asset-plugins `, `--assetPlugins ` | Specify any additional asset plugins to be used by the packager by full filepath | -| `--source-exts `,`--sourceExts ` | Specify any additional source extensions to be used by the packager | -| `--max-workers ` | Specifies the maximum number of workers the worker-pool will spawn for transforming files | -| `--transformer ` | Specify a custom transformer to be used | -| `--reset-cache`, `--resetCache` | Removes cached files | -| `--custom-log-reporter-path ` | Path to a JavaScript file that exports a log reporter as a replacement for TerminalReporter | -| `--https` | Enables https connections to the server | -| `--key ` | Path to custom SSL key | -| `--cert ` | Path to custom SSL cert | -| `--config ` | Path to the CLI configuration file | -| `--no-interactive` | Disables interactive mode | -| `--client-logs` | [Deprecated] Enable plain text JavaScript log streaming for all connected apps | - -### `rock bundle` Options - -The `bundle` command creates an optimized JavaScript bundle for your application, optionally using Hermes bytecode. - -| Option | Description | -| :-------------------------------------- | :--------------------------------------------------------------------------------------------------- | -| `--entry-file ` | Path to the root JS file, either absolute or relative to JS root | -| `--platform ` | Either "ios" or "android" (default: "ios") | -| `--transformer ` | Specify a custom transformer to be used | -| `--dev [boolean]` | If false, warnings are disabled and the bundle is minified (default: true) | -| `--minify [boolean]` | Allows overriding whether bundle is minified. Defaults to false if dev is true, true if dev is false | -| `--bundle-output ` | File name where to store the resulting bundle, ex. /tmp/groups.bundle | -| `--bundle-encoding ` | Encoding the bundle should be written in (default: "utf8") | -| `--max-workers ` | Specifies the maximum number of workers the worker-pool will spawn for transforming files | -| `--sourcemap-output ` | File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map | -| `--sourcemap-sources-root ` | Path to make sourcemap's sources entries relative to, ex. /root/dir | -| `--sourcemap-use-absolute-path` | Report SourceMapURL using its full path (default: false) | -| `--assets-dest ` | Directory name where to store assets referenced in the bundle | -| `--unstable-transform-profile ` | Experimental, transform JS for a specific JS engine (default: "default") | -| `--asset-catalog-dest [string]` | Path where to create an iOS Asset Catalog for images | -| `--reset-cache` | Removes cached files (default: false) | -| `--read-global-cache` | Try to fetch transformed JS code from the global cache, if configured (default: false) | -| `--config ` | Path to the CLI configuration file | -| `--resolver-option ` | Custom resolver options of the form key=value. URL-encoded. May be specified multiple times | -| `--config-cmd [string]` | [Internal] A hack for Xcode build script pointing to wrong bundle command | -| `--hermes` | Passes the output JS bundle to Hermes compiler and outputs a bytecode file | +## Platform iOS ### `rock build:ios` Options @@ -252,6 +209,8 @@ The `sign:ios` command either signs your iOS app with certificates and provision | `--jsbundle ` | Path to JS bundle to apply before signing | | `--no-hermes` | Don't use Hermes for JS bundle | +## Platform Android + ### `rock build:android` Options The `build:android` command builds your Android app for emulators, devices, or distribution, producing either APK or AAB files. It follows this build strategy: @@ -300,6 +259,126 @@ The `sign:android ` command signs your Android app with a keystore, | `--jsbundle ` | Path to JS bundle to apply before signing | | `--no-hermes` | Don't use Hermes for JS bundle | +## Platform HarmonyOS (experimental) + +:::warning +HarmonyOS integration is currently experimental and not fully feature complete with iOS and Android platforms. The API and functionality may change in future releases. + +Missing functionality: + +- Ready to use GitHub Action +- Re-signing with `sign:harmony` command +- Running on emulator (DevEco Studio doesn't allow for emulators outside of China) + +::: + +### `rock build:harmony` Options + +The `build:harmony` command builds your HarmonyOS app for emulators or devices, producing HAP files. It follows this build strategy: + +1. Build locally if `--local` flag is set +1. Otherwise, try to use a cached build from cache (in `.rock` folder) + +The build cache is populated by a local build only for now (remote cache is not supported yet). + +| Option | Description | +| :---------------------- | :---------------------------- | +| `--build-mode ` | Build mode (debug/release) | +| `--module ` | Module to build | +| `--product ` | Product to build | +| `--local` | Force local build with Hvigor | + +### `rock run:harmony` Options + +The `run:harmony` command runs your HarmonyOS app on an emulator or device. It extends the functionality of `build:harmony` with additional runtime options. + +Same as for `build:harmony` and: + +| Option | Description | +| :----------------------- | :------------------------------------- | +| `--port ` | Bundler port (default: 8081) | +| `--build-mode ` | Build mode (debug/release) | +| `--product ` | Product to build | +| `--binary-path ` | Path to pre-built HAP binary | +| `--device ` | Device/emulator to use (by name or ID) | +| `--local` | Force local build with Hvigor | +| `--ability ` | Name of the ability to start | + +## Plugin Bundler + +### `rock start` Options + +The `start` command launches a development server (either Re.Pack or Metro, depending on your bundler plugin) that connects to your apps through port 8081 by default. It provides features like Hot Module Reloading (HMR) and error reporting. + +| Option | Description | +| :------------------------------------------------ | :------------------------------------------------------------------------------------------ | +| `--port ` | Port to run the server on (default: 8081) | +| `--host ` | Host to run the server on (default: "") | +| `--project-root `, `--projectRoot ` | Path to a custom project root | +| `--watch-folders `, `--watchFolders ` | Specify any additional folders to be added to the watch list | +| `--asset-plugins `, `--assetPlugins ` | Specify any additional asset plugins to be used by the packager by full filepath | +| `--source-exts `,`--sourceExts ` | Specify any additional source extensions to be used by the packager | +| `--max-workers ` | Specifies the maximum number of workers the worker-pool will spawn for transforming files | +| `--transformer ` | Specify a custom transformer to be used | +| `--reset-cache`, `--resetCache` | Removes cached files | +| `--custom-log-reporter-path ` | Path to a JavaScript file that exports a log reporter as a replacement for TerminalReporter | +| `--https` | Enables https connections to the server | +| `--key ` | Path to custom SSL key | +| `--cert ` | Path to custom SSL cert | +| `--config ` | Path to the CLI configuration file | +| `--no-interactive` | Disables interactive mode | +| `--client-logs` | [Deprecated] Enable plain text JavaScript log streaming for all connected apps | + +### `rock bundle` Options + +The `bundle` command creates an optimized JavaScript bundle for your application, optionally using Hermes bytecode. + +| Option | Description | +| :-------------------------------------- | :--------------------------------------------------------------------------------------------------- | +| `--entry-file ` | Path to the root JS file, either absolute or relative to JS root | +| `--platform ` | Either "ios", "android", or "harmony" (default: "ios") | +| `--transformer ` | Specify a custom transformer to be used | +| `--dev [boolean]` | If false, warnings are disabled and the bundle is minified (default: true) | +| `--minify [boolean]` | Allows overriding whether bundle is minified. Defaults to false if dev is true, true if dev is false | +| `--bundle-output ` | File name where to store the resulting bundle, ex. /tmp/groups.bundle | +| `--bundle-encoding ` | Encoding the bundle should be written in (default: "utf8") | +| `--max-workers ` | Specifies the maximum number of workers the worker-pool will spawn for transforming files | +| `--sourcemap-output ` | File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map | +| `--sourcemap-sources-root ` | Path to make sourcemap's sources entries relative to, ex. /root/dir | +| `--sourcemap-use-absolute-path` | Report SourceMapURL using its full path (default: false) | +| `--assets-dest ` | Directory name where to store assets referenced in the bundle | +| `--unstable-transform-profile ` | Experimental, transform JS for a specific JS engine (default: "default") | +| `--asset-catalog-dest [string]` | Path where to create an iOS Asset Catalog for images | +| `--reset-cache` | Removes cached files (default: false) | +| `--read-global-cache` | Try to fetch transformed JS code from the global cache, if configured (default: false) | +| `--config ` | Path to the CLI configuration file | +| `--resolver-option ` | Custom resolver options of the form key=value. URL-encoded. May be specified multiple times | +| `--config-cmd [string]` | [Internal] A hack for Xcode build script pointing to wrong bundle command | +| `--hermes` | Passes the output JS bundle to Hermes compiler and outputs a bytecode file | + +## Built-in plugins + +### `rock fingerprint` Options + +The `fingerprint` command calculates a unique hash that represents your project's native state. This hash is used for build caching and remains stable across builds unless you modify native files, change dependencies with native code, or update scripts in package.json. + +| Option | Description | +| :------------------------ | :--------------------------------------------- | +| `-p, --platform ` | Select platform, e.g. ios, android, or harmony | +| `--raw` | Output the raw fingerprint hash for piping | + +**Arguments:** + +- `[path]` - Directory to calculate fingerprint for (optional) + +### `rock config` Options + +The `config` command outputs the autolinking configuration from Community CLI, which is useful for debugging and understanding how dependencies are linked. + +| Option | Description | +| :------------------------ | :--------------------------------------------- | +| `-p, --platform ` | Select platform, e.g. ios, android, or harmony | + ### `rock clean` Options The `clean` command helps you free up disk space by removing various caches and temporary files from your React Native project. It can clean Android (Gradle), iOS (CocoaPods), Metro, Watchman, Rock's own project caches, and package manager caches. @@ -310,6 +389,8 @@ The `clean` command helps you free up disk space by removing various caches and | `--verify-cache` | Whether to verify the cache (currently only applies to npm cache) | | `--all` | Clean all available caches without interactive prompt | +## Plugin Remote Cache + ### `rock remote-cache` Actions and Options The `remote-cache ` command provides utilities to interact with the remote build cache configured via your `remoteCacheProvider`. This is useful for inspecting, downloading, uploading, or deleting build artifacts stored remotely. @@ -333,7 +414,7 @@ Actions have different options available: | `--name ` | Full artifact name to operate on. Cannot be used with `--platform` or `--traits` | | `--all` | List or delete all matching artifacts (affects `list` and `delete` actions only) | | `--all-but-latest` | Delete all but the latest matching artifact (affects `delete` action only) | -| `-p, --platform ` | Platform to target (`ios` or `android`). Must be used with `--traits` | +| `-p, --platform ` | Platform to target (`ios`, `android`, or `harmony`). Must be used with `--traits` | | `-t, --traits ` | Comma-separated traits that construct the final artifact name. For Android: variant (e.g., `debug`, `release`). For iOS: destination and configuration (e.g., `simulator,Release`) | | `--binary-path ` | Path to the binary to upload (used with `upload` action) | | `--ad-hoc ` | Upload IPA for ad-hoc distribution and installation from URL. Additionally uploads index.html and manifest.plist' | diff --git a/website/src/docs/getting-started.mdx b/website/src/docs/getting-started.mdx index d4d53ccb4..cab9c15d6 100644 --- a/website/src/docs/getting-started.mdx +++ b/website/src/docs/getting-started.mdx @@ -56,6 +56,12 @@ To build and run your app on an Android emulator or device, run the `run:android +### Running the HarmonyOS app (experimental) + +To build and run your app on a HarmonyOS emulator or device, run the `run:harmony` command: + + + ## Next Steps - Learn about [CLI commands and features](/docs/cli/introduction) diff --git a/website/src/docs/introduction.md b/website/src/docs/introduction.md index 6a98930c5..2460cc9a3 100644 --- a/website/src/docs/introduction.md +++ b/website/src/docs/introduction.md @@ -11,13 +11,13 @@ Choose your path: [Getting Started →](/docs/getting-started) Rock is built for two kinds of teams: - **Existing React Native teams using Community CLI** who want to improve build times and developer experience while fitting into your existing workflows and infrastructure. -- **iOS/Android native teams** planning to incorporate React Native without disrupting existing workflows: Rock Brownfield lets you add your whole React Native app like any other dependency. +- **iOS/Android teams** planning to incorporate React Native without disrupting existing workflows: Rock Brownfield lets you add your whole React Native app like any other dependency. :::info New to React Native and building app from scratch? For **new projects that aren't brownfield**, consider starting with [Expo](https://expo.dev) for the best developer experience and similar remote caching capabilities. We recommend using [this template](https://github.com/nkzw-tech/expo-app-template) for sensible defaults. Rock is designed for teams who have outgrown the Community CLI. ::: -Both types of teams will benefit from Rock's cross‑platform reach: iOS and Android by default, with a flexible architecture that extends to TVs, macOS, Windows, and HarmonyOS (coming soon). +Both types of teams will benefit from Rock's cross‑platform reach: iOS, Android, and experimental HarmonyOS by default, with a flexible architecture that extends to TVs, macOS, and Windows (coming soon). ## Why We Exist @@ -25,7 +25,7 @@ At [Callstack](https://callstack.com/), we work with large teams building comple - **Build times** – No reuse of builds across CI jobs and development teams - **Infrastructure control** – Need to host everything on their own infrastructure -- **Platform diversity** – Shipping to 10+ platforms beyond iOS and Android +- **Platform diversity** – Shipping to 10+ platforms beyond iOS, Android, and HarmonyOS (experimental) - **Brownfield integration** – Embedding React Native in existing native apps - **Tech stack complexity** – Adding React Native to mixed technology environments diff --git a/website/src/index.md b/website/src/index.md index 571a808ad..14f548ef9 100644 --- a/website/src/index.md +++ b/website/src/index.md @@ -27,7 +27,7 @@ features: details: A plugin‑driven architecture that lets you customize platforms, bundlers, cache providers, and more. icon: - title: Cross‑platform ready - details: iOS and Android by default; designed to extend to TVs, macOS, and Windows (coming soon). + details: iOS, Android, and experimental HarmonyOS by default; designed to extend to TVs, macOS, and Windows (coming soon). icon: - title: Easy Community CLI migration details: A familiar CLI that helps you develop, run, and build your app. Integrates with Remote Build Cache. Migrate from Community CLI in minutes.