expo-harmony-toolkit 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +197 -0
  3. package/README.md +197 -0
  4. package/app.plugin.js +1 -0
  5. package/bin/expo-harmony.js +9 -0
  6. package/build/cli.d.ts +1 -0
  7. package/build/cli.js +56 -0
  8. package/build/commands/buildHap.d.ts +5 -0
  9. package/build/commands/buildHap.js +26 -0
  10. package/build/commands/bundle.d.ts +4 -0
  11. package/build/commands/bundle.js +18 -0
  12. package/build/commands/doctor.d.ts +7 -0
  13. package/build/commands/doctor.js +24 -0
  14. package/build/commands/env.d.ts +5 -0
  15. package/build/commands/env.js +22 -0
  16. package/build/commands/init.d.ts +5 -0
  17. package/build/commands/init.js +29 -0
  18. package/build/commands/syncTemplate.d.ts +5 -0
  19. package/build/commands/syncTemplate.js +23 -0
  20. package/build/core/build.d.ts +25 -0
  21. package/build/core/build.js +434 -0
  22. package/build/core/constants.d.ts +21 -0
  23. package/build/core/constants.js +32 -0
  24. package/build/core/env.d.ts +8 -0
  25. package/build/core/env.js +185 -0
  26. package/build/core/metadata.d.ts +9 -0
  27. package/build/core/metadata.js +54 -0
  28. package/build/core/project.d.ts +18 -0
  29. package/build/core/project.js +206 -0
  30. package/build/core/report.d.ts +4 -0
  31. package/build/core/report.js +319 -0
  32. package/build/core/template.d.ts +6 -0
  33. package/build/core/template.js +1030 -0
  34. package/build/data/compatibilityMatrix.d.ts +2 -0
  35. package/build/data/compatibilityMatrix.js +99 -0
  36. package/build/data/dependencyCatalog.d.ts +2 -0
  37. package/build/data/dependencyCatalog.js +108 -0
  38. package/build/data/uiStack.d.ts +39 -0
  39. package/build/data/uiStack.js +48 -0
  40. package/build/data/validatedMatrices.d.ts +3 -0
  41. package/build/data/validatedMatrices.js +94 -0
  42. package/build/index.d.ts +4 -0
  43. package/build/index.js +27 -0
  44. package/build/plugin.d.ts +7 -0
  45. package/build/plugin.js +76 -0
  46. package/build/types.d.ts +182 -0
  47. package/build/types.js +2 -0
  48. package/docs/cli-build.md +99 -0
  49. package/docs/npm-release.md +89 -0
  50. package/docs/official-app-shell-sample.md +39 -0
  51. package/docs/official-minimal-sample.md +32 -0
  52. package/docs/official-ui-stack-sample.md +77 -0
  53. package/docs/roadmap.md +67 -0
  54. package/docs/signing-and-release.md +57 -0
  55. package/docs/support-matrix.md +149 -0
  56. package/package.json +78 -0
  57. package/templates/harmony/AppScope/app.json5 +10 -0
  58. package/templates/harmony/AppScope/resources/base/element/string.json +8 -0
  59. package/templates/harmony/AppScope/resources/base/media/app_icon.png +0 -0
  60. package/templates/harmony/README.md +31 -0
  61. package/templates/harmony/build-profile.json5 +37 -0
  62. package/templates/harmony/codelinter.json +19 -0
  63. package/templates/harmony/entry/build-profile.json5 +18 -0
  64. package/templates/harmony/entry/hvigorfile.ts +13 -0
  65. package/templates/harmony/entry/oh-package.json5 +14 -0
  66. package/templates/harmony/entry/src/main/cpp/CMakeLists.txt +55 -0
  67. package/templates/harmony/entry/src/main/cpp/PackageProvider.cpp +12 -0
  68. package/templates/harmony/entry/src/main/ets/PackageProvider.ets +6 -0
  69. package/templates/harmony/entry/src/main/ets/entryability/EntryAbility.ets +11 -0
  70. package/templates/harmony/entry/src/main/ets/pages/Index.ets +42 -0
  71. package/templates/harmony/entry/src/main/ets/workers/RNOHWorker.ets +8 -0
  72. package/templates/harmony/entry/src/main/module.json5 +31 -0
  73. package/templates/harmony/entry/src/main/resources/base/element/color.json +8 -0
  74. package/templates/harmony/entry/src/main/resources/base/element/string.json +16 -0
  75. package/templates/harmony/entry/src/main/resources/base/media/background.png +0 -0
  76. package/templates/harmony/entry/src/main/resources/base/media/foreground.png +0 -0
  77. package/templates/harmony/entry/src/main/resources/base/media/layered_image.json +6 -0
  78. package/templates/harmony/entry/src/main/resources/base/media/startIcon.png +0 -0
  79. package/templates/harmony/entry/src/main/resources/base/profile/main_pages.json +5 -0
  80. package/templates/harmony/entry/src/main/resources/rawfile/.gitkeep +1 -0
  81. package/templates/harmony/hvigor/hvigor-config.json5 +10 -0
  82. package/templates/harmony/hvigorfile.ts +7 -0
  83. package/templates/harmony/oh-package.json5 +13 -0
@@ -0,0 +1,1030 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.initProject = initProject;
7
+ exports.syncProjectTemplate = syncProjectTemplate;
8
+ exports.buildDesiredPackageScripts = buildDesiredPackageScripts;
9
+ exports.usesExpoRouter = usesExpoRouter;
10
+ exports.resolveHarmonyBundleEntryFile = resolveHarmonyBundleEntryFile;
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ const json5_1 = __importDefault(require("json5"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const constants_1 = require("./constants");
16
+ const validatedMatrices_1 = require("../data/validatedMatrices");
17
+ const metadata_1 = require("./metadata");
18
+ const uiStack_1 = require("../data/uiStack");
19
+ const project_1 = require("./project");
20
+ const report_1 = require("./report");
21
+ const TEMPLATE_ROOT = path_1.default.resolve(__dirname, '..', '..', 'templates', 'harmony');
22
+ const TEMPLATE_FILE_PATHS = [
23
+ 'README.md',
24
+ 'build-profile.json5',
25
+ 'codelinter.json',
26
+ 'hvigor/hvigor-config.json5',
27
+ 'hvigorfile.ts',
28
+ 'AppScope/app.json5',
29
+ 'AppScope/resources/base/element/string.json',
30
+ 'AppScope/resources/base/media/app_icon.png',
31
+ 'entry/build-profile.json5',
32
+ 'entry/hvigorfile.ts',
33
+ 'entry/oh-package.json5',
34
+ 'entry/src/main/module.json5',
35
+ 'entry/src/main/cpp/CMakeLists.txt',
36
+ 'entry/src/main/cpp/PackageProvider.cpp',
37
+ 'entry/src/main/ets/PackageProvider.ets',
38
+ 'entry/src/main/ets/entryability/EntryAbility.ets',
39
+ 'entry/src/main/ets/pages/Index.ets',
40
+ 'entry/src/main/ets/workers/RNOHWorker.ets',
41
+ 'entry/src/main/resources/base/element/color.json',
42
+ 'entry/src/main/resources/base/element/string.json',
43
+ 'entry/src/main/resources/base/media/background.png',
44
+ 'entry/src/main/resources/base/media/foreground.png',
45
+ 'entry/src/main/resources/base/media/layered_image.json',
46
+ 'entry/src/main/resources/base/media/startIcon.png',
47
+ 'entry/src/main/resources/base/profile/main_pages.json',
48
+ 'entry/src/main/resources/rawfile/.gitkeep',
49
+ ];
50
+ const AUTOLINKED_FILE_PATHS = [
51
+ path_1.default.join('harmony', 'oh-package.json5'),
52
+ path_1.default.join('harmony', 'entry', 'src', 'main', 'ets', 'RNOHPackagesFactory.ets'),
53
+ path_1.default.join('harmony', 'entry', 'src', 'main', 'cpp', 'RNOHPackagesFactory.h'),
54
+ path_1.default.join('harmony', 'entry', 'src', 'main', 'cpp', 'autolinking.cmake'),
55
+ ];
56
+ async function initProject(projectRoot, force = false) {
57
+ const report = await (0, report_1.buildDoctorReport)(projectRoot);
58
+ const sync = await syncProjectTemplate(projectRoot, force);
59
+ const packageWarnings = await syncPackageScripts(projectRoot, force);
60
+ const doctorReportPath = await (0, report_1.writeDoctorReport)(projectRoot, report);
61
+ return {
62
+ report,
63
+ sync,
64
+ packageWarnings,
65
+ doctorReportPath,
66
+ };
67
+ }
68
+ async function syncProjectTemplate(projectRoot, force = false) {
69
+ const loadedProject = await (0, project_1.loadProject)(projectRoot);
70
+ const identifiers = (0, project_1.deriveHarmonyIdentifiers)(loadedProject.expoConfig, loadedProject.packageJson);
71
+ const previousToolkitConfig = await (0, metadata_1.readToolkitConfig)(loadedProject.projectRoot);
72
+ const desiredFiles = await buildManagedFiles(loadedProject, identifiers, previousToolkitConfig);
73
+ const previousManifest = await (0, metadata_1.readManifest)(loadedProject.projectRoot);
74
+ const result = {
75
+ writtenFiles: [],
76
+ unchangedFiles: [],
77
+ skippedFiles: [],
78
+ warnings: [],
79
+ manifestPath: (0, metadata_1.getManifestPath)(loadedProject.projectRoot),
80
+ };
81
+ result.warnings.push(...collectMetadataWarnings(previousManifest, previousToolkitConfig));
82
+ const manifestFiles = [];
83
+ for (const file of desiredFiles) {
84
+ const targetPath = path_1.default.join(loadedProject.projectRoot, file.relativePath);
85
+ const expectedHash = (0, project_1.createGeneratedSha)(file.contents);
86
+ const previousRecord = previousManifest?.files.find((record) => record.relativePath === file.relativePath);
87
+ if (await fs_extra_1.default.pathExists(targetPath)) {
88
+ const currentContents = await fs_extra_1.default.readFile(targetPath);
89
+ if (contentsEqual(currentContents, file.contents, file.binary)) {
90
+ result.unchangedFiles.push(file.relativePath);
91
+ manifestFiles.push({ relativePath: file.relativePath, sha1: expectedHash });
92
+ continue;
93
+ }
94
+ const currentHash = (0, project_1.createGeneratedSha)(currentContents);
95
+ const managedByToolkit = previousRecord?.sha1 === currentHash;
96
+ if (!force && !managedByToolkit) {
97
+ result.skippedFiles.push(file.relativePath);
98
+ result.warnings.push(`Skipped ${file.relativePath} because it drifted from the last generated version. Re-run with --force to overwrite it.`);
99
+ continue;
100
+ }
101
+ }
102
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(targetPath));
103
+ await fs_extra_1.default.writeFile(targetPath, file.contents);
104
+ result.writtenFiles.push(file.relativePath);
105
+ manifestFiles.push({ relativePath: file.relativePath, sha1: expectedHash });
106
+ }
107
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(result.manifestPath));
108
+ const manifest = {
109
+ generatedAt: new Date().toISOString(),
110
+ toolkitVersion: constants_1.TOOLKIT_VERSION,
111
+ templateVersion: constants_1.TEMPLATE_VERSION,
112
+ matrixId: validatedMatrices_1.DEFAULT_VALIDATED_MATRIX_ID,
113
+ projectRoot: loadedProject.projectRoot,
114
+ files: manifestFiles,
115
+ };
116
+ await fs_extra_1.default.writeJson(result.manifestPath, manifest, { spaces: 2 });
117
+ return result;
118
+ }
119
+ async function buildManagedFiles(loadedProject, identifiers, previousToolkitConfig) {
120
+ const hasExpoRouter = usesExpoRouter(loadedProject.packageJson);
121
+ const hvigorPluginFilename = await (0, project_1.resolveRnohHvigorPluginFilename)(loadedProject.projectRoot);
122
+ const renderedHarmonyRootPackage = renderTemplate(await fs_extra_1.default.readFile(path_1.default.join(TEMPLATE_ROOT, 'oh-package.json5'), 'utf8'), loadedProject, identifiers, hvigorPluginFilename);
123
+ const templateFiles = await Promise.all(TEMPLATE_FILE_PATHS.map(async (relativePath) => {
124
+ const templatePath = path_1.default.join(TEMPLATE_ROOT, relativePath);
125
+ const binary = isBinaryTemplate(relativePath);
126
+ const rawContents = await fs_extra_1.default.readFile(templatePath);
127
+ const contents = binary
128
+ ? rawContents
129
+ : renderTemplate(rawContents.toString('utf8'), loadedProject, identifiers, hvigorPluginFilename);
130
+ return {
131
+ relativePath: path_1.default.join('harmony', relativePath),
132
+ contents,
133
+ binary,
134
+ };
135
+ }));
136
+ const autolinkedFiles = await buildAutolinkedManagedFiles(loadedProject.projectRoot, renderedHarmonyRootPackage);
137
+ const nextToolkitConfig = {
138
+ generatedAt: new Date().toISOString(),
139
+ toolkitVersion: constants_1.TOOLKIT_VERSION,
140
+ templateVersion: constants_1.TEMPLATE_VERSION,
141
+ matrixId: validatedMatrices_1.DEFAULT_VALIDATED_MATRIX_ID,
142
+ rnohVersion: constants_1.RNOH_VERSION,
143
+ rnohCliVersion: constants_1.RNOH_CLI_VERSION,
144
+ bundleName: identifiers.bundleName,
145
+ entryModuleName: identifiers.entryModuleName,
146
+ project: {
147
+ name: loadedProject.expoConfig.name ?? identifiers.appName,
148
+ slug: loadedProject.expoConfig.slug ?? identifiers.slug,
149
+ version: loadedProject.expoConfig.version ?? '1.0.0',
150
+ hvigorPluginFilename,
151
+ },
152
+ };
153
+ const toolkitConfig = stabilizeToolkitConfigTimestamp(previousToolkitConfig, nextToolkitConfig);
154
+ return [
155
+ ...templateFiles,
156
+ ...autolinkedFiles,
157
+ {
158
+ relativePath: 'metro.harmony.config.js',
159
+ contents: renderMetroConfig(),
160
+ },
161
+ {
162
+ relativePath: path_1.default.join(constants_1.GENERATED_SHIMS_DIR, 'react-native-safe-area-context', 'index.js'),
163
+ contents: renderReactNativeSafeAreaContextHarmonyShim(),
164
+ },
165
+ {
166
+ relativePath: path_1.default.join(constants_1.GENERATED_SHIMS_DIR, 'expo-modules-core', 'index.js'),
167
+ contents: renderExpoModulesCoreHarmonyShim(loadedProject.expoConfig, identifiers),
168
+ },
169
+ {
170
+ relativePath: constants_1.HARMONY_RUNTIME_PRELUDE_RELATIVE_PATH,
171
+ contents: renderHarmonyRuntimePrelude(),
172
+ },
173
+ ...(hasExpoRouter
174
+ ? [
175
+ {
176
+ relativePath: constants_1.HARMONY_ROUTER_ENTRY_FILENAME,
177
+ contents: renderRouterHarmonyEntry(identifiers),
178
+ },
179
+ ]
180
+ : []),
181
+ {
182
+ relativePath: path_1.default.join(constants_1.GENERATED_DIR, constants_1.TOOLKIT_CONFIG_FILENAME),
183
+ contents: JSON.stringify(toolkitConfig, null, 2) + '\n',
184
+ },
185
+ ];
186
+ }
187
+ async function buildAutolinkedManagedFiles(projectRoot, harmonyRootPackageContents) {
188
+ const generated = await generateAutolinkingArtifacts(projectRoot, harmonyRootPackageContents);
189
+ return [
190
+ {
191
+ relativePath: AUTOLINKED_FILE_PATHS[0],
192
+ contents: generated.ohPackageContents,
193
+ },
194
+ {
195
+ relativePath: AUTOLINKED_FILE_PATHS[1],
196
+ contents: generated.etsFactoryContents,
197
+ },
198
+ {
199
+ relativePath: AUTOLINKED_FILE_PATHS[2],
200
+ contents: generated.cppFactoryContents,
201
+ },
202
+ {
203
+ relativePath: AUTOLINKED_FILE_PATHS[3],
204
+ contents: generated.cmakeContents,
205
+ },
206
+ ];
207
+ }
208
+ async function generateAutolinkingArtifacts(projectRoot, harmonyRootPackageContents) {
209
+ const rnohCliPackageJsonPath = resolveProjectPackageJson(projectRoot, '@react-native-oh/react-native-harmony-cli');
210
+ const managedOhPackageContents = await buildManagedHarmonyRootPackageContents(projectRoot, harmonyRootPackageContents);
211
+ if (!rnohCliPackageJsonPath) {
212
+ return createEmptyAutolinkingArtifacts(managedOhPackageContents);
213
+ }
214
+ try {
215
+ const temporaryRoot = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'expo-harmony-autolinking-'));
216
+ try {
217
+ const temporaryHarmonyRoot = path_1.default.join(temporaryRoot, 'harmony');
218
+ await fs_extra_1.default.ensureDir(path_1.default.join(temporaryHarmonyRoot, 'entry', 'src', 'main', 'ets'));
219
+ await fs_extra_1.default.ensureDir(path_1.default.join(temporaryHarmonyRoot, 'entry', 'src', 'main', 'cpp'));
220
+ await fs_extra_1.default.writeFile(path_1.default.join(temporaryHarmonyRoot, 'oh-package.json5'), harmonyRootPackageContents);
221
+ await runRnohLinkHarmonyCommand(projectRoot, rnohCliPackageJsonPath, temporaryHarmonyRoot);
222
+ return {
223
+ ohPackageContents: managedOhPackageContents,
224
+ etsFactoryContents: await fs_extra_1.default.readFile(path_1.default.join(temporaryHarmonyRoot, 'entry', 'src', 'main', 'ets', 'RNOHPackagesFactory.ets'), 'utf8'),
225
+ cppFactoryContents: await fs_extra_1.default.readFile(path_1.default.join(temporaryHarmonyRoot, 'entry', 'src', 'main', 'cpp', 'RNOHPackagesFactory.h'), 'utf8'),
226
+ cmakeContents: await fs_extra_1.default.readFile(path_1.default.join(temporaryHarmonyRoot, 'entry', 'src', 'main', 'cpp', 'autolinking.cmake'), 'utf8'),
227
+ };
228
+ }
229
+ finally {
230
+ await fs_extra_1.default.remove(temporaryRoot);
231
+ }
232
+ }
233
+ catch {
234
+ return createEmptyAutolinkingArtifacts(managedOhPackageContents);
235
+ }
236
+ }
237
+ async function runRnohLinkHarmonyCommand(projectRoot, rnohCliPackageJsonPath, harmonyProjectPath) {
238
+ const rnohCliRoot = path_1.default.dirname(rnohCliPackageJsonPath);
239
+ const { commandLinkHarmony } = require(path_1.default.join(rnohCliRoot, 'dist', 'commands', 'link-harmony.js'));
240
+ await commandLinkHarmony.func([], {}, {
241
+ harmonyProjectPath,
242
+ nodeModulesPath: path_1.default.join(projectRoot, 'node_modules'),
243
+ cmakeAutolinkPathRelativeToHarmony: './entry/src/main/cpp/autolinking.cmake',
244
+ cppRnohPackagesFactoryPathRelativeToHarmony: './entry/src/main/cpp/RNOHPackagesFactory.h',
245
+ etsRnohPackagesFactoryPathRelativeToHarmony: './entry/src/main/ets/RNOHPackagesFactory.ets',
246
+ ohPackagePathRelativeToHarmony: './oh-package.json5',
247
+ includeNpmPackages: uiStack_1.UI_STACK_ADAPTER_PACKAGE_NAMES,
248
+ });
249
+ }
250
+ async function buildManagedHarmonyRootPackageContents(projectRoot, harmonyRootPackageContents) {
251
+ const parsedPackageJson = json5_1.default.parse(harmonyRootPackageContents);
252
+ const dependencies = { ...(parsedPackageJson.dependencies ?? {}) };
253
+ for (const adapter of uiStack_1.UI_STACK_VALIDATED_ADAPTERS) {
254
+ const dependencySpecifier = await resolveHarmonyAdapterHarDependency(projectRoot, adapter.adapterPackageName);
255
+ if (dependencySpecifier) {
256
+ dependencies[adapter.adapterPackageName] = dependencySpecifier;
257
+ }
258
+ }
259
+ parsedPackageJson.dependencies = sortRecordByKey(dependencies);
260
+ return json5_1.default.stringify(parsedPackageJson, null, 2) + '\n';
261
+ }
262
+ async function resolveHarmonyAdapterHarDependency(projectRoot, adapterPackageName) {
263
+ const adapterEntry = uiStack_1.UI_STACK_VALIDATED_ADAPTERS.find((candidate) => candidate.adapterPackageName === adapterPackageName);
264
+ if (!adapterEntry) {
265
+ return null;
266
+ }
267
+ const adapterRoot = path_1.default.join(projectRoot, 'node_modules', ...adapterPackageName.split('/'));
268
+ const harPath = path_1.default.join(adapterRoot, 'harmony', adapterEntry.harmonyHarFileName);
269
+ if (!(await fs_extra_1.default.pathExists(harPath))) {
270
+ return null;
271
+ }
272
+ const relativeHarPath = path_1.default.relative(path_1.default.join(projectRoot, 'harmony'), harPath).replace(/\\/g, '/');
273
+ return `file:${relativeHarPath}`;
274
+ }
275
+ function createEmptyAutolinkingArtifacts(harmonyRootPackageContents) {
276
+ return {
277
+ ohPackageContents: harmonyRootPackageContents,
278
+ etsFactoryContents: `/*
279
+ * This file was generated by Expo Harmony Toolkit autolinking.
280
+ * DO NOT modify it manually, your changes WILL be overwritten.
281
+ */
282
+ import type { RNPackageContext, RNOHPackage } from '@rnoh/react-native-openharmony';
283
+
284
+ export function createRNOHPackages(_ctx: RNPackageContext): RNOHPackage[] {
285
+ return [];
286
+ }
287
+ `,
288
+ cppFactoryContents: `/*
289
+ * This file was generated by Expo Harmony Toolkit autolinking.
290
+ * DO NOT modify it manually, your changes WILL be overwritten.
291
+ */
292
+ #pragma once
293
+ #include "RNOH/Package.h"
294
+
295
+ std::vector<rnoh::Package::Shared> createRNOHPackages(const rnoh::Package::Context &_ctx) {
296
+ return {};
297
+ }
298
+ `,
299
+ cmakeContents: `# This file was generated by Expo Harmony Toolkit autolinking.
300
+ # DO NOT modify it manually, your changes WILL be overwritten.
301
+ cmake_minimum_required(VERSION 3.5)
302
+
303
+ function(autolink_libraries target)
304
+ set(AUTOLINKED_LIBRARIES
305
+ )
306
+
307
+ foreach(lib \${AUTOLINKED_LIBRARIES})
308
+ target_link_libraries(\${target} PUBLIC \${lib})
309
+ endforeach()
310
+ endfunction()
311
+ `,
312
+ };
313
+ }
314
+ function resolveProjectPackageJson(projectRoot, request) {
315
+ try {
316
+ return require.resolve(path_1.default.join(request, 'package.json'), {
317
+ paths: [projectRoot],
318
+ });
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ }
324
+ async function syncPackageScripts(projectRoot, _force) {
325
+ const packageJsonPath = path_1.default.join(projectRoot, 'package.json');
326
+ const packageJson = (await fs_extra_1.default.readJson(packageJsonPath));
327
+ const desiredScripts = buildDesiredPackageScripts(packageJson);
328
+ const scripts = { ...(packageJson.scripts ?? {}) };
329
+ const warnings = [];
330
+ let didChange = false;
331
+ for (const [scriptName, desiredCommand] of Object.entries(desiredScripts)) {
332
+ const currentCommand = scripts[scriptName];
333
+ if (!currentCommand) {
334
+ scripts[scriptName] = desiredCommand;
335
+ didChange = true;
336
+ continue;
337
+ }
338
+ if (currentCommand === desiredCommand) {
339
+ continue;
340
+ }
341
+ if (isEquivalentToolkitScript(scriptName, currentCommand, desiredCommand)) {
342
+ continue;
343
+ }
344
+ warnings.push(`Left package.json script "${scriptName}" unchanged because it already exists with different contents.`);
345
+ }
346
+ if (didChange) {
347
+ packageJson.scripts = sortRecordByKey(scripts);
348
+ await fs_extra_1.default.writeJson(packageJsonPath, packageJson, { spaces: 2 });
349
+ await fs_extra_1.default.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
350
+ }
351
+ return warnings;
352
+ }
353
+ function renderTemplate(template, loadedProject, identifiers, hvigorPluginFilename) {
354
+ const appDescription = `${identifiers.appName} official minimal Harmony sample`;
355
+ const replacements = {
356
+ APP_NAME: identifiers.appName,
357
+ APP_SLUG: identifiers.slug,
358
+ APP_VERSION: String(loadedProject.expoConfig.version ?? loadedProject.packageJson.version ?? '1.0.0'),
359
+ APP_DESCRIPTION: appDescription,
360
+ BUNDLE_NAME: identifiers.bundleName,
361
+ ENTRY_MODULE_NAME: identifiers.entryModuleName,
362
+ TEMPLATE_VERSION: constants_1.TEMPLATE_VERSION,
363
+ RNOH_VERSION: constants_1.RNOH_VERSION,
364
+ RNOH_CLI_VERSION: constants_1.RNOH_CLI_VERSION,
365
+ RNOH_HVIGOR_PLUGIN_FILENAME: hvigorPluginFilename,
366
+ };
367
+ return template.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => replacements[key] ?? '');
368
+ }
369
+ function renderMetroConfig() {
370
+ return `const fs = require('fs');
371
+ const path = require('path');
372
+
373
+ process.env.EXPO_ROUTER_APP_ROOT = process.env.EXPO_ROUTER_APP_ROOT ?? 'app';
374
+
375
+ const { getDefaultConfig } = require('expo/metro-config');
376
+ const { createHarmonyMetroConfig } = require('@react-native-oh/react-native-harmony/metro.config');
377
+
378
+ const defaultConfig = getDefaultConfig(__dirname);
379
+ const harmonyConfig = createHarmonyMetroConfig({
380
+ reactNativeHarmonyPackageName: '@react-native-oh/react-native-harmony',
381
+ });
382
+ const expoHarmonyShims = {
383
+ 'expo-modules-core': path.resolve(__dirname, '.expo-harmony/shims/expo-modules-core'),
384
+ 'react-native-safe-area-context': path.resolve(
385
+ __dirname,
386
+ '.expo-harmony/shims/react-native-safe-area-context',
387
+ ),
388
+ };
389
+ const reactNativeCompatibilityPackageMarkers = [
390
+ path.sep + '@react-native-oh' + path.sep + 'react-native-harmony' + path.sep,
391
+ path.sep + 'react-native' + path.sep,
392
+ ];
393
+ const resolveReactNativeCompatibilityWrapper = (context, moduleName, platform) => {
394
+ if (platform !== 'harmony' || !context.originModulePath || !moduleName.startsWith('.')) {
395
+ return null;
396
+ }
397
+
398
+ const originModulePath = context.originModulePath;
399
+ const isReactNativeCompatibilityWrapper = reactNativeCompatibilityPackageMarkers.some((marker) =>
400
+ originModulePath.includes(marker),
401
+ );
402
+
403
+ if (!isReactNativeCompatibilityWrapper) {
404
+ return null;
405
+ }
406
+
407
+ const originExtension = path.extname(originModulePath);
408
+ const originBasename = path.basename(originModulePath, originExtension);
409
+ const candidateModulePath = path.resolve(path.dirname(originModulePath), moduleName);
410
+ const candidateModuleExtension = path.extname(candidateModulePath);
411
+ const candidateBasename = path.basename(candidateModulePath, candidateModuleExtension);
412
+
413
+ if (candidateBasename !== originBasename) {
414
+ return null;
415
+ }
416
+
417
+ const candidateBasePath = candidateModuleExtension
418
+ ? candidateModulePath.slice(0, -candidateModuleExtension.length)
419
+ : candidateModulePath;
420
+
421
+ for (const candidatePlatform of ['harmony', 'android', 'ios']) {
422
+ const candidatePath = \`\${candidateBasePath}.\${candidatePlatform}.js\`;
423
+
424
+ if (fs.existsSync(candidatePath)) {
425
+ return context.resolveRequest(context, candidatePath, candidatePlatform);
426
+ }
427
+ }
428
+
429
+ return null;
430
+ };
431
+ const resolveExpoHarmonyShim = (context, moduleName, platform) => {
432
+ if (moduleName in expoHarmonyShims) {
433
+ return context.resolveRequest(context, expoHarmonyShims[moduleName], platform);
434
+ }
435
+
436
+ const compatibilityWrapperResolution = resolveReactNativeCompatibilityWrapper(
437
+ context,
438
+ moduleName,
439
+ platform,
440
+ );
441
+
442
+ if (compatibilityWrapperResolution) {
443
+ return compatibilityWrapperResolution;
444
+ }
445
+
446
+ return context.resolveRequest(context, moduleName, platform);
447
+ };
448
+
449
+ module.exports = {
450
+ ...defaultConfig,
451
+ ...harmonyConfig,
452
+ transformer: {
453
+ ...(defaultConfig.transformer ?? {}),
454
+ ...(harmonyConfig.transformer ?? {}),
455
+ },
456
+ serializer: {
457
+ ...(defaultConfig.serializer ?? {}),
458
+ ...(harmonyConfig.serializer ?? {}),
459
+ },
460
+ resolver: {
461
+ ...(defaultConfig.resolver ?? {}),
462
+ ...(harmonyConfig.resolver ?? {}),
463
+ extraNodeModules: {
464
+ ...((defaultConfig.resolver?.extraNodeModules ?? {})),
465
+ ...((harmonyConfig.resolver?.extraNodeModules ?? {})),
466
+ ...expoHarmonyShims,
467
+ },
468
+ resolveRequest: resolveExpoHarmonyShim,
469
+ sourceExts: [
470
+ 'harmony.ts',
471
+ 'harmony.tsx',
472
+ 'harmony.js',
473
+ 'harmony.jsx',
474
+ ...((harmonyConfig.resolver?.sourceExts ?? defaultConfig.resolver?.sourceExts ?? ['ts', 'tsx', 'js', 'jsx', 'json'])),
475
+ ],
476
+ },
477
+ };
478
+ `;
479
+ }
480
+ function renderExpoModulesCoreHarmonyShim(expoConfig, identifiers) {
481
+ const embeddedExpoConfig = buildExpoConfigForShim(expoConfig, identifiers);
482
+ const serializedExpoConfig = JSON.stringify(embeddedExpoConfig, null, 2);
483
+ const primaryScheme = getPrimarySchemeForShim(embeddedExpoConfig, identifiers);
484
+ const linkingUri = primaryScheme ? `${primaryScheme}://` : null;
485
+ const serializedLinkingUri = JSON.stringify(linkingUri);
486
+ return `'use strict';
487
+
488
+ const { Linking, Platform } = require('react-native');
489
+
490
+ const embeddedExpoConfig = ${serializedExpoConfig};
491
+ const nativeModules = Object.create(null);
492
+
493
+ class EventSubscription {
494
+ constructor(remove) {
495
+ this._remove = remove;
496
+ }
497
+
498
+ remove() {
499
+ if (!this._remove) {
500
+ return;
501
+ }
502
+
503
+ const remove = this._remove;
504
+ this._remove = null;
505
+ remove();
506
+ }
507
+ }
508
+
509
+ class EventEmitter {
510
+ constructor() {
511
+ this._listeners = new Map();
512
+ }
513
+
514
+ addListener(eventName, listener) {
515
+ const listeners = this._listeners.get(eventName) ?? new Set();
516
+ listeners.add(listener);
517
+ this._listeners.set(eventName, listeners);
518
+
519
+ return new EventSubscription(() => {
520
+ listeners.delete(listener);
521
+
522
+ if (listeners.size === 0) {
523
+ this._listeners.delete(eventName);
524
+ }
525
+ });
526
+ }
527
+
528
+ removeAllListeners(eventName) {
529
+ if (typeof eventName === 'string') {
530
+ this._listeners.delete(eventName);
531
+ return;
532
+ }
533
+
534
+ this._listeners.clear();
535
+ }
536
+
537
+ emit(eventName, payload) {
538
+ const listeners = this._listeners.get(eventName);
539
+
540
+ if (!listeners) {
541
+ return;
542
+ }
543
+
544
+ for (const listener of listeners) {
545
+ listener(payload);
546
+ }
547
+ }
548
+ }
549
+
550
+ class LegacyEventEmitter extends EventEmitter {}
551
+
552
+ class NativeModule extends EventEmitter {}
553
+
554
+ class SharedObject {}
555
+
556
+ class SharedRef extends SharedObject {}
557
+
558
+ class CodedError extends Error {
559
+ constructor(code, message) {
560
+ super(message);
561
+ this.code = code;
562
+ this.name = 'CodedError';
563
+ }
564
+ }
565
+
566
+ class UnavailabilityError extends CodedError {
567
+ constructor(moduleName, propertyName) {
568
+ super(
569
+ 'ERR_UNAVAILABLE',
570
+ propertyName
571
+ ? moduleName + '.' + propertyName + ' is not available on Harmony.'
572
+ : moduleName + ' is not available on Harmony.',
573
+ );
574
+ this.name = 'UnavailabilityError';
575
+ }
576
+ }
577
+
578
+ class ExpoLinkingModule extends NativeModule {
579
+ constructor(initialUrl) {
580
+ super();
581
+ this._currentUrl = initialUrl;
582
+ }
583
+
584
+ getLinkingURL() {
585
+ return this._currentUrl;
586
+ }
587
+
588
+ _setCurrentUrl(url) {
589
+ this._currentUrl = url;
590
+ this.emit('onURLReceived', {
591
+ url,
592
+ });
593
+ }
594
+ }
595
+
596
+ const expoLinkingModule = new ExpoLinkingModule(${serializedLinkingUri});
597
+
598
+ if (Linking?.addEventListener) {
599
+ Linking.addEventListener('url', (event) => {
600
+ expoLinkingModule._setCurrentUrl(event?.url ?? null);
601
+ });
602
+ }
603
+
604
+ nativeModules.ExpoLinking = expoLinkingModule;
605
+ nativeModules.ExponentConstants = {
606
+ manifest: embeddedExpoConfig,
607
+ appOwnership: null,
608
+ executionEnvironment: 'standalone',
609
+ experienceUrl: ${serializedLinkingUri},
610
+ linkingUri: ${serializedLinkingUri},
611
+ statusBarHeight: 0,
612
+ systemVersion: 'HarmonyOS',
613
+ platform: {
614
+ android: embeddedExpoConfig.android ?? null,
615
+ ios: embeddedExpoConfig.ios ?? null,
616
+ web: null,
617
+ },
618
+ };
619
+ nativeModules.ExpoAsset = {
620
+ async downloadAsync(url) {
621
+ return url;
622
+ },
623
+ };
624
+ nativeModules.ExpoFetchModule = {
625
+ NativeRequest: class NativeRequest {
626
+ constructor(_response) {
627
+ this._response = _response;
628
+ }
629
+
630
+ async start() {
631
+ throw new UnavailabilityError('ExpoFetchModule', 'NativeRequest.start');
632
+ }
633
+
634
+ cancel() {}
635
+ },
636
+ };
637
+
638
+ function requireOptionalNativeModule(name) {
639
+ return nativeModules[name] ?? null;
640
+ }
641
+
642
+ function requireNativeModule(name) {
643
+ const nativeModule = requireOptionalNativeModule(name);
644
+
645
+ if (nativeModule) {
646
+ return nativeModule;
647
+ }
648
+
649
+ throw new UnavailabilityError(name);
650
+ }
651
+
652
+ function requireNativeViewManager(name) {
653
+ throw new UnavailabilityError(name, 'viewManager');
654
+ }
655
+
656
+ function registerWebModule() {}
657
+
658
+ async function reloadAppAsync() {}
659
+
660
+ function installOnUIRuntime() {}
661
+
662
+ globalThis.expo = {
663
+ ...(globalThis.expo ?? {}),
664
+ EventEmitter,
665
+ LegacyEventEmitter,
666
+ NativeModule,
667
+ SharedObject,
668
+ SharedRef,
669
+ modules: {
670
+ ...(globalThis.expo?.modules ?? {}),
671
+ ...nativeModules,
672
+ },
673
+ };
674
+
675
+ module.exports = {
676
+ Platform,
677
+ CodedError,
678
+ UnavailabilityError,
679
+ EventEmitter,
680
+ LegacyEventEmitter,
681
+ NativeModule,
682
+ SharedObject,
683
+ SharedRef,
684
+ requireNativeModule,
685
+ requireOptionalNativeModule,
686
+ requireNativeViewManager,
687
+ registerWebModule,
688
+ reloadAppAsync,
689
+ installOnUIRuntime,
690
+ };
691
+ `;
692
+ }
693
+ function renderReactNativeSafeAreaContextHarmonyShim() {
694
+ return `'use strict';
695
+
696
+ const React = require('react');
697
+ const { Dimensions, View } = require('react-native');
698
+
699
+ function getWindowMetrics() {
700
+ const metrics = Dimensions.get('window') ?? { width: 0, height: 0 };
701
+
702
+ return {
703
+ frame: {
704
+ x: 0,
705
+ y: 0,
706
+ width: typeof metrics.width === 'number' ? metrics.width : 0,
707
+ height: typeof metrics.height === 'number' ? metrics.height : 0,
708
+ },
709
+ insets: {
710
+ top: 0,
711
+ right: 0,
712
+ bottom: 0,
713
+ left: 0,
714
+ },
715
+ };
716
+ }
717
+
718
+ const initialWindowMetrics = getWindowMetrics();
719
+ const initialWindowSafeAreaInsets = initialWindowMetrics.insets;
720
+ const SafeAreaInsetsContext = React.createContext(initialWindowMetrics.insets);
721
+ const SafeAreaFrameContext = React.createContext(initialWindowMetrics.frame);
722
+
723
+ function SafeAreaProvider({ children, initialMetrics = initialWindowMetrics, style }) {
724
+ const metrics = initialMetrics ?? initialWindowMetrics;
725
+
726
+ return React.createElement(
727
+ SafeAreaFrameContext.Provider,
728
+ { value: metrics.frame },
729
+ React.createElement(
730
+ SafeAreaInsetsContext.Provider,
731
+ { value: metrics.insets },
732
+ React.createElement(View, { style: [{ flex: 1 }, style] }, children),
733
+ ),
734
+ );
735
+ }
736
+
737
+ function NativeSafeAreaProvider(props) {
738
+ return React.createElement(SafeAreaProvider, props);
739
+ }
740
+
741
+ function SafeAreaView({ children, style, ...rest }) {
742
+ return React.createElement(View, { ...rest, style }, children);
743
+ }
744
+
745
+ function SafeAreaListener({ children }) {
746
+ return typeof children === 'function' ? children(initialWindowMetrics) : null;
747
+ }
748
+
749
+ function useSafeAreaInsets() {
750
+ return React.useContext(SafeAreaInsetsContext);
751
+ }
752
+
753
+ function useSafeAreaFrame() {
754
+ return React.useContext(SafeAreaFrameContext);
755
+ }
756
+
757
+ function useSafeArea() {
758
+ return useSafeAreaInsets();
759
+ }
760
+
761
+ function withSafeAreaInsets(Component) {
762
+ return React.forwardRef((props, ref) =>
763
+ React.createElement(Component, {
764
+ ...props,
765
+ ref,
766
+ insets: useSafeAreaInsets(),
767
+ }),
768
+ );
769
+ }
770
+
771
+ module.exports = {
772
+ EdgeInsets: undefined,
773
+ initialWindowMetrics,
774
+ initialWindowSafeAreaInsets,
775
+ NativeSafeAreaProvider,
776
+ SafeAreaConsumer: SafeAreaInsetsContext.Consumer,
777
+ SafeAreaFrameContext,
778
+ SafeAreaInsetsContext,
779
+ SafeAreaListener,
780
+ SafeAreaProvider,
781
+ SafeAreaView,
782
+ useSafeArea,
783
+ useSafeAreaFrame,
784
+ useSafeAreaInsets,
785
+ withSafeAreaInsets,
786
+ };
787
+ `;
788
+ }
789
+ function renderHarmonyRuntimePrelude() {
790
+ return `'use strict';
791
+
792
+ require('react-native/Libraries/Core/InitializeCore');
793
+
794
+ function requireReactNativeBaseViewConfigHarmony() {
795
+ try {
796
+ return require('react-native/Libraries/NativeComponent/BaseViewConfig.harmony');
797
+ } catch (_error) {
798
+ return null;
799
+ }
800
+ }
801
+
802
+ function requireRnohBaseViewConfigHarmony() {
803
+ try {
804
+ return require('@react-native-oh/react-native-harmony/Libraries/NativeComponent/BaseViewConfig.harmony');
805
+ } catch (_error) {
806
+ return null;
807
+ }
808
+ }
809
+
810
+ function requireReactNativeBaseViewConfig() {
811
+ try {
812
+ return require('react-native/Libraries/NativeComponent/BaseViewConfig');
813
+ } catch (_error) {
814
+ return null;
815
+ }
816
+ }
817
+
818
+ function requireReactNativePlatformBaseViewConfig() {
819
+ try {
820
+ return require('react-native/Libraries/NativeComponent/PlatformBaseViewConfig');
821
+ } catch (_error) {
822
+ return null;
823
+ }
824
+ }
825
+
826
+ function requireRnohBaseViewConfig() {
827
+ try {
828
+ return require('@react-native-oh/react-native-harmony/Libraries/NativeComponent/BaseViewConfig');
829
+ } catch (_error) {
830
+ return null;
831
+ }
832
+ }
833
+
834
+ function requireRnohPlatformBaseViewConfig() {
835
+ try {
836
+ return require('@react-native-oh/react-native-harmony/Libraries/NativeComponent/PlatformBaseViewConfig');
837
+ } catch (_error) {
838
+ return null;
839
+ }
840
+ }
841
+
842
+ function patchNativeComponentViewConfigDefaults() {
843
+ const harmonyBaseViewConfigModule =
844
+ requireReactNativeBaseViewConfigHarmony() ?? requireRnohBaseViewConfigHarmony();
845
+ const harmonyBaseViewConfig = harmonyBaseViewConfigModule?.default ?? harmonyBaseViewConfigModule;
846
+
847
+ if (!harmonyBaseViewConfig) {
848
+ return;
849
+ }
850
+
851
+ for (const moduleExports of [
852
+ requireReactNativeBaseViewConfig(),
853
+ requireReactNativePlatformBaseViewConfig(),
854
+ requireRnohBaseViewConfig(),
855
+ requireRnohPlatformBaseViewConfig(),
856
+ ]) {
857
+ if (moduleExports && typeof moduleExports === 'object') {
858
+ moduleExports.default = harmonyBaseViewConfig;
859
+ }
860
+ }
861
+ }
862
+
863
+ function installGlobalIfMissing(name, factory) {
864
+ if (typeof globalThis[name] !== 'undefined') {
865
+ return;
866
+ }
867
+
868
+ const value = factory();
869
+
870
+ if (typeof value !== 'undefined') {
871
+ globalThis[name] = value;
872
+ }
873
+ }
874
+
875
+ patchNativeComponentViewConfigDefaults();
876
+ installGlobalIfMissing('FormData', () => require('react-native/Libraries/Network/FormData').default);
877
+ installGlobalIfMissing('Blob', () => require('react-native/Libraries/Blob/Blob').default);
878
+ installGlobalIfMissing('FileReader', () => require('react-native/Libraries/Blob/FileReader').default);
879
+ `;
880
+ }
881
+ function isBinaryTemplate(relativePath) {
882
+ return ['.png'].includes(path_1.default.extname(relativePath));
883
+ }
884
+ function contentsEqual(currentContents, nextContents, binary = false) {
885
+ if (binary || Buffer.isBuffer(nextContents)) {
886
+ return currentContents.equals(Buffer.isBuffer(nextContents) ? nextContents : Buffer.from(nextContents));
887
+ }
888
+ return currentContents.toString('utf8') === nextContents;
889
+ }
890
+ function sortRecordByKey(record) {
891
+ return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
892
+ }
893
+ function isEquivalentToolkitScript(scriptName, currentCommand, desiredCommand) {
894
+ if (currentCommand === desiredCommand) {
895
+ return true;
896
+ }
897
+ const compatibilityPatterns = {
898
+ 'harmony:doctor': /\bexpo-harmony(?:\.js)?\s+doctor\b/,
899
+ 'harmony:init': /\bexpo-harmony(?:\.js)?\s+init\b/,
900
+ 'harmony:sync-template': /\bexpo-harmony(?:\.js)?\s+sync-template\b/,
901
+ 'harmony:env': /\bexpo-harmony(?:\.js)?\s+env\b/,
902
+ 'harmony:bundle': /\bexpo-harmony(?:\.js)?\s+bundle\b/,
903
+ 'harmony:build:debug': /\bexpo-harmony(?:\.js)?\s+build-hap\b[\s\S]*--mode\s+debug\b/,
904
+ 'harmony:build:release': /\bexpo-harmony(?:\.js)?\s+build-hap\b[\s\S]*--mode\s+release\b/,
905
+ };
906
+ const compatibilityPattern = compatibilityPatterns[scriptName];
907
+ return compatibilityPattern ? compatibilityPattern.test(currentCommand) : false;
908
+ }
909
+ function buildDesiredPackageScripts(packageJson) {
910
+ return {
911
+ ...constants_1.DESIRED_PACKAGE_SCRIPTS,
912
+ };
913
+ }
914
+ function usesExpoRouter(packageJson) {
915
+ return (0, project_1.hasDeclaredDependency)(packageJson, 'expo-router');
916
+ }
917
+ function resolveHarmonyBundleEntryFile(packageJson) {
918
+ return usesExpoRouter(packageJson) ? constants_1.HARMONY_ROUTER_ENTRY_FILENAME : 'index.js';
919
+ }
920
+ function renderRouterHarmonyEntry(identifiers) {
921
+ return `require('./${constants_1.HARMONY_RUNTIME_PRELUDE_RELATIVE_PATH}');
922
+
923
+ const React = require('react');
924
+ const { AppRegistry } = require('react-native');
925
+ const { registerRootComponent } = require('expo');
926
+ const { ExpoRoot } = require('expo-router');
927
+
928
+ const context = require.context('./app', true, /\\.[jt]sx?$/);
929
+
930
+ function App() {
931
+ return React.createElement(ExpoRoot, {
932
+ context,
933
+ });
934
+ }
935
+
936
+ registerRootComponent(App);
937
+ AppRegistry.registerComponent(${JSON.stringify(identifiers.slug)}, () => App);
938
+ `;
939
+ }
940
+ function buildExpoConfigForShim(expoConfig, identifiers) {
941
+ const normalized = toSerializableValue(expoConfig);
942
+ const config = normalized && typeof normalized === 'object' && !Array.isArray(normalized)
943
+ ? { ...normalized }
944
+ : {};
945
+ config.name = config.name ?? identifiers.appName;
946
+ config.slug = config.slug ?? identifiers.slug;
947
+ config.version = config.version ?? '1.0.0';
948
+ if (!config.scheme) {
949
+ config.scheme = getPrimarySchemeForShim(config, identifiers);
950
+ }
951
+ const android = config.android && typeof config.android === 'object' && !Array.isArray(config.android)
952
+ ? { ...config.android }
953
+ : {};
954
+ const ios = config.ios && typeof config.ios === 'object' && !Array.isArray(config.ios)
955
+ ? { ...config.ios }
956
+ : {};
957
+ android.package = android.package ?? identifiers.androidPackage ?? identifiers.bundleName;
958
+ ios.bundleIdentifier =
959
+ ios.bundleIdentifier ?? identifiers.iosBundleIdentifier ?? identifiers.bundleName;
960
+ config.android = android;
961
+ config.ios = ios;
962
+ return config;
963
+ }
964
+ function getPrimarySchemeForShim(expoConfig, identifiers) {
965
+ const scheme = expoConfig.scheme;
966
+ if (typeof scheme === 'string' && scheme.trim().length > 0) {
967
+ return scheme.trim();
968
+ }
969
+ if (Array.isArray(scheme)) {
970
+ const firstScheme = scheme.find((value) => typeof value === 'string' && value.trim().length > 0);
971
+ if (firstScheme) {
972
+ return firstScheme.trim();
973
+ }
974
+ }
975
+ return identifiers.androidPackage ?? identifiers.iosBundleIdentifier ?? identifiers.bundleName;
976
+ }
977
+ function toSerializableValue(value) {
978
+ if (value === null ||
979
+ typeof value === 'string' ||
980
+ typeof value === 'number' ||
981
+ typeof value === 'boolean') {
982
+ return value;
983
+ }
984
+ if (Array.isArray(value)) {
985
+ return value
986
+ .map((entry) => toSerializableValue(entry))
987
+ .filter((entry) => entry !== undefined);
988
+ }
989
+ if (typeof value === 'object') {
990
+ const result = {};
991
+ for (const [key, entry] of Object.entries(value)) {
992
+ const serializedEntry = toSerializableValue(entry);
993
+ if (serializedEntry !== undefined) {
994
+ result[key] = serializedEntry;
995
+ }
996
+ }
997
+ return result;
998
+ }
999
+ return undefined;
1000
+ }
1001
+ function collectMetadataWarnings(previousManifest, previousToolkitConfig) {
1002
+ const warnings = [];
1003
+ if (previousManifest && previousManifest.templateVersion !== constants_1.TEMPLATE_VERSION) {
1004
+ warnings.push(`Existing manifest template version ${previousManifest.templateVersion} does not match current template ${constants_1.TEMPLATE_VERSION}. Sync will refresh managed metadata.`);
1005
+ }
1006
+ if (previousManifest && previousManifest.matrixId !== validatedMatrices_1.DEFAULT_VALIDATED_MATRIX_ID) {
1007
+ warnings.push(`Existing manifest matrix ${previousManifest.matrixId ?? 'unknown'} does not match current matrix ${validatedMatrices_1.DEFAULT_VALIDATED_MATRIX_ID}. Sync will refresh managed metadata.`);
1008
+ }
1009
+ if (previousToolkitConfig && previousToolkitConfig.templateVersion !== constants_1.TEMPLATE_VERSION) {
1010
+ warnings.push(`Existing toolkit-config template version ${previousToolkitConfig.templateVersion} does not match current template ${constants_1.TEMPLATE_VERSION}. Sync will refresh managed metadata.`);
1011
+ }
1012
+ if (previousToolkitConfig && previousToolkitConfig.matrixId !== validatedMatrices_1.DEFAULT_VALIDATED_MATRIX_ID) {
1013
+ warnings.push(`Existing toolkit-config matrix ${previousToolkitConfig.matrixId ?? 'unknown'} does not match current matrix ${validatedMatrices_1.DEFAULT_VALIDATED_MATRIX_ID}. Sync will refresh managed metadata.`);
1014
+ }
1015
+ return warnings;
1016
+ }
1017
+ function stabilizeToolkitConfigTimestamp(previousToolkitConfig, nextToolkitConfig) {
1018
+ if (!previousToolkitConfig) {
1019
+ return nextToolkitConfig;
1020
+ }
1021
+ const { generatedAt: previousGeneratedAt, ...previousComparable } = previousToolkitConfig;
1022
+ const { generatedAt: nextGeneratedAt, ...nextComparable } = nextToolkitConfig;
1023
+ if (JSON.stringify(previousComparable) === JSON.stringify(nextComparable)) {
1024
+ return {
1025
+ ...nextToolkitConfig,
1026
+ generatedAt: previousGeneratedAt,
1027
+ };
1028
+ }
1029
+ return nextToolkitConfig;
1030
+ }