Skip to content

Commit b47210e

Browse files
authored
feat: add experimental support for HarmonyOS platform (#600)
* wip: initial harmony support * make run and build work; remove signing for now * add template * fixups * fingerprints * add missing files and deps; change js entrypoint * Skip copying metro.config.js for Harmony platform * fixup template * update fs-fingerprint, platform config, tests, validation * add basic dependency config * fix hvigor-plugin dep * use React 18 * resolve @ohos from harmonyos registry; pnpm hoisted; community cli * fix: use PascalCase * versions and fixups * add display name; update tests * fixup nits from cursor review * add/remove todos * docs: add docs * update docs * moar docs updates * fix linter * changeset * add harmony to supported types in fingerprint * fix tests
1 parent 1e69b57 commit b47210e

File tree

85 files changed

+2134
-293
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+2134
-293
lines changed

.changeset/free-squids-walk.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@rock-js/platform-android': patch
3+
'@rock-js/platform-harmony': patch
4+
'create-rock': patch
5+
'@rock-js/tools': patch
6+
'rock': patch
7+
'rock-docs': patch
8+
---
9+
10+
feat: add experimental support for HarmonyOS platform

packages/cli/src/lib/plugins/fingerprint.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function redactSensitiveSources(sources: FingerprintInputHash[]) {
3333
}
3434

3535
type NativeFingerprintCommandOptions = {
36-
platform: 'ios' | 'android';
36+
platform: 'ios' | 'android' | 'harmony';
3737
raw?: boolean;
3838
};
3939

@@ -44,7 +44,12 @@ export async function nativeFingerprintCommand(
4444
) {
4545
validateOptions(options);
4646
const platform = options.platform;
47-
const readablePlatformName = platform === 'ios' ? 'iOS' : 'Android';
47+
const readablePlatformName =
48+
platform === 'ios'
49+
? 'iOS'
50+
: platform === 'android'
51+
? 'Android'
52+
: 'HarmonyOS';
4853

4954
if (options.raw || !isInteractive()) {
5055
const fingerprint = await nativeFingerprint(path, {
@@ -106,12 +111,16 @@ export async function nativeFingerprintCommand(
106111
function validateOptions(options: NativeFingerprintCommandOptions) {
107112
if (!options.platform) {
108113
throw new RockError(
109-
'The --platform flag is required. Please specify either "ios" or "android".',
114+
'The --platform flag is required. Please specify either "ios", "android" or "harmony".',
110115
);
111116
}
112-
if (options.platform !== 'ios' && options.platform !== 'android') {
117+
if (
118+
options.platform !== 'ios' &&
119+
options.platform !== 'android' &&
120+
options.platform !== 'harmony'
121+
) {
113122
throw new RockError(
114-
`Unsupported platform "${options.platform}". Please specify either "ios" or "android".`,
123+
`Unsupported platform "${options.platform}". Please specify either "ios", "android" or "harmony".`,
115124
);
116125
}
117126
}

packages/cli/src/lib/plugins/logConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const logConfigPlugin =
7474
options: [
7575
{
7676
name: '-p, --platform <string>',
77-
description: 'Select platform, e.g. ios or android',
77+
description: 'Select platform, e.g. ios, android, or harmony',
7878
},
7979
],
8080
});

packages/cli/src/lib/plugins/remoteCache.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,13 +426,15 @@ export const remoteCachePlugin =
426426
},
427427
{
428428
name: '-p, --platform <string>',
429-
description: 'Select platform, e.g. ios or android',
429+
description:
430+
'Select platform, e.g. ios, android, or harmony (experimental)',
430431
},
431432
{
432433
name: '-t, --traits <list>',
433434
description: `Comma-separated traits that construct final artifact name. Traits for Android are: variant; for iOS: destination and configuration.
434435
Example iOS: --traits simulator,Release
435-
Example Android: --traits debug`,
436+
Example Android: --traits debug
437+
Example Harmony: --traits debug`,
436438
parse: (val: string) => val.split(','),
437439
},
438440
{

packages/create-app/src/lib/__tests__/bin.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ test('should format config without plugins', () => {
77
.toMatchInlineSnapshot(`
88
"import { platformIOS } from '@rock-js/platform-ios';
99
import { platformAndroid } from '@rock-js/platform-android';
10+
import { platformHarmony } from '@rock-js/platform-harmony';
1011
import { pluginMetro } from '@rock-js/plugin-metro';
1112
1213
export default {
1314
bundler: pluginMetro(),
1415
platforms: {
1516
ios: platformIOS(),
1617
android: platformAndroid(),
18+
harmony: platformHarmony(),
1719
},
1820
};
1921
"
@@ -25,6 +27,7 @@ test('should format config with plugins', () => {
2527
{
2628
type: 'npm',
2729
name: 'test',
30+
displayName: 'test',
2831
packageName: '@rock-js/plugin-test',
2932
version: 'latest',
3033
directory: 'template',

packages/create-app/src/lib/__tests__/templates.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ test('resolveTemplateName with built-in templates', () => {
66
expect(resolveTemplate(TEMPLATES, 'default')).toEqual({
77
type: 'npm',
88
name: 'default',
9+
displayName: 'default',
910
packageName: '@rock-js/template-default',
1011
version: 'latest',
1112
directory: '.',
@@ -16,6 +17,7 @@ test('resolveTemplateName with local paths', () => {
1617
expect(resolveTemplate(TEMPLATES, './directory/template-1')).toEqual({
1718
type: 'local',
1819
name: 'template-1',
20+
displayName: 'template-1',
1921
localPath: path.resolve('./directory/template-1'),
2022
directory: '.',
2123
packageName: 'template-1',
@@ -24,6 +26,7 @@ test('resolveTemplateName with local paths', () => {
2426
expect(resolveTemplate(TEMPLATES, '../../up/up/away/template-2')).toEqual({
2527
type: 'local',
2628
name: 'template-2',
29+
displayName: 'template-2',
2730
localPath: path.resolve('../../up/up/away/template-2'),
2831
directory: '.',
2932
packageName: 'template-2',
@@ -32,6 +35,7 @@ test('resolveTemplateName with local paths', () => {
3235
expect(resolveTemplate(TEMPLATES, '/absolute/path/template-3')).toEqual({
3336
type: 'local',
3437
name: 'template-3',
38+
displayName: 'template-3',
3539
localPath: '/absolute/path/template-3',
3640
directory: '.',
3741
packageName: 'template-3',
@@ -42,6 +46,7 @@ test('resolveTemplateName with local paths', () => {
4246
).toEqual({
4347
type: 'local',
4448
name: 'template-4',
49+
displayName: 'template-4',
4550
localPath: '/url-based/path/template-4',
4651
directory: '.',
4752
packageName: 'template-4',
@@ -50,6 +55,7 @@ test('resolveTemplateName with local paths', () => {
5055
expect(resolveTemplate(TEMPLATES, './directory/template-5.tgz')).toEqual({
5156
type: 'local',
5257
name: 'template-5',
58+
displayName: 'template-5',
5359
localPath: path.resolve('./directory/template-5.tgz'),
5460
directory: '.',
5561
packageName: 'template-5',
@@ -58,6 +64,7 @@ test('resolveTemplateName with local paths', () => {
5864
expect(resolveTemplate(TEMPLATES, '../up/template-6.tar')).toEqual({
5965
type: 'local',
6066
name: 'template-6',
67+
displayName: 'template-6',
6168
localPath: path.resolve('../up/template-6.tar'),
6269
directory: '.',
6370
packageName: 'template-6',
@@ -66,6 +73,7 @@ test('resolveTemplateName with local paths', () => {
6673
expect(resolveTemplate(TEMPLATES, '/root/directory/template-7.tgz')).toEqual({
6774
type: 'local',
6875
name: 'template-7',
76+
displayName: 'template-7',
6977
localPath: '/root/directory/template-7.tgz',
7078
directory: '.',
7179
packageName: 'template-7',
@@ -76,20 +84,23 @@ test('resolveTemplateName with npm packages', () => {
7684
expect(resolveTemplate(TEMPLATES, 'package-name')).toEqual({
7785
type: 'npm',
7886
name: 'package-name',
87+
displayName: 'package-name',
7988
directory: '.',
8089
packageName: 'package-name',
8190
version: 'latest',
8291
});
8392
expect(resolveTemplate(TEMPLATES, '[email protected]')).toEqual({
8493
type: 'npm',
8594
name: 'package-name',
95+
displayName: 'package-name',
8696
directory: '.',
8797
packageName: 'package-name',
8898
version: '1.2.3',
8999
});
90100
expect(resolveTemplate(TEMPLATES, '@scoped/[email protected]')).toEqual({
91101
type: 'npm',
92102
name: '@scoped/package-name',
103+
displayName: '@scoped/package-name',
93104
directory: '.',
94105
packageName: '@scoped/package-name',
95106
version: '1.2.3',

packages/create-app/src/lib/bin.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,13 @@ export async function run() {
165165
for (const platform of platforms) {
166166
await extractPackage(absoluteTargetDir, platform);
167167
}
168-
await extractPackage(absoluteTargetDir, bundler);
168+
// Skip metro.config.js for Harmony platform as it's included in the platform template
169+
const skipFiles = platforms
170+
.map((platform) => platform.name)
171+
.includes('harmony')
172+
? ['metro.config.js']
173+
: undefined;
174+
await extractPackage(absoluteTargetDir, bundler, skipFiles);
169175
for (const plugin of plugins ?? []) {
170176
await extractPackage(absoluteTargetDir, plugin);
171177
}
@@ -252,7 +258,11 @@ async function installDependencies(
252258
loader.stop(`Installed dependencies with ${pkgManager}`);
253259
}
254260

255-
async function extractPackage(absoluteTargetDir: string, pkg: TemplateInfo) {
261+
async function extractPackage(
262+
absoluteTargetDir: string,
263+
pkg: TemplateInfo,
264+
skipFiles?: string[],
265+
) {
256266
let tarballPath: string | null = null;
257267
// NPM package: download tarball file
258268
if (pkg.type === 'npm') {
@@ -282,7 +292,9 @@ async function extractPackage(absoluteTargetDir: string, pkg: TemplateInfo) {
282292
fs.unlinkSync(tarballPath);
283293
}
284294

285-
copyDirSync(path.join(localPath, pkg.directory ?? ''), absoluteTargetDir);
295+
copyDirSync(path.join(localPath, pkg.directory ?? ''), absoluteTargetDir, {
296+
skipFiles,
297+
});
286298
removeDirSync(localPath);
287299

288300
return;
@@ -292,6 +304,7 @@ async function extractPackage(absoluteTargetDir: string, pkg: TemplateInfo) {
292304
copyDirSync(
293305
path.join(pkg.localPath, pkg.directory ?? ''),
294306
absoluteTargetDir,
307+
{ skipFiles },
295308
);
296309

297310
return;

packages/create-app/src/lib/templates.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type TemplateInfo = NpmTemplateInfo | LocalTemplateInfo;
99
export type NpmTemplateInfo = {
1010
type: 'npm';
1111
name: string;
12+
displayName: string;
1213
version: string;
1314
packageName: string;
1415
/** Directory inside package that contains the template */
@@ -21,6 +22,7 @@ export type LocalTemplateInfo = {
2122
type: 'local';
2223
name: string;
2324
localPath: string;
25+
displayName: string | undefined;
2426
packageName: string;
2527
directory: string | undefined;
2628
importName?: string;
@@ -31,6 +33,7 @@ export const TEMPLATES: TemplateInfo[] = [
3133
{
3234
type: 'npm',
3335
name: 'default',
36+
displayName: 'default',
3437
packageName: '@rock-js/template-default',
3538
version: 'latest',
3639
directory: '.',
@@ -41,6 +44,7 @@ export const PLUGINS: TemplateInfo[] = [
4144
{
4245
type: 'npm',
4346
name: 'brownfield-ios',
47+
displayName: 'Brownfield iOS',
4448
packageName: '@rock-js/plugin-brownfield-ios',
4549
hint: 'Setup packaging React Native app as a XCFramework',
4650
version: 'latest',
@@ -50,6 +54,7 @@ export const PLUGINS: TemplateInfo[] = [
5054
{
5155
type: 'npm',
5256
name: 'brownfield-android',
57+
displayName: 'Brownfield Android',
5358
packageName: '@rock-js/plugin-brownfield-android',
5459
hint: 'Setup packaging React Native app as an AAR',
5560
version: 'latest',
@@ -62,6 +67,7 @@ export const BUNDLERS: TemplateInfo[] = [
6267
{
6368
type: 'npm',
6469
name: 'metro',
70+
displayName: 'Metro',
6571
packageName: '@rock-js/plugin-metro',
6672
version: 'latest',
6773
directory: 'template',
@@ -70,6 +76,7 @@ export const BUNDLERS: TemplateInfo[] = [
7076
{
7177
type: 'npm',
7278
name: 'repack',
79+
displayName: 'Re.Pack',
7380
packageName: '@rock-js/plugin-repack',
7481
version: 'latest',
7582
directory: 'template',
@@ -81,6 +88,7 @@ export const PLATFORMS: TemplateInfo[] = [
8188
{
8289
type: 'npm',
8390
name: 'ios',
91+
displayName: 'iOS',
8492
packageName: '@rock-js/platform-ios',
8593
version: 'latest',
8694
directory: 'template',
@@ -89,11 +97,21 @@ export const PLATFORMS: TemplateInfo[] = [
8997
{
9098
type: 'npm',
9199
name: 'android',
100+
displayName: 'Android',
92101
packageName: '@rock-js/platform-android',
93102
version: 'latest',
94103
directory: 'template',
95104
importName: 'platformAndroid',
96105
},
106+
{
107+
type: 'npm',
108+
name: 'harmony',
109+
displayName: 'Harmony (experimental)',
110+
packageName: '@rock-js/platform-harmony',
111+
version: 'latest',
112+
directory: 'template',
113+
importName: 'platformHarmony',
114+
},
97115
];
98116

99117
export function remoteCacheProviderToImportTemplate(
@@ -175,6 +193,7 @@ export function resolveTemplate(
175193
return {
176194
type: 'local',
177195
name: basename.slice(0, basename.length - ext.length),
196+
displayName: basename.slice(0, basename.length - ext.length),
178197
localPath: resolveAbsolutePath(name),
179198
directory: '.',
180199
packageName: basename.slice(0, basename.length - ext.length),
@@ -187,6 +206,7 @@ export function resolveTemplate(
187206
return {
188207
type: 'npm',
189208
name: getNpmLibraryName(name),
209+
displayName: getNpmLibraryName(name),
190210
packageName: getNpmLibraryName(name),
191211
directory: '.',
192212
version: getNpmLibraryVersion(name) ?? 'latest',

packages/create-app/src/lib/utils/edit-template.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ const PLACEHOLDER_NAME = 'HelloWorld';
1212
*/
1313
export function renameCommonFiles(projectPath: string) {
1414
const sourceGitIgnorePath = path.join(projectPath, 'gitignore');
15-
if (!fs.existsSync(sourceGitIgnorePath)) {
16-
return;
15+
if (fs.existsSync(sourceGitIgnorePath)) {
16+
fs.renameSync(sourceGitIgnorePath, path.join(projectPath, '.gitignore'));
1717
}
1818

19-
fs.renameSync(sourceGitIgnorePath, path.join(projectPath, '.gitignore'));
19+
// Harmony platform has a separate gitignore file.
20+
const harmonyGitIgnorePath = path.join(projectPath, 'harmony', 'gitignore');
21+
if (fs.existsSync(harmonyGitIgnorePath)) {
22+
fs.renameSync(
23+
harmonyGitIgnorePath,
24+
path.join(projectPath, 'harmony', '.gitignore'),
25+
);
26+
}
2027
}
2128

2229
/**

packages/create-app/src/lib/utils/fs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function copyDirSync(
2929
} else {
3030
if (nodePath.basename(srcFile) === 'package.json') {
3131
mergePackageJsons(srcFile, distFile);
32-
} else {
32+
} else if (!skipFiles?.includes(nodePath.basename(srcFile))) {
3333
fs.copyFileSync(srcFile, distFile);
3434
}
3535
}

0 commit comments

Comments
 (0)