expo-harmony-toolkit 1.7.2 → 1.8.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 (69) hide show
  1. package/README.en.md +25 -11
  2. package/README.md +27 -13
  3. package/build/core/autolinking.d.ts +10 -0
  4. package/build/core/autolinking.js +421 -0
  5. package/build/core/build/commands.d.ts +19 -0
  6. package/build/core/build/commands.js +61 -0
  7. package/build/core/build/compatibilityShims.d.ts +1 -0
  8. package/build/core/build/compatibilityShims.js +272 -0
  9. package/build/core/build/localHar.d.ts +13 -0
  10. package/build/core/build/localHar.js +337 -0
  11. package/build/core/build/reporting.d.ts +19 -0
  12. package/build/core/build/reporting.js +67 -0
  13. package/build/core/build/rnohCompatibility.d.ts +1 -0
  14. package/build/core/build/rnohCompatibility.js +38 -0
  15. package/build/core/build.d.ts +4 -12
  16. package/build/core/build.js +70 -762
  17. package/build/core/constants.d.ts +2 -1
  18. package/build/core/constants.js +3 -2
  19. package/build/core/dependencyInspection.d.ts +2 -0
  20. package/build/core/dependencyInspection.js +130 -0
  21. package/build/core/env.js +1 -1
  22. package/build/core/report.js +162 -10
  23. package/build/core/signing.d.ts +2 -0
  24. package/build/core/signing.js +30 -0
  25. package/build/core/template/capabilityRegistry.d.ts +9 -0
  26. package/build/core/template/capabilityRegistry.js +84 -0
  27. package/build/core/template/expoModulesCoreShim.d.ts +2 -0
  28. package/build/core/template/expoModulesCoreShim.js +279 -0
  29. package/build/core/template/metro.d.ts +2 -0
  30. package/build/core/template/metro.js +213 -0
  31. package/build/core/template/nativeFiles.d.ts +8 -0
  32. package/build/core/template/nativeFiles.js +333 -0
  33. package/build/core/template/renderers/camera.d.ts +4 -0
  34. package/build/core/template/renderers/camera.js +612 -0
  35. package/build/core/template/renderers/fileSystem.d.ts +4 -0
  36. package/build/core/template/renderers/fileSystem.js +814 -0
  37. package/build/core/template/renderers/imagePicker.d.ts +4 -0
  38. package/build/core/template/renderers/imagePicker.js +753 -0
  39. package/build/core/template/renderers/location.d.ts +4 -0
  40. package/build/core/template/renderers/location.js +836 -0
  41. package/build/core/template/runtimeShims.d.ts +2 -0
  42. package/build/core/template/runtimeShims.js +192 -0
  43. package/build/core/template/support.d.ts +11 -0
  44. package/build/core/template/support.js +108 -0
  45. package/build/core/template.d.ts +3 -6
  46. package/build/core/template.js +49 -4457
  47. package/build/data/capabilities.js +21 -4
  48. package/build/data/dependencyCatalog.js +10 -0
  49. package/build/data/publicDocs.d.ts +11 -0
  50. package/build/data/publicDocs.js +28 -0
  51. package/build/data/validatedMatrices.js +9 -1
  52. package/build/docs/render.d.ts +9 -0
  53. package/build/docs/render.js +250 -0
  54. package/build/types.d.ts +19 -0
  55. package/docs/cli-build.md +1 -1
  56. package/docs/npm-release.md +1 -0
  57. package/docs/official-app-shell-sample.md +2 -1
  58. package/docs/official-minimal-sample.md +2 -1
  59. package/docs/official-native-capabilities-sample.md +12 -2
  60. package/docs/official-ui-stack-sample.md +2 -1
  61. package/docs/roadmap.md +146 -51
  62. package/docs/signing-and-release.md +3 -0
  63. package/docs/support-matrix.md +59 -13
  64. package/package.json +13 -5
  65. package/docs/v1.5.1-acceptance.md +0 -385
  66. package/docs/v1.6.0-acceptance.md +0 -193
  67. package/docs/v1.7.0-acceptance.md +0 -111
  68. package/docs/v1.7.1-acceptance.md +0 -111
  69. package/docs/v1.7.2-acceptance.md +0 -37
@@ -1,6 +1,6 @@
1
1
  export declare const TOOLKIT_PACKAGE_NAME = "expo-harmony-toolkit";
2
2
  export declare const CLI_NAME = "expo-harmony";
3
- export declare const TOOLKIT_VERSION = "1.7.2";
3
+ export declare const TOOLKIT_VERSION = "1.8.0";
4
4
  export declare const TEMPLATE_VERSION = "rnoh-0.82.18";
5
5
  export declare const RNOH_VERSION = "0.82.18";
6
6
  export declare const RNOH_CLI_VERSION = "0.82.18";
@@ -8,6 +8,7 @@ export declare const SUPPORTED_EXPO_SDKS: number[];
8
8
  export declare const GENERATED_DIR = ".expo-harmony";
9
9
  export declare const GENERATED_SHIMS_DIR = ".expo-harmony/shims";
10
10
  export declare const SIGNING_LOCAL_FILENAME = "signing.local.json";
11
+ export declare const SIGNING_LOCAL_EXAMPLE_FILENAME = "signing.local.example.json";
11
12
  export declare const MANIFEST_FILENAME = "manifest.json";
12
13
  export declare const DOCTOR_REPORT_FILENAME = "doctor-report.json";
13
14
  export declare const ENV_REPORT_FILENAME = "env-report.json";
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DESIRED_PACKAGE_SCRIPTS = exports.HARMONY_RUNTIME_PRELUDE_RELATIVE_PATH = exports.HARMONY_ROUTER_ENTRY_FILENAME = exports.STRICT_ENV_EXIT_CODE = exports.STRICT_DOCTOR_EXIT_CODE = exports.DEFAULT_HVIGOR_PLUGIN_FILENAME = exports.PREBUILD_METADATA_FILENAME = exports.TOOLKIT_CONFIG_FILENAME = exports.BUILD_REPORT_FILENAME = exports.ENV_REPORT_FILENAME = exports.DOCTOR_REPORT_FILENAME = exports.MANIFEST_FILENAME = exports.SIGNING_LOCAL_FILENAME = exports.GENERATED_SHIMS_DIR = exports.GENERATED_DIR = exports.SUPPORTED_EXPO_SDKS = exports.RNOH_CLI_VERSION = exports.RNOH_VERSION = exports.TEMPLATE_VERSION = exports.TOOLKIT_VERSION = exports.CLI_NAME = exports.TOOLKIT_PACKAGE_NAME = void 0;
3
+ exports.DESIRED_PACKAGE_SCRIPTS = exports.HARMONY_RUNTIME_PRELUDE_RELATIVE_PATH = exports.HARMONY_ROUTER_ENTRY_FILENAME = exports.STRICT_ENV_EXIT_CODE = exports.STRICT_DOCTOR_EXIT_CODE = exports.DEFAULT_HVIGOR_PLUGIN_FILENAME = exports.PREBUILD_METADATA_FILENAME = exports.TOOLKIT_CONFIG_FILENAME = exports.BUILD_REPORT_FILENAME = exports.ENV_REPORT_FILENAME = exports.DOCTOR_REPORT_FILENAME = exports.MANIFEST_FILENAME = exports.SIGNING_LOCAL_EXAMPLE_FILENAME = exports.SIGNING_LOCAL_FILENAME = exports.GENERATED_SHIMS_DIR = exports.GENERATED_DIR = exports.SUPPORTED_EXPO_SDKS = exports.RNOH_CLI_VERSION = exports.RNOH_VERSION = exports.TEMPLATE_VERSION = exports.TOOLKIT_VERSION = exports.CLI_NAME = exports.TOOLKIT_PACKAGE_NAME = void 0;
4
4
  exports.TOOLKIT_PACKAGE_NAME = 'expo-harmony-toolkit';
5
5
  exports.CLI_NAME = 'expo-harmony';
6
- exports.TOOLKIT_VERSION = '1.7.2';
6
+ exports.TOOLKIT_VERSION = '1.8.0';
7
7
  exports.TEMPLATE_VERSION = 'rnoh-0.82.18';
8
8
  exports.RNOH_VERSION = '0.82.18';
9
9
  exports.RNOH_CLI_VERSION = '0.82.18';
@@ -11,6 +11,7 @@ exports.SUPPORTED_EXPO_SDKS = [53, 55];
11
11
  exports.GENERATED_DIR = '.expo-harmony';
12
12
  exports.GENERATED_SHIMS_DIR = `${exports.GENERATED_DIR}/shims`;
13
13
  exports.SIGNING_LOCAL_FILENAME = 'signing.local.json';
14
+ exports.SIGNING_LOCAL_EXAMPLE_FILENAME = 'signing.local.example.json';
14
15
  exports.MANIFEST_FILENAME = 'manifest.json';
15
16
  exports.DOCTOR_REPORT_FILENAME = 'doctor-report.json';
16
17
  exports.ENV_REPORT_FILENAME = 'env-report.json';
@@ -0,0 +1,2 @@
1
+ import { DetectedDependency } from '../types';
2
+ export declare function annotateDependencyBuildability(projectRoot: string, dependencies: DetectedDependency[]): Promise<DetectedDependency[]>;
@@ -0,0 +1,130 @@
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.annotateDependencyBuildability = annotateDependencyBuildability;
7
+ const node_module_1 = require("node:module");
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const NATIVE_DIRECTORY_CANDIDATES = ['android', 'ios', 'harmony'];
11
+ const NATIVE_CONFIG_CANDIDATES = [
12
+ 'oh-package.json5',
13
+ 'react-native.config.js',
14
+ 'react-native.config.cjs',
15
+ 'react-native.config.mjs',
16
+ 'react-native.config.ts',
17
+ 'expo-module.config.json',
18
+ 'expo-module.config.js',
19
+ 'expo-module.config.cjs',
20
+ 'expo-module.config.mjs',
21
+ 'expo-module.config.ts',
22
+ ];
23
+ const PACKAGE_JSON_NATIVE_HINT_KEYS = [
24
+ 'codegenConfig',
25
+ 'expo-module',
26
+ 'harmony',
27
+ 'nativePackageName',
28
+ 'podspecPath',
29
+ 'react-native',
30
+ 'react-native-builder-bob',
31
+ ];
32
+ const PACKAGE_NAME_NATIVE_PATTERNS = [
33
+ /^expo(?:-|$)/,
34
+ /^@expo\//,
35
+ /^react-native(?:-|$)/,
36
+ /^@react-native\//,
37
+ /^@react-native-community\//,
38
+ ];
39
+ async function annotateDependencyBuildability(projectRoot, dependencies) {
40
+ return Promise.all(dependencies.map(async (dependency) => ({
41
+ ...dependency,
42
+ buildabilityRisk: await resolveDependencyBuildabilityRisk(projectRoot, dependency),
43
+ })));
44
+ }
45
+ async function resolveDependencyBuildabilityRisk(projectRoot, dependency) {
46
+ if (dependency.status !== 'unknown') {
47
+ return 'known';
48
+ }
49
+ const packageJsonPath = resolveInstalledDependencyPackageJson(projectRoot, dependency.name, dependency.version);
50
+ if (!packageJsonPath || !(await fs_extra_1.default.pathExists(packageJsonPath))) {
51
+ return 'unresolved';
52
+ }
53
+ const packageRoot = path_1.default.dirname(packageJsonPath);
54
+ const dependencyPackageJson = (await fs_extra_1.default.readJson(packageJsonPath));
55
+ if (looksLikeNativeDependencyByPackageName(dependency.name)) {
56
+ return 'native-risk';
57
+ }
58
+ if (await hasNativeSurface(packageRoot, dependencyPackageJson)) {
59
+ return 'native-risk';
60
+ }
61
+ return 'js-only-unknown';
62
+ }
63
+ function resolveInstalledDependencyPackageJson(projectRoot, dependencyName, dependencyVersion) {
64
+ const localPackageJsonPath = resolveLocalDependencyPackageJson(projectRoot, dependencyVersion);
65
+ if (localPackageJsonPath) {
66
+ return localPackageJsonPath;
67
+ }
68
+ try {
69
+ const projectRequire = (0, node_module_1.createRequire)(path_1.default.join(projectRoot, 'package.json'));
70
+ return projectRequire.resolve(`${dependencyName}/package.json`);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ function resolveLocalDependencyPackageJson(projectRoot, dependencyVersion) {
77
+ const trimmedVersion = dependencyVersion.trim();
78
+ for (const prefix of ['file:', 'link:', 'portal:']) {
79
+ if (!trimmedVersion.startsWith(prefix)) {
80
+ continue;
81
+ }
82
+ const relativeTarget = trimmedVersion.slice(prefix.length).trim();
83
+ if (!relativeTarget) {
84
+ return null;
85
+ }
86
+ return path_1.default.resolve(projectRoot, relativeTarget, 'package.json');
87
+ }
88
+ return null;
89
+ }
90
+ function looksLikeNativeDependencyByPackageName(dependencyName) {
91
+ return PACKAGE_NAME_NATIVE_PATTERNS.some((pattern) => pattern.test(dependencyName));
92
+ }
93
+ async function hasNativeSurface(packageRoot, dependencyPackageJson) {
94
+ for (const directoryName of NATIVE_DIRECTORY_CANDIDATES) {
95
+ if (await fs_extra_1.default.pathExists(path_1.default.join(packageRoot, directoryName))) {
96
+ return true;
97
+ }
98
+ }
99
+ for (const fileName of NATIVE_CONFIG_CANDIDATES) {
100
+ if (await fs_extra_1.default.pathExists(path_1.default.join(packageRoot, fileName))) {
101
+ return true;
102
+ }
103
+ }
104
+ if (await hasPodspecFile(packageRoot)) {
105
+ return true;
106
+ }
107
+ if (PACKAGE_JSON_NATIVE_HINT_KEYS.some((key) => dependencyPackageJson[key] !== undefined)) {
108
+ return true;
109
+ }
110
+ const files = Array.isArray(dependencyPackageJson.files)
111
+ ? dependencyPackageJson.files.filter((entry) => typeof entry === 'string')
112
+ : [];
113
+ if (files.some((entry) => hasNativePathHint(entry))) {
114
+ return true;
115
+ }
116
+ const keywords = Array.isArray(dependencyPackageJson.keywords)
117
+ ? dependencyPackageJson.keywords.filter((entry) => typeof entry === 'string')
118
+ : [];
119
+ if (keywords.some((keyword) => keyword === 'react-native' || keyword === 'expo-module')) {
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+ async function hasPodspecFile(packageRoot) {
125
+ const entries = await fs_extra_1.default.readdir(packageRoot);
126
+ return entries.some((entry) => entry.endsWith('.podspec'));
127
+ }
128
+ function hasNativePathHint(value) {
129
+ return /(^|\/)(android|ios|harmony)(\/|$)/.test(value) || value.endsWith('.podspec');
130
+ }
package/build/core/env.js CHANGED
@@ -95,7 +95,7 @@ async function buildEnvReport(projectRoot, options = {}) {
95
95
  else if (!signingConfigured) {
96
96
  advisories.push({
97
97
  code: 'env.signing.missing',
98
- message: 'Neither .expo-harmony/signing.local.json nor harmony/build-profile.json5 declares usable signingConfigs yet. Debug GUI flows may still work, but release builds require signing.',
98
+ message: 'Neither .expo-harmony/signing.local.json nor harmony/build-profile.json5 declares usable signingConfigs yet. Copy .expo-harmony/signing.local.example.json to signing.local.json and fill your real release credentials before running release builds.',
99
99
  });
100
100
  }
101
101
  return {
@@ -10,6 +10,7 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const semver_1 = __importDefault(require("semver"));
12
12
  const constants_1 = require("./constants");
13
+ const dependencyInspection_1 = require("./dependencyInspection");
13
14
  const dependencyCatalog_1 = require("../data/dependencyCatalog");
14
15
  const validatedMatrices_1 = require("../data/validatedMatrices");
15
16
  const capabilities_1 = require("../data/capabilities");
@@ -22,6 +23,8 @@ const DEFAULT_RECORD = {
22
23
  supportTier: 'unsupported',
23
24
  note: 'This dependency is not in the current compatibility catalog yet.',
24
25
  };
26
+ const BARE_WORKFLOW_DIRECTORY_NAMES = ['android', 'ios'];
27
+ const BARE_WORKFLOW_DEPENDENCIES = new Set(['expo-build-properties', 'expo-dev-client']);
25
28
  async function buildDoctorReport(projectRoot, options = {}) {
26
29
  const loadedProject = await (0, project_1.loadProject)(projectRoot);
27
30
  const expoSdkVersion = (0, project_1.detectExpoSdkVersion)(loadedProject.packageJson);
@@ -39,7 +42,7 @@ async function buildDoctorReport(projectRoot, options = {}) {
39
42
  dependencyRecords.set(pluginName, createDependencyRecord(pluginName, 'configured', 'expo-plugin'));
40
43
  }
41
44
  }
42
- const dependencies = [...dependencyRecords.values()].sort((left, right) => left.name.localeCompare(right.name));
45
+ const dependencies = await (0, dependencyInspection_1.annotateDependencyBuildability)(loadedProject.projectRoot, [...dependencyRecords.values()].sort((left, right) => left.name.localeCompare(right.name)));
43
46
  const blockingIssues = await collectBlockingIssues(loadedProject.projectRoot, loadedProject.expoConfig, loadedProject.packageJson, expoPlugins, expoSchemes, expoSdkVersion, dependencies, matrix, targetTier);
44
47
  const capabilities = (0, capabilities_1.getCapabilityDefinitionsForProject)(loadedProject.packageJson).map((definition) => ({
45
48
  id: definition.id,
@@ -48,6 +51,7 @@ async function buildDoctorReport(projectRoot, options = {}) {
48
51
  supportTier: definition.supportTier,
49
52
  runtimeMode: definition.runtimeMode,
50
53
  evidence: { ...definition.evidence },
54
+ evidenceSource: { ...definition.evidenceSource },
51
55
  note: definition.note,
52
56
  docsUrl: definition.docsUrl,
53
57
  nativePackageNames: [...definition.nativePackageNames],
@@ -55,13 +59,22 @@ async function buildDoctorReport(projectRoot, options = {}) {
55
59
  sampleRoute: definition.sampleRoute,
56
60
  acceptanceChecklist: [...definition.acceptanceChecklist],
57
61
  }));
62
+ const coverageProfile = await detectCoverageProfile(loadedProject.projectRoot, loadedProject.packageJson, dependencies);
58
63
  const blockingDependencyNames = new Set(blockingIssues
59
64
  .filter((issue) => issue.code.startsWith('dependency.') && issue.subject)
60
65
  .map((issue) => issue.subject));
61
66
  const resolvedDependencies = dependencies.map((dependency) => ({
62
67
  ...dependency,
68
+ gapCategory: resolveDependencyGapCategory(dependency, coverageProfile),
63
69
  blocking: blockingDependencyNames.has(dependency.name),
64
70
  }));
71
+ const nextActions = buildNextActions({
72
+ targetTier,
73
+ coverageProfile,
74
+ blockingIssues,
75
+ dependencies: resolvedDependencies,
76
+ capabilities,
77
+ });
65
78
  const warnings = buildWarnings(loadedProject.expoConfig, expoSdkVersion, resolvedDependencies);
66
79
  const advisories = buildAdvisories(loadedProject.expoConfig);
67
80
  return {
@@ -76,6 +89,7 @@ async function buildDoctorReport(projectRoot, options = {}) {
76
89
  rnohCliVersion: constants_1.RNOH_CLI_VERSION,
77
90
  expoSdkVersion,
78
91
  targetTier,
92
+ coverageProfile,
79
93
  expoConfig: {
80
94
  name: loadedProject.expoConfig.name ?? null,
81
95
  slug: loadedProject.expoConfig.slug ?? null,
@@ -100,6 +114,7 @@ async function buildDoctorReport(projectRoot, options = {}) {
100
114
  },
101
115
  capabilities,
102
116
  blockingIssues,
117
+ nextActions,
103
118
  advisories,
104
119
  warnings,
105
120
  };
@@ -118,6 +133,7 @@ function renderDoctorReport(report) {
118
133
  `Expo SDK: ${report.expoSdkVersion ?? 'unknown'} (recognized ${constants_1.SUPPORTED_EXPO_SDKS.join(', ')})`,
119
134
  `Matrix: ${report.matrixId ?? 'none'}`,
120
135
  `Target tier: ${report.targetTier}`,
136
+ `Coverage profile: ${report.coverageProfile}`,
121
137
  `Eligibility: ${report.eligibility}`,
122
138
  `Schemes: ${report.expoConfig.schemes.join(', ') || 'none'}`,
123
139
  `Plugins: ${report.expoConfig.plugins.join(', ') || 'none'}`,
@@ -129,7 +145,7 @@ function renderDoctorReport(report) {
129
145
  ...report.dependencies.map((dependency) => {
130
146
  const replacement = dependency.replacement ? ` | replacement: ${dependency.replacement}` : '';
131
147
  const blocking = dependency.blocking ? ' | blocking: yes' : '';
132
- return `- [${dependency.status}/${dependency.supportTier}] ${dependency.name}@${dependency.version} (${dependency.source}) - ${dependency.note}${replacement}${blocking}`;
148
+ return `- [${dependency.status}/${dependency.supportTier}] ${dependency.name}@${dependency.version} (${dependency.source}) - ${dependency.note} | buildability: ${renderDependencyBuildabilityRisk(dependency.buildabilityRisk)} | gap: ${dependency.gapCategory}${replacement}${blocking}`;
133
149
  }),
134
150
  ];
135
151
  if (report.capabilities.length > 0) {
@@ -138,7 +154,7 @@ function renderDoctorReport(report) {
138
154
  ? ` | permissions: ${capability.harmonyPermissions.join(', ')}`
139
155
  : '';
140
156
  const missingEvidence = getMissingCapabilityEvidence(capability.evidence);
141
- const evidence = ` | evidence: ${renderCapabilityEvidence(capability.evidence)}`;
157
+ const evidence = ` | evidence: ${renderCapabilityEvidence(capability.evidence, capability.evidenceSource)}`;
142
158
  const promotionGaps = buildCapabilityPromotionGaps(capability.runtimeMode, missingEvidence);
143
159
  const gapSuffix = promotionGaps.length > 0
144
160
  ? ` | verified gaps: ${promotionGaps.join(', ')}`
@@ -149,6 +165,9 @@ function renderDoctorReport(report) {
149
165
  if (report.blockingIssues.length > 0) {
150
166
  sections.push('', 'Blocking issues:', ...report.blockingIssues.map((issue) => `- ${issue.code}: ${issue.message}${issue.subject ? ` (${issue.subject})` : ''}`));
151
167
  }
168
+ if (report.nextActions.length > 0) {
169
+ sections.push('', 'Next actions:', ...report.nextActions.map((action, index) => `${index + 1}. ${action}`));
170
+ }
152
171
  if (report.advisories.length > 0) {
153
172
  sections.push('', 'Advisories:', ...report.advisories.map((advisory) => `- ${advisory}`));
154
173
  }
@@ -157,12 +176,12 @@ function renderDoctorReport(report) {
157
176
  }
158
177
  return sections.join('\n');
159
178
  }
160
- function renderCapabilityEvidence(evidence) {
179
+ function renderCapabilityEvidence(evidence, evidenceSource) {
161
180
  return [
162
- `bundle=${evidence.bundle ? 'yes' : 'no'}`,
163
- `debugBuild=${evidence.debugBuild ? 'yes' : 'no'}`,
164
- `device=${evidence.device ? 'yes' : 'no'}`,
165
- `release=${evidence.release ? 'yes' : 'no'}`,
181
+ `bundle=${evidence.bundle ? 'yes' : 'no'}[${evidenceSource.bundle}]`,
182
+ `debugBuild=${evidence.debugBuild ? 'yes' : 'no'}[${evidenceSource.debugBuild}]`,
183
+ `device=${evidence.device ? 'yes' : 'no'}[${evidenceSource.device}]`,
184
+ `release=${evidence.release ? 'yes' : 'no'}[${evidenceSource.release}]`,
166
185
  ].join(', ');
167
186
  }
168
187
  function getMissingCapabilityEvidence(evidence) {
@@ -185,6 +204,8 @@ function createDependencyRecord(name, version, source) {
185
204
  source,
186
205
  status: matrixRecord.status,
187
206
  supportTier: matrixRecord.supportTier,
207
+ buildabilityRisk: 'known',
208
+ gapCategory: 'matrix-drift',
188
209
  blocking: false,
189
210
  note: matrixRecord.note,
190
211
  replacement: matrixRecord.replacement,
@@ -332,6 +353,114 @@ async function collectBlockingIssues(projectRoot, expoConfig, packageJson, expoP
332
353
  }
333
354
  return dedupeIssues(issues);
334
355
  }
356
+ async function detectCoverageProfile(projectRoot, packageJson, dependencies) {
357
+ for (const directoryName of BARE_WORKFLOW_DIRECTORY_NAMES) {
358
+ if (await fs_extra_1.default.pathExists(path_1.default.join(projectRoot, directoryName))) {
359
+ return 'bare';
360
+ }
361
+ }
362
+ if (dependencies.some((dependency) => isThirdPartyNativeGapDependency(dependency))) {
363
+ return 'third-party-native-heavy';
364
+ }
365
+ if ((0, capabilities_1.getCapabilityDefinitionsForProject)(packageJson).length > 0 ||
366
+ dependencies.some((dependency) => isOfficialExpoDependencyName(dependency.name) && dependency.supportTier !== 'verified')) {
367
+ return 'managed-native-heavy';
368
+ }
369
+ return 'managed-core';
370
+ }
371
+ function resolveDependencyGapCategory(dependency, coverageProfile) {
372
+ if (coverageProfile === 'bare' &&
373
+ (BARE_WORKFLOW_DEPENDENCIES.has(dependency.name) ||
374
+ dependency.name === 'expo' ||
375
+ dependency.name === 'react-native')) {
376
+ return 'bare-workflow-gap';
377
+ }
378
+ if (isOfficialExpoDependencyName(dependency.name) || capabilities_1.CAPABILITY_BY_PACKAGE[dependency.name]) {
379
+ return dependency.supportTier === 'verified' && dependency.status === 'supported'
380
+ ? 'matrix-drift'
381
+ : 'official-module-gap';
382
+ }
383
+ if (isThirdPartyNativeGapDependency(dependency)) {
384
+ return 'third-party-native-gap';
385
+ }
386
+ if (coverageProfile === 'bare') {
387
+ return 'bare-workflow-gap';
388
+ }
389
+ return 'matrix-drift';
390
+ }
391
+ function isOfficialExpoDependencyName(dependencyName) {
392
+ return dependencyName === 'expo' || dependencyName.startsWith('expo-') || dependencyName.startsWith('@expo/');
393
+ }
394
+ function isThirdPartyNativeGapDependency(dependency) {
395
+ if (dependency.name === 'react-native-gesture-handler' ||
396
+ dependency.name === '@react-native-oh-tpl/react-native-gesture-handler') {
397
+ return true;
398
+ }
399
+ if (dependency.buildabilityRisk === 'native-risk') {
400
+ return true;
401
+ }
402
+ return (dependency.supportTier !== 'verified' &&
403
+ !isOfficialExpoDependencyName(dependency.name) &&
404
+ dependency.name.startsWith('react-native'));
405
+ }
406
+ function buildNextActions(input) {
407
+ const actions = [];
408
+ const { targetTier, coverageProfile, blockingIssues, dependencies, capabilities } = input;
409
+ const hasPreviewCapabilities = capabilities.some((capability) => capability.supportTier === 'preview');
410
+ const hasRouterBlockingIssues = blockingIssues.some((issue) => [
411
+ 'dependency.router_peer_missing',
412
+ 'config.router_plugin.missing',
413
+ 'config.scheme.missing',
414
+ 'config.bundle_script.mismatch',
415
+ ].includes(issue.code));
416
+ if (hasBlockingIssueCode(blockingIssues, 'matrix.expo_sdk.unsupported') ||
417
+ hasBlockingIssueCode(blockingIssues, 'dependency.version_mismatch') ||
418
+ hasBlockingIssueCode(blockingIssues, 'dependency.specifier_mismatch') ||
419
+ hasBlockingIssueCode(blockingIssues, 'dependency.required_missing')) {
420
+ actions.push(`Align Expo SDK, React Native, RNOH, and validated adapter versions to ${validatedMatrices_1.DEFAULT_VALIDATED_MATRIX_ID}, then rerun \`expo-harmony doctor --project-root . --strict\`.`);
421
+ }
422
+ if (targetTier === 'verified' && hasPreviewCapabilities) {
423
+ actions.push('Use `expo-harmony doctor --project-root . --target-tier preview` to measure the current preview-capability baseline while keeping `latest` pinned to verified-only releases.');
424
+ }
425
+ if (hasPreviewCapabilities) {
426
+ actions.push('Keep combined sample smoke for regression coverage, but track bundle/debug/device/release evidence separately for each preview capability before promotion.');
427
+ }
428
+ switch (coverageProfile) {
429
+ case 'managed-core':
430
+ actions.push('Stay on the verified lane: rerun `expo-harmony sync-template --project-root .`, `expo-harmony bundle --project-root .`, and `expo-harmony build-hap --project-root . --mode debug` before claiming release readiness.');
431
+ break;
432
+ case 'managed-native-heavy':
433
+ actions.push('After every native-capability change, rerun `expo-harmony sync-template --project-root .`, `expo-harmony bundle --project-root .`, and `expo-harmony build-hap --project-root . --mode debug` to keep the managed sidecar and preview evidence aligned.');
434
+ break;
435
+ case 'bare':
436
+ actions.push('Keep this project on the bare workflow track for now: preserve the native directories, use `expo-harmony doctor --project-root .` for classification, and only claim verified support after bare workflow support lands in the mainline capability catalog.');
437
+ break;
438
+ case 'third-party-native-heavy':
439
+ actions.push('Isolate third-party native packages and onboard them through the mainline capability catalog one by one; start with `react-native-gesture-handler` if it is present, and treat unknown native surfaces as explicit unblockers rather than matrix drift.');
440
+ break;
441
+ }
442
+ if (hasRouterBlockingIssues) {
443
+ actions.push('Add the missing expo-router peers/plugin/scheme or update the Harmony bundle script to use `expo-harmony bundle`, then rerun doctor before trusting router builds.');
444
+ }
445
+ if (blockingIssues.some((issue) => issue.code === 'dependency.not_allowed' &&
446
+ (issue.subject === 'react-native-gesture-handler' ||
447
+ issue.subject === '@react-native-oh-tpl/react-native-gesture-handler'))) {
448
+ actions.push('Keep `react-native-gesture-handler` out of the verified lane until its Harmony adapter path has stable doctor, sample, and build coverage.');
449
+ }
450
+ if (hasBlockingIssueCode(blockingIssues, 'config.native_identifier.missing')) {
451
+ actions.push('Set `android.package` or `ios.bundleIdentifier` in Expo config before expecting a strict Harmony build path.');
452
+ }
453
+ if (dependencies.some((dependency) => dependency.buildabilityRisk === 'native-risk')) {
454
+ actions.push('Inspect unknown native-looking dependencies and either replace them, gate them behind preview work, or onboard them explicitly before promising Harmony portability.');
455
+ }
456
+ if (dependencies.some((dependency) => dependency.buildabilityRisk === 'js-only-unknown')) {
457
+ actions.push('Unknown JavaScript-only packages still sit outside the public matrix; verify bundling manually, but prioritize native gaps first.');
458
+ }
459
+ return dedupeStrings(actions);
460
+ }
461
+ function hasBlockingIssueCode(issues, code) {
462
+ return issues.some((issue) => issue.code === code);
463
+ }
335
464
  function buildWarnings(expoConfig, expoSdkVersion, dependencies) {
336
465
  const warnings = [];
337
466
  const expoSdkWarning = (0, project_1.getExpoSdkWarning)(expoSdkVersion);
@@ -350,8 +479,14 @@ function buildWarnings(expoConfig, expoSdkVersion, dependencies) {
350
479
  if (dependencies.some((dependency) => dependency.supportTier === 'experimental')) {
351
480
  warnings.push('Experimental-tier dependencies were detected. Expect bridge drift, runtime gaps, or additional manual validation before claiming release readiness.');
352
481
  }
353
- if (dependencies.some((dependency) => dependency.status === 'unknown')) {
354
- warnings.push('Unknown dependencies were detected. The toolkit can scaffold the project, but runtime portability is not guaranteed.');
482
+ if (dependencies.some((dependency) => dependency.buildabilityRisk === 'js-only-unknown')) {
483
+ warnings.push('Some unknown dependencies look JavaScript-only. They remain outside the public matrix, but they are less likely to block bundling or a debug HAP build outright.');
484
+ }
485
+ if (dependencies.some((dependency) => dependency.buildabilityRisk === 'native-risk')) {
486
+ warnings.push('Some unknown dependencies appear to carry native surfaces. Treat them as real Harmony portability risks until they are explicitly onboarded.');
487
+ }
488
+ if (dependencies.some((dependency) => dependency.buildabilityRisk === 'unresolved')) {
489
+ warnings.push('Some unknown dependencies could not be inspected because their installed package metadata was unavailable. Install dependencies before treating the doctor report as a buildability signal.');
355
490
  }
356
491
  return warnings;
357
492
  }
@@ -384,6 +519,20 @@ function matchesVersionRange(rawVersion, range) {
384
519
  function matchesDependencySpecifier(rawSpecifier, expectedSpecifier) {
385
520
  return rawSpecifier.trim() === expectedSpecifier.trim();
386
521
  }
522
+ function renderDependencyBuildabilityRisk(buildabilityRisk) {
523
+ switch (buildabilityRisk) {
524
+ case 'known':
525
+ return 'known';
526
+ case 'js-only-unknown':
527
+ return 'unknown-js';
528
+ case 'native-risk':
529
+ return 'unknown-native';
530
+ case 'unresolved':
531
+ return 'unresolved';
532
+ default:
533
+ return buildabilityRisk;
534
+ }
535
+ }
387
536
  function dedupeIssues(issues) {
388
537
  const seen = new Set();
389
538
  return issues.filter((issue) => {
@@ -395,3 +544,6 @@ function dedupeIssues(issues) {
395
544
  return true;
396
545
  });
397
546
  }
547
+ function dedupeStrings(values) {
548
+ return [...new Set(values)];
549
+ }
@@ -4,8 +4,10 @@ type SigningLocalAppFragment = {
4
4
  products?: JsonRecord[];
5
5
  };
6
6
  export declare function getSigningLocalPath(projectRoot: string): string;
7
+ export declare function getSigningLocalExamplePath(projectRoot: string): string;
7
8
  export declare function readSigningLocalConfig(projectRoot: string): Promise<SigningLocalAppFragment | null>;
8
9
  export declare function mergeSigningLocalConfigIntoBuildProfile(rawBuildProfileContents: string, signingLocalConfig: SigningLocalAppFragment | null): string;
9
10
  export declare function hasSigningConfiguration(rawBuildProfileContents: string | null, signingLocalConfig?: SigningLocalAppFragment | null): boolean;
10
11
  export declare function hasSigningLocalConfiguration(signingLocalConfig: SigningLocalAppFragment | null): boolean;
12
+ export declare function renderSigningLocalExampleConfig(): string;
11
13
  export {};
@@ -4,10 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getSigningLocalPath = getSigningLocalPath;
7
+ exports.getSigningLocalExamplePath = getSigningLocalExamplePath;
7
8
  exports.readSigningLocalConfig = readSigningLocalConfig;
8
9
  exports.mergeSigningLocalConfigIntoBuildProfile = mergeSigningLocalConfigIntoBuildProfile;
9
10
  exports.hasSigningConfiguration = hasSigningConfiguration;
10
11
  exports.hasSigningLocalConfiguration = hasSigningLocalConfiguration;
12
+ exports.renderSigningLocalExampleConfig = renderSigningLocalExampleConfig;
11
13
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
14
  const json5_1 = __importDefault(require("json5"));
13
15
  const path_1 = __importDefault(require("path"));
@@ -15,6 +17,9 @@ const constants_1 = require("./constants");
15
17
  function getSigningLocalPath(projectRoot) {
16
18
  return path_1.default.join(projectRoot, constants_1.GENERATED_DIR, constants_1.SIGNING_LOCAL_FILENAME);
17
19
  }
20
+ function getSigningLocalExamplePath(projectRoot) {
21
+ return path_1.default.join(projectRoot, constants_1.GENERATED_DIR, constants_1.SIGNING_LOCAL_EXAMPLE_FILENAME);
22
+ }
18
23
  async function readSigningLocalConfig(projectRoot) {
19
24
  const signingLocalPath = getSigningLocalPath(projectRoot);
20
25
  if (!(await fs_extra_1.default.pathExists(signingLocalPath))) {
@@ -49,6 +54,31 @@ function hasSigningConfiguration(rawBuildProfileContents, signingLocalConfig = n
49
54
  function hasSigningLocalConfiguration(signingLocalConfig) {
50
55
  return hasNonEmptySigningConfigList(signingLocalConfig?.signingConfigs);
51
56
  }
57
+ function renderSigningLocalExampleConfig() {
58
+ return (JSON.stringify({
59
+ signingConfigs: [
60
+ {
61
+ name: 'default',
62
+ type: 'HarmonyOS',
63
+ material: {
64
+ storeFile: './signing/release.p12',
65
+ storePassword: '<replace-with-store-password>',
66
+ keyAlias: '<replace-with-key-alias>',
67
+ keyPassword: '<replace-with-key-password>',
68
+ signAlg: 'SHA256withECDSA',
69
+ profile: './signing/release.p7b',
70
+ certpath: './signing/release.cer',
71
+ },
72
+ },
73
+ ],
74
+ products: [
75
+ {
76
+ name: 'default',
77
+ signingConfig: 'default',
78
+ },
79
+ ],
80
+ }, null, 2) + '\n');
81
+ }
52
82
  function normalizeSigningLocalConfig(rawConfig) {
53
83
  const candidate = rawConfig && typeof rawConfig === 'object' && !Array.isArray(rawConfig)
54
84
  ? 'app' in rawConfig &&
@@ -0,0 +1,9 @@
1
+ import { CapabilityDefinition } from '../../types';
2
+ type ManagedExpoHarmonyModuleRenderer = {
3
+ filename: string;
4
+ render: () => string;
5
+ };
6
+ export declare const MANAGED_EXPO_HARMONY_MODULE_RENDERERS: readonly ManagedExpoHarmonyModuleRenderer[];
7
+ export declare function renderCapabilityModuleShim(capability: CapabilityDefinition): string;
8
+ export declare function renderUnsupportedCapabilityShim(capability: CapabilityDefinition): string;
9
+ export {};
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MANAGED_EXPO_HARMONY_MODULE_RENDERERS = void 0;
4
+ exports.renderCapabilityModuleShim = renderCapabilityModuleShim;
5
+ exports.renderUnsupportedCapabilityShim = renderUnsupportedCapabilityShim;
6
+ const camera_1 = require("./renderers/camera");
7
+ const fileSystem_1 = require("./renderers/fileSystem");
8
+ const imagePicker_1 = require("./renderers/imagePicker");
9
+ const location_1 = require("./renderers/location");
10
+ const CAPABILITY_SHIM_RENDERERS = {
11
+ 'expo-file-system': {
12
+ adapter: fileSystem_1.renderExpoFileSystemHarmonyAdapterShim,
13
+ preview: fileSystem_1.renderExpoFileSystemPreviewShim,
14
+ },
15
+ 'expo-image-picker': {
16
+ adapter: imagePicker_1.renderExpoImagePickerHarmonyAdapterShim,
17
+ preview: imagePicker_1.renderExpoImagePickerPreviewShim,
18
+ },
19
+ 'expo-location': {
20
+ adapter: location_1.renderExpoLocationHarmonyAdapterShim,
21
+ preview: location_1.renderExpoLocationPreviewShim,
22
+ },
23
+ 'expo-camera': {
24
+ adapter: camera_1.renderExpoCameraHarmonyAdapterShim,
25
+ preview: camera_1.renderExpoCameraPreviewShim,
26
+ },
27
+ };
28
+ exports.MANAGED_EXPO_HARMONY_MODULE_RENDERERS = [
29
+ {
30
+ filename: 'ExpoHarmonyFileSystemTurboModule.ts',
31
+ render: fileSystem_1.renderExpoHarmonyFileSystemTurboModule,
32
+ },
33
+ {
34
+ filename: 'ExpoHarmonyImagePickerTurboModule.ts',
35
+ render: imagePicker_1.renderExpoHarmonyImagePickerTurboModule,
36
+ },
37
+ {
38
+ filename: 'ExpoHarmonyLocationTurboModule.ts',
39
+ render: location_1.renderExpoHarmonyLocationTurboModule,
40
+ },
41
+ {
42
+ filename: 'ExpoHarmonyCameraTurboModule.ts',
43
+ render: camera_1.renderExpoHarmonyCameraTurboModule,
44
+ },
45
+ ];
46
+ function renderCapabilityModuleShim(capability) {
47
+ const renderer = CAPABILITY_SHIM_RENDERERS[capability.packageName];
48
+ if (!renderer) {
49
+ return renderUnsupportedCapabilityShim(capability);
50
+ }
51
+ return capability.runtimeMode !== 'shim'
52
+ ? renderer.adapter(capability)
53
+ : renderer.preview(capability);
54
+ }
55
+ function renderUnsupportedCapabilityShim(capability) {
56
+ return `'use strict';
57
+
58
+ const { CodedError } = require('expo-modules-core');
59
+
60
+ function createPreviewError() {
61
+ return new CodedError(
62
+ 'ERR_EXPO_HARMONY_PREVIEW',
63
+ '${capability.packageName} is classified as ${capability.supportTier} for Harmony, but no managed runtime shim has been wired yet.',
64
+ );
65
+ }
66
+
67
+ function unavailable() {
68
+ throw createPreviewError();
69
+ }
70
+
71
+ module.exports = new Proxy(
72
+ {},
73
+ {
74
+ get(_target, propertyName) {
75
+ if (propertyName === '__esModule') {
76
+ return true;
77
+ }
78
+
79
+ return unavailable;
80
+ },
81
+ },
82
+ );
83
+ `;
84
+ }
@@ -0,0 +1,2 @@
1
+ import { HarmonyIdentifiers } from '../../types';
2
+ export declare function renderExpoModulesCoreHarmonyShim(expoConfig: Record<string, any>, identifiers: HarmonyIdentifiers): string;