expo-harmony-toolkit 1.7.3 → 1.8.1

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.
package/README.en.md CHANGED
@@ -9,7 +9,7 @@
9
9
  <p>
10
10
  <a href="https://github.com/BlackishGreen33/Expo-Harmony-Toolkit/actions/workflows/ci.yml"><img alt="Checks" src="https://img.shields.io/badge/checks-passing-16a34a?style=flat-square&logo=githubactions&logoColor=white"></a>
11
11
  <a href="./LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-0f766e?style=flat-square"></a>
12
- <a href="https://github.com/BlackishGreen33/Expo-Harmony-Toolkit/releases"><img alt="Version" src="https://img.shields.io/badge/version-v1.7.3-111827?style=flat-square"></a>
12
+ <a href="https://github.com/BlackishGreen33/Expo-Harmony-Toolkit/releases"><img alt="Version" src="https://img.shields.io/badge/version-v1.8.1-111827?style=flat-square"></a>
13
13
  <a href="./docs/support-matrix.md"><img alt="Matrix" src="https://img.shields.io/badge/matrix-expo55--rnoh082--ui--stack-2563eb?style=flat-square"></a>
14
14
  <img alt="Input" src="https://img.shields.io/badge/input-Managed%2FCNG-059669?style=flat-square">
15
15
  </p>
@@ -24,7 +24,7 @@
24
24
  </div>
25
25
 
26
26
  > [!IMPORTANT]
27
- > `v1.7` keeps the `verified + preview + experimental` model and moves `expo-location` and `expo-camera` into `preview`. Starting with this documentation refresh, the public promise is tighter: `latest` only carries fully accepted `verified` capabilities, while `next` is reserved for preview fast-track work. The roadmap still targets `Managed/CNG Core Expo Coverage` first and long-tail extension coverage second.
27
+ > `v1.8` keeps the `verified + preview + experimental` model and continues to keep `expo-location` and `expo-camera` in `preview`. The public promise remains tighter: `latest` only carries fully accepted `verified` capabilities, while `next` is reserved for preview fast-track work. The roadmap now defines `v2.0.0` itself as “reliable Harmony packaging for any Expo project”, so the intermediate versions will keep getting split smaller, but that still does not widen the current `verified` boundary yet.
28
28
 
29
29
  > [!TIP]
30
30
  > The two validated `@react-native-oh-tpl/*` adapters in the public matrix are currently consumed via exact Git URLs and commits. For repository development and the official UI-stack sample, prefer `pnpm install --ignore-scripts` so adapter prepare hooks do not fail on private upstream resources.
@@ -48,7 +48,7 @@
48
48
  <!-- GENERATED:readme-current-status:start -->
49
49
  | Item | Status |
50
50
  | --- | --- |
51
- | Current version | `v1.7.3` |
51
+ | Current version | `v1.8.1` |
52
52
  | Support model | `verified + preview + experimental` |
53
53
  | Public `verified` matrix | `expo55-rnoh082-ui-stack` |
54
54
  | Supported input | Managed/CNG Expo projects |
@@ -56,7 +56,7 @@
56
56
  | `preview` native capabilities | `expo-file-system`, `expo-image-picker`, `expo-location`, `expo-camera` |
57
57
  | `experimental` capabilities | `expo-notifications`, `react-native-gesture-handler` |
58
58
  | Release tracks | `latest` = fully accepted `verified` only; `next` = preview fast track |
59
- | Capability telemetry | `runtimeMode` + `evidence(...)` + `evidenceSource(...)` |
59
+ | Capability telemetry | `runtimeMode` + `evidence(...)` + `evidenceSource(...)` + `coverageProfile` + `nextActions` |
60
60
  | Build path | `doctor -> init -> bundle -> build-hap` |
61
61
  | Primary sample | `examples/official-ui-stack-sample` |
62
62
  | Preview sample | `examples/official-native-capabilities-sample` |
@@ -182,20 +182,23 @@ Common decision points:
182
182
  - `doctor-report.json` exposes `capabilities[].runtimeMode`
183
183
  - `doctor-report.json` and `toolkit-config.json` expose `evidence.bundle`, `evidence.debugBuild`, `evidence.device`, and `evidence.release`
184
184
  - `doctor-report.json` and `toolkit-config.json` expose `evidenceSource.bundle`, `evidenceSource.debugBuild`, `evidenceSource.device`, and `evidenceSource.release`
185
+ - `doctor-report.json` and `toolkit-config.json` also expose `coverageProfile` plus ordered `nextActions`
185
186
  - `runtimeMode=shim` means the capability still has not reached a verified runtime path even if bundling and debug-build scaffolding already exist
186
187
  - `evidenceSource.device=manual-doc` means the current device signal comes from manual acceptance records, not automated verification
187
188
  <!-- GENERATED:readme-support-matrix:end -->
188
189
 
189
- Starting in this refresh, `doctor` also emits `buildabilityRisk` so matrix-out dependencies that still look JavaScript-only no longer get described the same way as packages with clear native risk. This does not relax any gate; it only improves diagnosis.
190
+ Starting in this refresh, `doctor` also emits `buildabilityRisk`, `coverageProfile`, `gapCategory`, and ordered `nextActions` so matrix drift, official-module gaps, third-party native blockers, and bare-workflow tracks no longer get described the same way. This does not relax any gate; it only improves diagnosis.
190
191
 
191
192
  See [docs/support-matrix.md](./docs/support-matrix.md) for the full allowlist, pairing rules, exact specifiers, issue codes, and release gates.
192
193
 
194
+ If you want the `v1.8.x` promotion board directly, use [acceptance/v1.8.x-capability-board.md](./acceptance/v1.8.x-capability-board.md).
195
+
193
196
  ## Official Samples
194
197
 
195
198
  - `examples/official-ui-stack-sample`
196
199
  The current public main sample, covering router, linking, constants, SVG, reanimated, and the Harmony sidecar build flow.
197
200
  - `examples/official-native-capabilities-sample`
198
- The `v1.7.x` Batch A+B preview walkthrough sample, covering the supported core subsets for `expo-file-system`, `expo-image-picker`, `expo-location`, and `expo-camera` plus permission, bundle, and debug-build validation.
201
+ The `v1.8.x` preview walkthrough sample, covering the supported core subsets for `expo-file-system`, `expo-image-picker`, `expo-location`, and `expo-camera` plus permission, bundle, debug-build validation, and per-capability acceptance tracking.
199
202
  - `examples/official-app-shell-sample`
200
203
  The minimal App Shell onboarding sample that demonstrates router, linking, constants, pathname, observed URL, and a generated deep-link flow.
201
204
  - `examples/official-minimal-sample`
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  <p>
10
10
  <a href="https://github.com/BlackishGreen33/Expo-Harmony-Toolkit/actions/workflows/ci.yml"><img alt="Checks" src="https://img.shields.io/badge/checks-passing-16a34a?style=flat-square&logo=githubactions&logoColor=white"></a>
11
11
  <a href="./LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-0f766e?style=flat-square"></a>
12
- <a href="https://github.com/BlackishGreen33/Expo-Harmony-Toolkit/releases"><img alt="Version" src="https://img.shields.io/badge/version-v1.7.3-111827?style=flat-square"></a>
12
+ <a href="https://github.com/BlackishGreen33/Expo-Harmony-Toolkit/releases"><img alt="Version" src="https://img.shields.io/badge/version-v1.8.1-111827?style=flat-square"></a>
13
13
  <a href="./docs/support-matrix.md"><img alt="Matrix" src="https://img.shields.io/badge/matrix-expo55--rnoh082--ui--stack-2563eb?style=flat-square"></a>
14
14
  <img alt="Input" src="https://img.shields.io/badge/input-Managed%2FCNG-059669?style=flat-square">
15
15
  </p>
@@ -24,7 +24,7 @@
24
24
  </div>
25
25
 
26
26
  > [!IMPORTANT]
27
- > `v1.7` 延续 `verified + preview + experimental` 三层支持模型,并把 `expo-location`、`expo-camera` 推进到 `preview`。从这版文档开始,对外承诺进一步收紧为:`latest` 只承诺完整验收的 `verified` 能力,`next` 用于 preview fast track;路线仍然是先做到 `Managed/CNG Core Expo Coverage`,再通过 extension model 逼近“任意 Expo 项目”。
27
+ > `v1.8` 延续 `verified + preview + experimental` 三层支持模型,并把 `expo-location`、`expo-camera` 继续保持在 `preview`。当前公开承诺仍然收紧为:`latest` 只承诺完整验收的 `verified` 能力,`next` 用于 preview fast track。最新 roadmap 已把 `v2.0.0` 直接定义成“任何 Expo 项目都能可靠打包成鸿蒙 App”,因此中间版本会继续拆小,但这不等于当前 `verified` 边界已经放宽。
28
28
 
29
29
  > [!TIP]
30
30
  > 由于当前公开矩阵内的两套 `@react-native-oh-tpl/*` adapter 依赖以 Git URL + exact commit 形式接入,仓库开发和官方 UI-stack sample 推荐使用 `pnpm install --ignore-scripts`,避免 Git adapter 在 prepare 阶段拉取私有资源而中断安装。
@@ -48,7 +48,7 @@
48
48
  <!-- GENERATED:readme-current-status:start -->
49
49
  | 项目 | 说明 |
50
50
  | --- | --- |
51
- | 当前版本 | `v1.7.3` |
51
+ | 当前版本 | `v1.8.1` |
52
52
  | 支持模型 | `verified + preview + experimental` |
53
53
  | 唯一 `verified` 公开矩阵 | `expo55-rnoh082-ui-stack` |
54
54
  | 输入范围 | Managed/CNG Expo 项目 |
@@ -56,7 +56,7 @@
56
56
  | `preview` 原生能力 | `expo-file-system`、`expo-image-picker`、`expo-location`、`expo-camera` |
57
57
  | `experimental` 能力 | `expo-notifications`、`react-native-gesture-handler` |
58
58
  | 发布轨 | `latest` = fully accepted verified only;`next` = preview fast track |
59
- | capability 遥测 | `runtimeMode` + `evidence(...)` + `evidenceSource(...)` |
59
+ | capability 遥测 | `runtimeMode` + `evidence(...)` + `evidenceSource(...)` + `coverageProfile` + `nextActions` |
60
60
  | 构建链 | `doctor -> init -> bundle -> build-hap` |
61
61
  | 主 sample | `examples/official-ui-stack-sample` |
62
62
  | preview sample | `examples/official-native-capabilities-sample` |
@@ -182,11 +182,12 @@ pnpm exec expo-harmony build-hap --mode release
182
182
  - `doctor-report.json` 的 `capabilities[]` 会带出 `runtimeMode`
183
183
  - `doctor-report.json` 与 `toolkit-config.json` 会带出 `evidence.bundle`、`evidence.debugBuild`、`evidence.device`、`evidence.release`
184
184
  - `doctor-report.json` 与 `toolkit-config.json` 会带出 `evidenceSource.bundle`、`evidenceSource.debugBuild`、`evidenceSource.device`、`evidenceSource.release`
185
+ - `doctor-report.json` 与 `toolkit-config.json` 也会带出 `coverageProfile` 与按顺序排列的 `nextActions`
185
186
  - `runtimeMode=shim` 说明当前仍未进入 verified runtime path,即使 bundle / debug build 已经可走通
186
187
  - `evidenceSource.device=manual-doc` 表示当前只有人工设备验收记录,不代表机器自动验证
187
188
  <!-- GENERATED:readme-support-matrix:end -->
188
189
 
189
- 从本版开始,`doctor` 还会额外输出 `buildabilityRisk`,把“超出公开矩阵但看起来更像纯 JS 层”的依赖,与“存在明确原生风险”的依赖区分开来;这不会放宽 gate,只是让构建可诊断性更强。
190
+ 从本版开始,`doctor` 还会额外输出 `buildabilityRisk`、`coverageProfile`、`gapCategory` 与有序 `nextActions`,把“矩阵漂移”“官方模块缺口”“第三方 native blocker”“bare workflow 轨道”分开描述;这不会放宽 gate,只是让构建可诊断性更强。
190
191
 
191
192
  文档里的状态标记额外约定为:
192
193
 
@@ -196,12 +197,14 @@ pnpm exec expo-harmony build-hap --mode release
196
197
 
197
198
  完整白名单、配对规则、exact specifier、issue code 与 release gate 见 [docs/support-matrix.md](./docs/support-matrix.md)。
198
199
 
200
+ 如果你要跟进 `v1.8.x` 的并行晋升节奏,可直接看 [acceptance/v1.8.x-capability-board.md](./acceptance/v1.8.x-capability-board.md)。
201
+
199
202
  ## 官方 Samples
200
203
 
201
204
  - `examples/official-ui-stack-sample`
202
205
  当前唯一对外主 sample,同时覆盖 router、linking、constants、SVG、reanimated 和 Harmony sidecar 构建链。
203
206
  - `examples/official-native-capabilities-sample`
204
- `v1.7.x` 的 Batch A+B preview walkthrough sample,用来承接 `expo-file-system`、`expo-image-picker`、`expo-location`、`expo-camera` 的核心支持子集、permission、bundledebug build 验收。
207
+ `v1.8.x` 的 preview walkthrough sample,用来承接 `expo-file-system`、`expo-image-picker`、`expo-location`、`expo-camera` 的核心支持子集、permission、bundledebug build 与逐 capability 验收追踪。
205
208
  - `examples/official-app-shell-sample`
206
209
  最小可理解的 App Shell onboarding sample,用来展示 router、linking、constants、pathname、observed URL 与 generated deep link。
207
210
  - `examples/official-minimal-sample`
@@ -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.3";
3
+ export declare const TOOLKIT_VERSION = "1.8.1";
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";
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
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.3';
6
+ exports.TOOLKIT_VERSION = '1.8.1';
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';
@@ -46,7 +46,7 @@ async function resolveDependencyBuildabilityRisk(projectRoot, dependency) {
46
46
  if (dependency.status !== 'unknown') {
47
47
  return 'known';
48
48
  }
49
- const packageJsonPath = resolveInstalledDependencyPackageJson(projectRoot, dependency.name);
49
+ const packageJsonPath = resolveInstalledDependencyPackageJson(projectRoot, dependency.name, dependency.version);
50
50
  if (!packageJsonPath || !(await fs_extra_1.default.pathExists(packageJsonPath))) {
51
51
  return 'unresolved';
52
52
  }
@@ -60,7 +60,11 @@ async function resolveDependencyBuildabilityRisk(projectRoot, dependency) {
60
60
  }
61
61
  return 'js-only-unknown';
62
62
  }
63
- function resolveInstalledDependencyPackageJson(projectRoot, dependencyName) {
63
+ function resolveInstalledDependencyPackageJson(projectRoot, dependencyName, dependencyVersion) {
64
+ const localPackageJsonPath = resolveLocalDependencyPackageJson(projectRoot, dependencyVersion);
65
+ if (localPackageJsonPath) {
66
+ return localPackageJsonPath;
67
+ }
64
68
  try {
65
69
  const projectRequire = (0, node_module_1.createRequire)(path_1.default.join(projectRoot, 'package.json'));
66
70
  return projectRequire.resolve(`${dependencyName}/package.json`);
@@ -69,6 +73,20 @@ function resolveInstalledDependencyPackageJson(projectRoot, dependencyName) {
69
73
  return null;
70
74
  }
71
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
+ }
72
90
  function looksLikeNativeDependencyByPackageName(dependencyName) {
73
91
  return PACKAGE_NAME_NATIVE_PATTERNS.some((pattern) => pattern.test(dependencyName));
74
92
  }
@@ -1,4 +1,4 @@
1
- import { DependencySource, ExpoHarmonyPluginProps, HarmonyIdentifiers, LoadedProject, PackageJson } from '../types';
1
+ import { DependencySource, ExpoHarmonyResolvedDoctorConfig, ExpoHarmonyPluginProps, HarmonyIdentifiers, LoadedProject, PackageJson } from '../types';
2
2
  export declare function resolveProjectRoot(projectRoot?: string): Promise<string>;
3
3
  export declare function loadProject(projectRoot: string): Promise<LoadedProject>;
4
4
  export declare function ensureProjectPackageJsonPath(projectRoot: string): Promise<string>;
@@ -10,6 +10,8 @@ export declare function collectDeclaredDependencies(packageJson: PackageJson): A
10
10
  }>;
11
11
  export declare function hasDeclaredDependency(packageJson: PackageJson, dependencyName: string): boolean;
12
12
  export declare function collectExpoPlugins(expoConfig: Record<string, any>): string[];
13
+ export declare function resolveExpoHarmonyDoctorConfig(expoConfig: Record<string, any>): ExpoHarmonyResolvedDoctorConfig;
14
+ export declare function applyDependencyExclusions(packageJson: PackageJson, excludedDependencies: readonly string[]): PackageJson;
13
15
  export declare function collectExpoSchemes(expoConfig: Record<string, any>): string[];
14
16
  export declare function deriveHarmonyIdentifiers(expoConfig: Record<string, any>, packageJson?: PackageJson, props?: ExpoHarmonyPluginProps): HarmonyIdentifiers;
15
17
  export declare function detectExpoSdkVersion(packageJson: PackageJson): number | null;
@@ -10,6 +10,8 @@ exports.findAppConfigPath = findAppConfigPath;
10
10
  exports.collectDeclaredDependencies = collectDeclaredDependencies;
11
11
  exports.hasDeclaredDependency = hasDeclaredDependency;
12
12
  exports.collectExpoPlugins = collectExpoPlugins;
13
+ exports.resolveExpoHarmonyDoctorConfig = resolveExpoHarmonyDoctorConfig;
14
+ exports.applyDependencyExclusions = applyDependencyExclusions;
13
15
  exports.collectExpoSchemes = collectExpoSchemes;
14
16
  exports.deriveHarmonyIdentifiers = deriveHarmonyIdentifiers;
15
17
  exports.detectExpoSdkVersion = detectExpoSdkVersion;
@@ -100,6 +102,27 @@ function collectExpoPlugins(expoConfig) {
100
102
  }
101
103
  return [...names].sort((left, right) => left.localeCompare(right));
102
104
  }
105
+ function resolveExpoHarmonyDoctorConfig(expoConfig) {
106
+ const rawDoctorConfig = expoConfig.extra?.expoHarmony?.doctor;
107
+ const coverageProfile = normalizeCoverageProfile(rawDoctorConfig?.coverageProfile);
108
+ return {
109
+ excludeDependencies: normalizeStringArray(rawDoctorConfig?.excludeDependencies),
110
+ excludePlugins: normalizeStringArray(rawDoctorConfig?.excludePlugins),
111
+ coverageProfile,
112
+ };
113
+ }
114
+ function applyDependencyExclusions(packageJson, excludedDependencies) {
115
+ if (excludedDependencies.length === 0) {
116
+ return packageJson;
117
+ }
118
+ const excludedDependencySet = new Set(excludedDependencies);
119
+ return {
120
+ ...packageJson,
121
+ dependencies: filterDependencySection(packageJson.dependencies, excludedDependencySet),
122
+ devDependencies: filterDependencySection(packageJson.devDependencies, excludedDependencySet),
123
+ peerDependencies: filterDependencySection(packageJson.peerDependencies, excludedDependencySet),
124
+ };
125
+ }
103
126
  function collectExpoSchemes(expoConfig) {
104
127
  const rawScheme = expoConfig.scheme;
105
128
  if (typeof rawScheme === 'string' && rawScheme.trim().length > 0) {
@@ -204,3 +227,27 @@ function sanitizeIdentifierSegment(value) {
204
227
  .replace(/^_+/, '');
205
228
  return cleaned.length > 0 ? cleaned : 'entry';
206
229
  }
230
+ function normalizeStringArray(value) {
231
+ if (!Array.isArray(value)) {
232
+ return [];
233
+ }
234
+ return [...new Set(value
235
+ .filter((entry) => typeof entry === 'string')
236
+ .map((entry) => entry.trim())
237
+ .filter((entry) => entry.length > 0))].sort((left, right) => left.localeCompare(right));
238
+ }
239
+ function normalizeCoverageProfile(value) {
240
+ if (value === 'managed-core' ||
241
+ value === 'managed-native-heavy' ||
242
+ value === 'bare' ||
243
+ value === 'third-party-native-heavy') {
244
+ return value;
245
+ }
246
+ return null;
247
+ }
248
+ function filterDependencySection(section, excludedDependencies) {
249
+ if (!section) {
250
+ return section;
251
+ }
252
+ return Object.fromEntries(Object.entries(section).filter(([dependencyName]) => !excludedDependencies.has(dependencyName)));
253
+ }
@@ -23,15 +23,20 @@ const DEFAULT_RECORD = {
23
23
  supportTier: 'unsupported',
24
24
  note: 'This dependency is not in the current compatibility catalog yet.',
25
25
  };
26
+ const BARE_WORKFLOW_DIRECTORY_NAMES = ['android', 'ios'];
27
+ const BARE_WORKFLOW_DEPENDENCIES = new Set(['expo-build-properties', 'expo-dev-client']);
26
28
  async function buildDoctorReport(projectRoot, options = {}) {
27
29
  const loadedProject = await (0, project_1.loadProject)(projectRoot);
30
+ const doctorConfig = (0, project_1.resolveExpoHarmonyDoctorConfig)(loadedProject.expoConfig);
31
+ const filteredPackageJson = (0, project_1.applyDependencyExclusions)(loadedProject.packageJson, doctorConfig.excludeDependencies);
28
32
  const expoSdkVersion = (0, project_1.detectExpoSdkVersion)(loadedProject.packageJson);
29
33
  const matrix = validatedMatrices_1.VALIDATED_RELEASE_MATRICES[validatedMatrices_1.DEFAULT_VALIDATED_MATRIX_ID];
30
34
  const targetTier = options.targetTier ?? 'verified';
31
- const expoPlugins = (0, project_1.collectExpoPlugins)(loadedProject.expoConfig);
35
+ const excludedPlugins = new Set(doctorConfig.excludePlugins);
36
+ const expoPlugins = (0, project_1.collectExpoPlugins)(loadedProject.expoConfig).filter((pluginName) => !excludedPlugins.has(pluginName));
32
37
  const expoSchemes = (0, project_1.collectExpoSchemes)(loadedProject.expoConfig);
33
38
  const dependencyRecords = new Map();
34
- const declaredDependencies = (0, project_1.collectDeclaredDependencies)(loadedProject.packageJson);
39
+ const declaredDependencies = (0, project_1.collectDeclaredDependencies)(filteredPackageJson);
35
40
  for (const dependency of declaredDependencies) {
36
41
  dependencyRecords.set(dependency.name, createDependencyRecord(dependency.name, dependency.version, dependency.source));
37
42
  }
@@ -41,8 +46,11 @@ async function buildDoctorReport(projectRoot, options = {}) {
41
46
  }
42
47
  }
43
48
  const dependencies = await (0, dependencyInspection_1.annotateDependencyBuildability)(loadedProject.projectRoot, [...dependencyRecords.values()].sort((left, right) => left.name.localeCompare(right.name)));
44
- const blockingIssues = await collectBlockingIssues(loadedProject.projectRoot, loadedProject.expoConfig, loadedProject.packageJson, expoPlugins, expoSchemes, expoSdkVersion, dependencies, matrix, targetTier);
45
- const capabilities = (0, capabilities_1.getCapabilityDefinitionsForProject)(loadedProject.packageJson).map((definition) => ({
49
+ const blockingIssues = await collectBlockingIssues(loadedProject.projectRoot, loadedProject.expoConfig, filteredPackageJson, expoPlugins, expoSchemes, expoSdkVersion, dependencies, matrix, targetTier);
50
+ const excludedDependencies = new Set(doctorConfig.excludeDependencies);
51
+ const capabilities = (0, capabilities_1.getCapabilityDefinitionsForProject)(filteredPackageJson, {
52
+ excludedDependencies,
53
+ }).map((definition) => ({
46
54
  id: definition.id,
47
55
  packageName: definition.packageName,
48
56
  status: definition.status,
@@ -57,15 +65,24 @@ async function buildDoctorReport(projectRoot, options = {}) {
57
65
  sampleRoute: definition.sampleRoute,
58
66
  acceptanceChecklist: [...definition.acceptanceChecklist],
59
67
  }));
68
+ const coverageProfile = await detectCoverageProfile(loadedProject.projectRoot, filteredPackageJson, dependencies, doctorConfig.coverageProfile);
60
69
  const blockingDependencyNames = new Set(blockingIssues
61
70
  .filter((issue) => issue.code.startsWith('dependency.') && issue.subject)
62
71
  .map((issue) => issue.subject));
63
72
  const resolvedDependencies = dependencies.map((dependency) => ({
64
73
  ...dependency,
74
+ gapCategory: resolveDependencyGapCategory(dependency, coverageProfile),
65
75
  blocking: blockingDependencyNames.has(dependency.name),
66
76
  }));
77
+ const nextActions = buildNextActions({
78
+ targetTier,
79
+ coverageProfile,
80
+ blockingIssues,
81
+ dependencies: resolvedDependencies,
82
+ capabilities,
83
+ });
67
84
  const warnings = buildWarnings(loadedProject.expoConfig, expoSdkVersion, resolvedDependencies);
68
- const advisories = buildAdvisories(loadedProject.expoConfig);
85
+ const advisories = buildAdvisories(loadedProject.expoConfig, doctorConfig);
69
86
  return {
70
87
  generatedAt: new Date().toISOString(),
71
88
  projectRoot: loadedProject.projectRoot,
@@ -78,6 +95,7 @@ async function buildDoctorReport(projectRoot, options = {}) {
78
95
  rnohCliVersion: constants_1.RNOH_CLI_VERSION,
79
96
  expoSdkVersion,
80
97
  targetTier,
98
+ coverageProfile,
81
99
  expoConfig: {
82
100
  name: loadedProject.expoConfig.name ?? null,
83
101
  slug: loadedProject.expoConfig.slug ?? null,
@@ -102,6 +120,7 @@ async function buildDoctorReport(projectRoot, options = {}) {
102
120
  },
103
121
  capabilities,
104
122
  blockingIssues,
123
+ nextActions,
105
124
  advisories,
106
125
  warnings,
107
126
  };
@@ -120,6 +139,7 @@ function renderDoctorReport(report) {
120
139
  `Expo SDK: ${report.expoSdkVersion ?? 'unknown'} (recognized ${constants_1.SUPPORTED_EXPO_SDKS.join(', ')})`,
121
140
  `Matrix: ${report.matrixId ?? 'none'}`,
122
141
  `Target tier: ${report.targetTier}`,
142
+ `Coverage profile: ${report.coverageProfile}`,
123
143
  `Eligibility: ${report.eligibility}`,
124
144
  `Schemes: ${report.expoConfig.schemes.join(', ') || 'none'}`,
125
145
  `Plugins: ${report.expoConfig.plugins.join(', ') || 'none'}`,
@@ -131,7 +151,7 @@ function renderDoctorReport(report) {
131
151
  ...report.dependencies.map((dependency) => {
132
152
  const replacement = dependency.replacement ? ` | replacement: ${dependency.replacement}` : '';
133
153
  const blocking = dependency.blocking ? ' | blocking: yes' : '';
134
- return `- [${dependency.status}/${dependency.supportTier}] ${dependency.name}@${dependency.version} (${dependency.source}) - ${dependency.note} | buildability: ${renderDependencyBuildabilityRisk(dependency.buildabilityRisk)}${replacement}${blocking}`;
154
+ return `- [${dependency.status}/${dependency.supportTier}] ${dependency.name}@${dependency.version} (${dependency.source}) - ${dependency.note} | buildability: ${renderDependencyBuildabilityRisk(dependency.buildabilityRisk)} | gap: ${dependency.gapCategory}${replacement}${blocking}`;
135
155
  }),
136
156
  ];
137
157
  if (report.capabilities.length > 0) {
@@ -151,6 +171,9 @@ function renderDoctorReport(report) {
151
171
  if (report.blockingIssues.length > 0) {
152
172
  sections.push('', 'Blocking issues:', ...report.blockingIssues.map((issue) => `- ${issue.code}: ${issue.message}${issue.subject ? ` (${issue.subject})` : ''}`));
153
173
  }
174
+ if (report.nextActions.length > 0) {
175
+ sections.push('', 'Next actions:', ...report.nextActions.map((action, index) => `${index + 1}. ${action}`));
176
+ }
154
177
  if (report.advisories.length > 0) {
155
178
  sections.push('', 'Advisories:', ...report.advisories.map((advisory) => `- ${advisory}`));
156
179
  }
@@ -188,6 +211,7 @@ function createDependencyRecord(name, version, source) {
188
211
  status: matrixRecord.status,
189
212
  supportTier: matrixRecord.supportTier,
190
213
  buildabilityRisk: 'known',
214
+ gapCategory: 'matrix-drift',
191
215
  blocking: false,
192
216
  note: matrixRecord.note,
193
217
  replacement: matrixRecord.replacement,
@@ -335,6 +359,117 @@ async function collectBlockingIssues(projectRoot, expoConfig, packageJson, expoP
335
359
  }
336
360
  return dedupeIssues(issues);
337
361
  }
362
+ async function detectCoverageProfile(projectRoot, packageJson, dependencies, coverageProfileOverride) {
363
+ if (coverageProfileOverride) {
364
+ return coverageProfileOverride;
365
+ }
366
+ for (const directoryName of BARE_WORKFLOW_DIRECTORY_NAMES) {
367
+ if (await fs_extra_1.default.pathExists(path_1.default.join(projectRoot, directoryName))) {
368
+ return 'bare';
369
+ }
370
+ }
371
+ if (dependencies.some((dependency) => isThirdPartyNativeGapDependency(dependency))) {
372
+ return 'third-party-native-heavy';
373
+ }
374
+ if ((0, capabilities_1.getCapabilityDefinitionsForProject)(packageJson).length > 0 ||
375
+ dependencies.some((dependency) => isOfficialExpoDependencyName(dependency.name) && dependency.supportTier !== 'verified')) {
376
+ return 'managed-native-heavy';
377
+ }
378
+ return 'managed-core';
379
+ }
380
+ function resolveDependencyGapCategory(dependency, coverageProfile) {
381
+ if (coverageProfile === 'bare' &&
382
+ (BARE_WORKFLOW_DEPENDENCIES.has(dependency.name) ||
383
+ dependency.name === 'expo' ||
384
+ dependency.name === 'react-native')) {
385
+ return 'bare-workflow-gap';
386
+ }
387
+ if (isOfficialExpoDependencyName(dependency.name) || capabilities_1.CAPABILITY_BY_PACKAGE[dependency.name]) {
388
+ return dependency.supportTier === 'verified' && dependency.status === 'supported'
389
+ ? 'matrix-drift'
390
+ : 'official-module-gap';
391
+ }
392
+ if (isThirdPartyNativeGapDependency(dependency)) {
393
+ return 'third-party-native-gap';
394
+ }
395
+ if (coverageProfile === 'bare') {
396
+ return 'bare-workflow-gap';
397
+ }
398
+ return 'matrix-drift';
399
+ }
400
+ function isOfficialExpoDependencyName(dependencyName) {
401
+ return dependencyName === 'expo' || dependencyName.startsWith('expo-') || dependencyName.startsWith('@expo/');
402
+ }
403
+ function isThirdPartyNativeGapDependency(dependency) {
404
+ if (dependency.name === 'react-native-gesture-handler' ||
405
+ dependency.name === '@react-native-oh-tpl/react-native-gesture-handler') {
406
+ return true;
407
+ }
408
+ if (dependency.buildabilityRisk === 'native-risk') {
409
+ return true;
410
+ }
411
+ return (dependency.supportTier !== 'verified' &&
412
+ !isOfficialExpoDependencyName(dependency.name) &&
413
+ dependency.name.startsWith('react-native'));
414
+ }
415
+ function buildNextActions(input) {
416
+ const actions = [];
417
+ const { targetTier, coverageProfile, blockingIssues, dependencies, capabilities } = input;
418
+ const hasPreviewCapabilities = capabilities.some((capability) => capability.supportTier === 'preview');
419
+ const hasRouterBlockingIssues = blockingIssues.some((issue) => [
420
+ 'dependency.router_peer_missing',
421
+ 'config.router_plugin.missing',
422
+ 'config.scheme.missing',
423
+ 'config.bundle_script.mismatch',
424
+ ].includes(issue.code));
425
+ if (hasBlockingIssueCode(blockingIssues, 'matrix.expo_sdk.unsupported') ||
426
+ hasBlockingIssueCode(blockingIssues, 'dependency.version_mismatch') ||
427
+ hasBlockingIssueCode(blockingIssues, 'dependency.specifier_mismatch') ||
428
+ hasBlockingIssueCode(blockingIssues, 'dependency.required_missing')) {
429
+ 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\`.`);
430
+ }
431
+ if (targetTier === 'verified' && hasPreviewCapabilities) {
432
+ 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.');
433
+ }
434
+ if (hasPreviewCapabilities) {
435
+ actions.push('Keep combined sample smoke for regression coverage, but track bundle/debug/device/release evidence separately for each preview capability before promotion.');
436
+ }
437
+ switch (coverageProfile) {
438
+ case 'managed-core':
439
+ 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.');
440
+ break;
441
+ case 'managed-native-heavy':
442
+ 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.');
443
+ break;
444
+ case 'bare':
445
+ 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.');
446
+ break;
447
+ case 'third-party-native-heavy':
448
+ 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.');
449
+ break;
450
+ }
451
+ if (hasRouterBlockingIssues) {
452
+ 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.');
453
+ }
454
+ if (blockingIssues.some((issue) => issue.code === 'dependency.not_allowed' &&
455
+ (issue.subject === 'react-native-gesture-handler' ||
456
+ issue.subject === '@react-native-oh-tpl/react-native-gesture-handler'))) {
457
+ actions.push('Keep `react-native-gesture-handler` out of the verified lane until its Harmony adapter path has stable doctor, sample, and build coverage.');
458
+ }
459
+ if (hasBlockingIssueCode(blockingIssues, 'config.native_identifier.missing')) {
460
+ actions.push('Set `android.package` or `ios.bundleIdentifier` in Expo config before expecting a strict Harmony build path.');
461
+ }
462
+ if (dependencies.some((dependency) => dependency.buildabilityRisk === 'native-risk')) {
463
+ actions.push('Inspect unknown native-looking dependencies and either replace them, gate them behind preview work, or onboard them explicitly before promising Harmony portability.');
464
+ }
465
+ if (dependencies.some((dependency) => dependency.buildabilityRisk === 'js-only-unknown')) {
466
+ actions.push('Unknown JavaScript-only packages still sit outside the public matrix; verify bundling manually, but prioritize native gaps first.');
467
+ }
468
+ return dedupeStrings(actions);
469
+ }
470
+ function hasBlockingIssueCode(issues, code) {
471
+ return issues.some((issue) => issue.code === code);
472
+ }
338
473
  function buildWarnings(expoConfig, expoSdkVersion, dependencies) {
339
474
  const warnings = [];
340
475
  const expoSdkWarning = (0, project_1.getExpoSdkWarning)(expoSdkVersion);
@@ -364,8 +499,17 @@ function buildWarnings(expoConfig, expoSdkVersion, dependencies) {
364
499
  }
365
500
  return warnings;
366
501
  }
367
- function buildAdvisories(expoConfig) {
502
+ function buildAdvisories(expoConfig, doctorConfig) {
368
503
  const advisories = [];
504
+ if (doctorConfig.excludeDependencies.length > 0) {
505
+ advisories.push(`Expo Harmony doctor excluded dependencies: ${doctorConfig.excludeDependencies.join(', ')}.`);
506
+ }
507
+ if (doctorConfig.excludePlugins.length > 0) {
508
+ advisories.push(`Expo Harmony doctor excluded plugins: ${doctorConfig.excludePlugins.join(', ')}.`);
509
+ }
510
+ if (doctorConfig.coverageProfile) {
511
+ advisories.push(`Expo Harmony doctor forced coverage profile to ${doctorConfig.coverageProfile}.`);
512
+ }
369
513
  if (!expoConfig.android?.package && expoConfig.ios?.bundleIdentifier) {
370
514
  advisories.push('The project is using ios.bundleIdentifier as the only explicit native identifier. Prefer setting android.package as well before claiming release readiness.');
371
515
  }
@@ -375,11 +519,12 @@ function isDependencyAllowedForTargetTier(dependencyName, matrix, targetTier) {
375
519
  if (matrix.allowedDependencies.includes(dependencyName)) {
376
520
  return true;
377
521
  }
378
- const capability = capabilities_1.CAPABILITY_BY_PACKAGE[dependencyName];
379
- if (!capability) {
380
- return false;
522
+ const catalogRecord = dependencyCatalog_1.DEPENDENCY_CATALOG[dependencyName];
523
+ if (catalogRecord) {
524
+ return (0, capabilities_1.isSupportTierAllowed)(catalogRecord.supportTier, targetTier);
381
525
  }
382
- return (0, capabilities_1.isSupportTierAllowed)(capability.supportTier, targetTier);
526
+ const capability = capabilities_1.CAPABILITY_BY_PACKAGE[dependencyName];
527
+ return capability ? (0, capabilities_1.isSupportTierAllowed)(capability.supportTier, targetTier) : false;
383
528
  }
384
529
  function matchesVersionRange(rawVersion, range) {
385
530
  const coerced = semver_1.default.coerce(rawVersion);
@@ -418,3 +563,6 @@ function dedupeIssues(issues) {
418
563
  return true;
419
564
  });
420
565
  }
566
+ function dedupeStrings(values) {
567
+ return [...new Set(values)];
568
+ }
@@ -1,9 +1,10 @@
1
- import { InitResult, SyncResult } from '../types';
1
+ import { InitResult, DoctorReport, SyncResult } from '../types';
2
2
  export { buildDesiredPackageScripts, resolveHarmonyBundleEntryFile, usesExpoRouter, } from './template/support';
3
3
  export declare const BUILD_REQUIRED_MANAGED_FILE_PATHS: readonly [string, string, string, string, string];
4
4
  interface SyncProjectTemplateOptions {
5
5
  forceManagedPaths?: readonly string[];
6
6
  skipJavaScriptDependencyNormalization?: boolean;
7
+ doctorReport?: DoctorReport;
7
8
  }
8
9
  export declare function initProject(projectRoot: string, force?: boolean): Promise<InitResult>;
9
10
  export declare function syncProjectTemplate(projectRoot: string, force?: boolean, options?: SyncProjectTemplateOptions): Promise<SyncResult>;
@@ -64,7 +64,9 @@ exports.BUILD_REQUIRED_MANAGED_FILE_PATHS = [
64
64
  ];
65
65
  async function initProject(projectRoot, force = false) {
66
66
  const report = await (0, report_1.buildDoctorReport)(projectRoot);
67
- const sync = await syncProjectTemplate(projectRoot, force);
67
+ const sync = await syncProjectTemplate(projectRoot, force, {
68
+ doctorReport: report,
69
+ });
68
70
  const packageWarnings = await syncPackageScripts(projectRoot, force);
69
71
  const doctorReportPath = await (0, report_1.writeDoctorReport)(projectRoot, report);
70
72
  return {
@@ -78,7 +80,8 @@ async function syncProjectTemplate(projectRoot, force = false, options = {}) {
78
80
  const loadedProject = await (0, project_1.loadProject)(projectRoot);
79
81
  const identifiers = (0, project_1.deriveHarmonyIdentifiers)(loadedProject.expoConfig, loadedProject.packageJson);
80
82
  const previousToolkitConfig = await (0, metadata_1.readToolkitConfig)(loadedProject.projectRoot);
81
- const desiredFiles = await buildManagedFiles(loadedProject, identifiers, previousToolkitConfig);
83
+ const doctorReport = options.doctorReport ?? (await (0, report_1.buildDoctorReport)(loadedProject.projectRoot));
84
+ const desiredFiles = await buildManagedFiles(loadedProject, identifiers, previousToolkitConfig, doctorReport);
82
85
  const previousManifest = await (0, metadata_1.readManifest)(loadedProject.projectRoot);
83
86
  const forceManagedPaths = new Set(options.forceManagedPaths ?? []);
84
87
  const result = {
@@ -130,11 +133,14 @@ async function syncProjectTemplate(projectRoot, force = false, options = {}) {
130
133
  await fs_extra_1.default.writeJson(result.manifestPath, manifest, { spaces: 2 });
131
134
  return result;
132
135
  }
133
- async function buildManagedFiles(loadedProject, identifiers, previousToolkitConfig) {
136
+ async function buildManagedFiles(loadedProject, identifiers, previousToolkitConfig, doctorReport) {
134
137
  const hasExpoRouter = (0, support_1.usesExpoRouter)(loadedProject.packageJson);
135
- const enabledCapabilities = (0, capabilities_1.getCapabilityDefinitionsForProject)(loadedProject.packageJson);
138
+ const doctorConfig = (0, project_1.resolveExpoHarmonyDoctorConfig)(loadedProject.expoConfig);
139
+ const enabledCapabilities = doctorReport.capabilities;
136
140
  const hasManagedExpoHarmonyPackage = enabledCapabilities.some((capability) => capability.runtimeMode !== 'shim');
137
- const requestedHarmonyPermissions = (0, capabilities_1.collectCapabilityHarmonyPermissions)(loadedProject.packageJson);
141
+ const requestedHarmonyPermissions = (0, capabilities_1.collectCapabilityHarmonyPermissions)(loadedProject.packageJson, {
142
+ excludedDependencies: new Set(doctorConfig.excludeDependencies),
143
+ });
138
144
  const signingLocalConfig = await (0, signing_1.readSigningLocalConfig)(loadedProject.projectRoot);
139
145
  const hvigorPluginFilename = await (0, project_1.resolveRnohHvigorPluginFilename)(loadedProject.projectRoot);
140
146
  const renderedHarmonyRootPackage = renderTemplate(await fs_extra_1.default.readFile(path_1.default.join(TEMPLATE_ROOT, 'oh-package.json5'), 'utf8'), loadedProject, identifiers, hvigorPluginFilename);
@@ -175,6 +181,8 @@ async function buildManagedFiles(loadedProject, identifiers, previousToolkitConf
175
181
  rnohCliVersion: constants_1.RNOH_CLI_VERSION,
176
182
  bundleName: identifiers.bundleName,
177
183
  entryModuleName: identifiers.entryModuleName,
184
+ coverageProfile: doctorReport.coverageProfile,
185
+ doctorConfig,
178
186
  capabilities: enabledCapabilities.map((capability) => ({
179
187
  id: capability.id,
180
188
  packageName: capability.packageName,
@@ -184,6 +192,7 @@ async function buildManagedFiles(loadedProject, identifiers, previousToolkitConf
184
192
  evidenceSource: { ...capability.evidenceSource },
185
193
  })),
186
194
  requestedHarmonyPermissions,
195
+ nextActions: [...doctorReport.nextActions],
187
196
  project: {
188
197
  name: loadedProject.expoConfig.name ?? identifiers.appName,
189
198
  slug: loadedProject.expoConfig.slug ?? identifiers.slug,
@@ -3,5 +3,9 @@ export declare const CAPABILITY_DEFINITIONS: readonly CapabilityDefinition[];
3
3
  export declare const CAPABILITY_BY_PACKAGE: Record<string, CapabilityDefinition>;
4
4
  export declare function compareSupportTiers(left: SupportTier, right: SupportTier): number;
5
5
  export declare function isSupportTierAllowed(dependencyTier: SupportTier, targetTier: DoctorTargetTier): boolean;
6
- export declare function getCapabilityDefinitionsForProject(packageJson: PackageJson): CapabilityDefinition[];
7
- export declare function collectCapabilityHarmonyPermissions(packageJson: PackageJson): string[];
6
+ export declare function getCapabilityDefinitionsForProject(packageJson: PackageJson, options?: {
7
+ excludedDependencies?: ReadonlySet<string>;
8
+ }): CapabilityDefinition[];
9
+ export declare function collectCapabilityHarmonyPermissions(packageJson: PackageJson, options?: {
10
+ excludedDependencies?: ReadonlySet<string>;
11
+ }): string[];
@@ -39,7 +39,7 @@ exports.CAPABILITY_DEFINITIONS = [
39
39
  runtimeMode: 'adapter',
40
40
  evidence: PREVIEW_BASELINE_EVIDENCE,
41
41
  evidenceSource: PREVIEW_BASELINE_EVIDENCE_SOURCE,
42
- note: 'Toolkit-managed Harmony native file-system adapters cover the v1.7.3 preview subset: UTF-8/base64 sandbox I/O, append and partial reads, md5 metadata, and direct downloads into the app sandbox.',
42
+ note: 'Toolkit-managed Harmony native file-system adapters cover the v1.8.0 preview subset: UTF-8/base64 sandbox I/O, append and partial reads, md5 metadata, and direct downloads into the app sandbox.',
43
43
  docsUrl: 'https://github.com/react-native-oh-library/usage-docs/blob/master/en/react-native-fs.md',
44
44
  nativePackageNames: ['react-native-fs'],
45
45
  harmonyPermissions: [],
@@ -58,7 +58,7 @@ exports.CAPABILITY_DEFINITIONS = [
58
58
  runtimeMode: 'adapter',
59
59
  evidence: PREVIEW_BASELINE_EVIDENCE,
60
60
  evidenceSource: PREVIEW_BASELINE_EVIDENCE_SOURCE,
61
- note: 'Toolkit-managed Harmony adapters cover the v1.7.3 preview subset: media and camera permissions, single/multi-select library flows, system photo/video capture, pending-result restore, and richer video asset metadata.',
61
+ note: 'Toolkit-managed Harmony adapters cover the v1.8.0 preview subset: media and camera permissions, single/multi-select library flows, system photo/video capture, pending-result restore, and richer video asset metadata.',
62
62
  docsUrl: 'https://github.com/react-native-oh-library/usage-docs/blob/master/en/react-native-image-picker.md',
63
63
  nativePackageNames: ['react-native-image-picker', 'react-native-permissions'],
64
64
  harmonyPermissions: [
@@ -81,7 +81,7 @@ exports.CAPABILITY_DEFINITIONS = [
81
81
  runtimeMode: 'adapter',
82
82
  evidence: PREVIEW_BASELINE_EVIDENCE,
83
83
  evidenceSource: PREVIEW_BASELINE_EVIDENCE_SOURCE,
84
- note: 'Toolkit-managed Harmony adapters cover the v1.7.3 preview subset: foreground/background permission flows, current and last-known fixes, geocoding, continuous watch subscriptions, and sensor-backed heading updates.',
84
+ note: 'Toolkit-managed Harmony adapters cover the v1.8.0 preview subset: foreground/background permission flows, current and last-known fixes, geocoding, continuous watch subscriptions, and sensor-backed heading updates.',
85
85
  docsUrl: 'https://github.com/react-native-oh-library/usage-docs/blob/master/en/%40react-native-community-geolocation.md',
86
86
  nativePackageNames: ['@react-native-community/geolocation', 'react-native-permissions'],
87
87
  harmonyPermissions: [
@@ -105,7 +105,7 @@ exports.CAPABILITY_DEFINITIONS = [
105
105
  runtimeMode: 'adapter',
106
106
  evidence: PREVIEW_BASELINE_EVIDENCE,
107
107
  evidenceSource: PREVIEW_BASELINE_EVIDENCE_SOURCE,
108
- note: 'Toolkit-managed Harmony adapters cover the v1.7.3 preview subset: embedded camera preview, still capture, preview pause/resume, microphone permission, and in-session video recording controls.',
108
+ note: 'Toolkit-managed Harmony adapters cover the v1.8.0 preview subset: embedded camera preview, still capture, preview pause/resume, microphone permission, and in-session video recording controls.',
109
109
  docsUrl: 'https://github.com/react-native-oh-library/usage-docs/blob/master/en/react-native-camera-kit.md',
110
110
  nativePackageNames: ['react-native-camera-kit', 'react-native-permissions'],
111
111
  harmonyPermissions: ['ohos.permission.CAMERA', 'ohos.permission.MICROPHONE'],
@@ -149,9 +149,11 @@ function compareSupportTiers(left, right) {
149
149
  function isSupportTierAllowed(dependencyTier, targetTier) {
150
150
  return compareSupportTiers(dependencyTier, targetTier) <= 0;
151
151
  }
152
- function getCapabilityDefinitionsForProject(packageJson) {
153
- return exports.CAPABILITY_DEFINITIONS.filter((definition) => (0, project_1.hasDeclaredDependency)(packageJson, definition.packageName)).sort((left, right) => left.packageName.localeCompare(right.packageName));
152
+ function getCapabilityDefinitionsForProject(packageJson, options = {}) {
153
+ const excludedDependencies = options.excludedDependencies ?? new Set();
154
+ return exports.CAPABILITY_DEFINITIONS.filter((definition) => !excludedDependencies.has(definition.packageName) &&
155
+ (0, project_1.hasDeclaredDependency)(packageJson, definition.packageName)).sort((left, right) => left.packageName.localeCompare(right.packageName));
154
156
  }
155
- function collectCapabilityHarmonyPermissions(packageJson) {
156
- return Array.from(new Set(getCapabilityDefinitionsForProject(packageJson).flatMap((definition) => definition.harmonyPermissions))).sort((left, right) => left.localeCompare(right));
157
+ function collectCapabilityHarmonyPermissions(packageJson, options = {}) {
158
+ return Array.from(new Set(getCapabilityDefinitionsForProject(packageJson, options).flatMap((definition) => definition.harmonyPermissions))).sort((left, right) => left.localeCompare(right));
157
159
  }
@@ -112,6 +112,30 @@ exports.DEPENDENCY_CATALOG = {
112
112
  supportTier: 'verified',
113
113
  note: 'File-based routing is treated as part of the validated App Shell matrix when router peers, scheme, and plugin config are present.',
114
114
  },
115
+ '@react-native-async-storage/async-storage': {
116
+ status: 'manual',
117
+ supportTier: 'experimental',
118
+ note: 'Async storage is a high-frequency startup dependency. Treat it as an experimental Harmony intake dependency until the managed adapter path has stable doctor, sample, and build coverage.',
119
+ docsUrl: 'https://github.com/react-native-oh-library',
120
+ },
121
+ 'react-native-safe-area-context': {
122
+ status: 'supported',
123
+ supportTier: 'experimental',
124
+ note: 'Safe-area handling is shimmed by the toolkit for Harmony so App Shell layouts can keep rendering while the native path remains outside the verified matrix.',
125
+ docsUrl: 'https://github.com/react-native-oh-library',
126
+ },
127
+ 'react-native-screens': {
128
+ status: 'manual',
129
+ supportTier: 'experimental',
130
+ note: 'Navigation stacks frequently depend on react-native-screens. Keep it on the experimental lane until the Harmony adapter path has repeatable sample and device coverage.',
131
+ docsUrl: 'https://github.com/react-native-oh-library',
132
+ },
133
+ 'react-native-webview': {
134
+ status: 'manual',
135
+ supportTier: 'experimental',
136
+ note: 'WebView is a common app-shell blocker. Treat it as an experimental intake dependency until the Harmony runtime path is validated on device.',
137
+ docsUrl: 'https://github.com/react-native-oh-library',
138
+ },
115
139
  ...UI_STACK_COMPATIBILITY_RECORDS,
116
140
  'react-native-gesture-handler': {
117
141
  status: 'manual',
@@ -8,4 +8,4 @@ export declare const SUPPORTING_SAMPLE_PATHS: readonly ["examples/official-app-s
8
8
  export declare const VERIFIED_JS_UI_CAPABILITY_NAMES: readonly ["expo-router", "expo-linking", "expo-constants", ...("react-native-reanimated" | "react-native-svg")[]];
9
9
  export declare const PREVIEW_CAPABILITY_DEFINITIONS: import("..").CapabilityDefinition[];
10
10
  export declare const EXPERIMENTAL_CAPABILITY_NAMES: readonly [...string[], "react-native-gesture-handler"];
11
- export declare const PUBLIC_CURRENT_VERSION = "1.7.3";
11
+ export declare const PUBLIC_CURRENT_VERSION = "1.8.1";
@@ -45,7 +45,7 @@ function renderReadmeCurrentStatus(locale) {
45
45
  `| ${previewLabel} | ${joinInlineCode(publicDocs_1.PREVIEW_CAPABILITY_DEFINITIONS.map((definition) => definition.packageName), listJoiner)} |`,
46
46
  `| ${experimentalLabel} | ${joinInlineCode(publicDocs_1.EXPERIMENTAL_CAPABILITY_NAMES, listJoiner)} |`,
47
47
  `| ${releaseTracksLabel} | ${releaseTracksValue} |`,
48
- `| ${telemetryLabel} | \`runtimeMode\` + \`evidence(...)\` + \`evidenceSource(...)\` |`,
48
+ `| ${telemetryLabel} | \`runtimeMode\` + \`evidence(...)\` + \`evidenceSource(...)\` + \`coverageProfile\` + \`nextActions\` |`,
49
49
  `| ${buildPathLabel} | \`doctor -> init -> bundle -> build-hap\` |`,
50
50
  `| ${primarySampleLabel} | \`${publicDocs_1.PRIMARY_SAMPLE_PATH}\` |`,
51
51
  `| ${previewSampleLabel} | \`${publicDocs_1.PREVIEW_SAMPLE_PATH}\` |`,
@@ -70,6 +70,7 @@ function renderReadmeSupportMatrixSection(locale) {
70
70
  '- `doctor-report.json` 的 `capabilities[]` 会带出 `runtimeMode`',
71
71
  '- `doctor-report.json` 与 `toolkit-config.json` 会带出 `evidence.bundle`、`evidence.debugBuild`、`evidence.device`、`evidence.release`',
72
72
  '- `doctor-report.json` 与 `toolkit-config.json` 会带出 `evidenceSource.bundle`、`evidenceSource.debugBuild`、`evidenceSource.device`、`evidenceSource.release`',
73
+ '- `doctor-report.json` 与 `toolkit-config.json` 也会带出 `coverageProfile` 与按顺序排列的 `nextActions`',
73
74
  '- `runtimeMode=shim` 说明当前仍未进入 verified runtime path,即使 bundle / debug build 已经可走通',
74
75
  '- `evidenceSource.device=manual-doc` 表示当前只有人工设备验收记录,不代表机器自动验证',
75
76
  ]
@@ -77,6 +78,7 @@ function renderReadmeSupportMatrixSection(locale) {
77
78
  '- `doctor-report.json` exposes `capabilities[].runtimeMode`',
78
79
  '- `doctor-report.json` and `toolkit-config.json` expose `evidence.bundle`, `evidence.debugBuild`, `evidence.device`, and `evidence.release`',
79
80
  '- `doctor-report.json` and `toolkit-config.json` expose `evidenceSource.bundle`, `evidenceSource.debugBuild`, `evidenceSource.device`, and `evidenceSource.release`',
81
+ '- `doctor-report.json` and `toolkit-config.json` also expose `coverageProfile` plus ordered `nextActions`',
80
82
  '- `runtimeMode=shim` means the capability still has not reached a verified runtime path even if bundling and debug-build scaffolding already exist',
81
83
  '- `evidenceSource.device=manual-doc` means the current device signal comes from manual acceptance records, not automated verification',
82
84
  ];
package/build/plugin.js CHANGED
@@ -16,9 +16,13 @@ const project_2 = require("./core/project");
16
16
  const withExpoHarmony = (config, props = {}) => {
17
17
  validatePluginProps(props);
18
18
  const identifiers = (0, project_1.deriveHarmonyIdentifiers)(config, undefined, props);
19
+ const existingExpoHarmonyConfig = config.extra && typeof config.extra.expoHarmony === 'object' && config.extra.expoHarmony
20
+ ? config.extra.expoHarmony
21
+ : {};
19
22
  config.extra = {
20
23
  ...(config.extra ?? {}),
21
24
  expoHarmony: {
25
+ ...existingExpoHarmonyConfig,
22
26
  bundleName: identifiers.bundleName,
23
27
  entryModuleName: identifiers.entryModuleName,
24
28
  templateVersion: props.templateVersion ?? constants_1.TEMPLATE_VERSION,
package/build/types.d.ts CHANGED
@@ -5,7 +5,19 @@ export type DoctorTargetTier = Exclude<SupportTier, 'unsupported'>;
5
5
  export type CapabilityRuntimeMode = 'shim' | 'adapter' | 'verified';
6
6
  export type CapabilityEvidenceSource = 'automated' | 'manual-doc' | 'none';
7
7
  export type DependencyBuildabilityRisk = 'known' | 'js-only-unknown' | 'native-risk' | 'unresolved';
8
+ export type CoverageProfile = 'managed-core' | 'managed-native-heavy' | 'bare' | 'third-party-native-heavy';
9
+ export type GapCategory = 'matrix-drift' | 'official-module-gap' | 'third-party-native-gap' | 'bare-workflow-gap';
8
10
  export type DependencySource = 'dependency' | 'devDependency' | 'peerDependency' | 'expo-plugin';
11
+ export interface ExpoHarmonyDoctorConfig {
12
+ excludeDependencies?: string[];
13
+ excludePlugins?: string[];
14
+ coverageProfile?: CoverageProfile;
15
+ }
16
+ export interface ExpoHarmonyResolvedDoctorConfig {
17
+ excludeDependencies: string[];
18
+ excludePlugins: string[];
19
+ coverageProfile: CoverageProfile | null;
20
+ }
9
21
  export interface ExpoHarmonyPluginProps {
10
22
  bundleName?: string;
11
23
  entryModuleName?: string;
@@ -69,6 +81,7 @@ export interface DetectedDependency {
69
81
  status: CompatibilityStatus;
70
82
  supportTier: SupportTier;
71
83
  buildabilityRisk: DependencyBuildabilityRisk;
84
+ gapCategory: GapCategory;
72
85
  blocking: boolean;
73
86
  note: string;
74
87
  replacement?: string;
@@ -148,6 +161,7 @@ export interface DoctorReport {
148
161
  rnohCliVersion: string;
149
162
  expoSdkVersion: number | null;
150
163
  targetTier: DoctorTargetTier;
164
+ coverageProfile: CoverageProfile;
151
165
  expoConfig: {
152
166
  name: string | null;
153
167
  slug: string | null;
@@ -162,6 +176,7 @@ export interface DoctorReport {
162
176
  supportSummary: SupportTierSummary;
163
177
  capabilities: ProjectCapabilityReport[];
164
178
  blockingIssues: BlockingIssue[];
179
+ nextActions: string[];
165
180
  advisories: string[];
166
181
  warnings: string[];
167
182
  }
@@ -230,8 +245,11 @@ export interface ToolkitConfig {
230
245
  rnohCliVersion: string;
231
246
  bundleName: string;
232
247
  entryModuleName: string;
248
+ coverageProfile: CoverageProfile;
249
+ doctorConfig: ExpoHarmonyResolvedDoctorConfig;
233
250
  capabilities: ManagedCapabilityRecord[];
234
251
  requestedHarmonyPermissions: string[];
252
+ nextActions: string[];
235
253
  project: {
236
254
  name: string;
237
255
  slug: string;
package/docs/cli-build.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CLI 构建指南
2
2
 
3
- `v1.7.3` 延续 `verified + preview + experimental` 支持分层,并把四项 preview capability 的现有 `🟠` 缺口收口到明确 `🟡` 子集,其中 `expo55-rnoh082-ui-stack` 仍是唯一 verified 矩阵。
3
+ `v1.8.0` 延续 `verified + preview + experimental` 支持分层,并把四项 preview capability 的现有 `🟠` 缺口收口到明确 `🟡` 子集,其中 `expo55-rnoh082-ui-stack` 仍是唯一 verified 矩阵。
4
4
 
5
5
  CLI 命令集合不变:
6
6
 
@@ -2,7 +2,12 @@
2
2
 
3
3
  路径:`examples/official-native-capabilities-sample`
4
4
 
5
- 这个 sample 是 `v1.7.3` 的官方 preview native-capability walkthrough。它的目标不是把 preview 能力包装成 `verified`,而是把四项能力当前真实可承诺的 `🟡` 子集集中演示出来,并把 preview 边界收敛到真机 / release 证据,而不是接口缺口。
5
+ 这个 sample 是 `v1.8.0` 的官方 preview native-capability walkthrough。它的目标不是把 preview 能力包装成 `verified`,而是把四项能力当前真实可承诺的 `🟡` 子集集中演示出来,并把 preview 边界收敛到真机 / release 证据,而不是接口缺口。
6
+
7
+ 从 `v1.8.x` 开始,这个 sample 的角色会固定成两层:
8
+
9
+ - combined smoke:负责四项 preview capability 的总回归
10
+ - per-capability acceptance:每条 route 继续单独维护 device / release 记录,不再让 combined smoke 代替单项证据
6
11
 
7
12
  标记说明:
8
13
 
@@ -22,6 +27,10 @@
22
27
  - `/location`
23
28
  - `/camera`
24
29
 
30
+ 对应的逐 capability 追踪板见:
31
+
32
+ - [v1.8.x capability board](../acceptance/v1.8.x-capability-board.md)
33
+
25
34
  ## 每条 route 的真实范围
26
35
 
27
36
  ### `/file-system`
package/docs/roadmap.md CHANGED
@@ -2,18 +2,19 @@
2
2
 
3
3
  ## 收官定义
4
4
 
5
- 这个项目的“完美落幕”不再定义为“一次性完美支持任意 Expo 项目”。
5
+ 这个项目的终局重新定义为:
6
6
 
7
- 新的收官定义是:
7
+ - 所有 Expo project types 与 package categories,最终都进入同一条 `mainline capability catalog`
8
+ - `v2.0.0` 本身就要接近最完美、最最终的交付形态,而不是“还差最后一段 extension”
9
+ - 公开承诺仍然按 evidence gate 逐步放开,而不是因为目标扩大就提前放宽 `latest`
10
+ - `latest` 继续只承接完整验收的 `verified`
11
+ - `next` 继续承接 fast track,用来加速收敛,但不伪装成 `verified`
8
12
 
9
- - 在 `Managed/CNG Expo` 主线下,主流官方 Expo 能力可稳定分析、可 bundle、可 debug/release 验证、可交付
10
- - `latest` 只承诺已经完成完整验收的 `verified` 能力
11
- - 长尾第三方 native module 不再挤进同一条公开承诺,而是进入 `extension model`
13
+ 这意味着:
12
14
 
13
- 因此,路线会分成两段:
14
-
15
- - `Core Expo Coverage`:主线收官点
16
- - `Long-tail Extension`:收官后的持续扩展模型
15
+ - `Managed/CNG` 仍是当前最成熟的已验证输入
16
+ - bare workflow、高频第三方 native packages、长尾第三方依赖,不再被定义成“收官后才考虑的另一条扩展路线”
17
+ - 它们会被逐步拉回同一条公开主线,只是进入主线的时间点和 evidence gate 不同
17
18
 
18
19
  ## 已完成里程碑
19
20
 
@@ -59,77 +60,171 @@
59
60
  - `expo-location`、`expo-camera` 进入 `preview`
60
61
  - Batch A+B preview capability 进入统一 sample / fixture / 文档 / 验收模型
61
62
  - 形成 `doctor -> sync-template -> bundle -> build-hap --mode debug` 的 preview onboarding baseline
62
- - 公开结论保持保守:可接入,不等于已 verified
63
+ - `doctor` 增加 `buildabilityRisk`
64
+ - 文档生成链收口到 marker block + renderer source data
63
65
 
64
66
  ## 当前主线
65
67
 
66
- ### v1.8 Capability Graduation + Fast Track Release
68
+ ### v1.8.0 Intake Hardening + Parallel Promotion
67
69
 
68
70
  目标日期:`2026-05-15`
69
71
 
70
- 目标:优先把“已经接入的 preview 能力”推进到可晋升状态,而不是只继续堆新模块数量。
72
+ 目标:先把“任何 Expo 项目都能被正确分类并拿到下一步”做硬,再把现有 preview 能力和主线阻塞项并行推进。
73
+
74
+ - `doctor` 增加:
75
+ - `coverageProfile`
76
+ - `gapCategory`
77
+ - `nextActions`
78
+ - 先把任意项目分进四类:
79
+ - `managed-core`
80
+ - `managed-native-heavy`
81
+ - `bare`
82
+ - `third-party-native-heavy`
83
+ - 四项 preview capability 进入同一个 graduation board:
84
+ - `expo-file-system`
85
+ - `expo-image-picker`
86
+ - `expo-location`
87
+ - `expo-camera`
88
+ - `react-native-gesture-handler` 从“矩阵外模糊探索”拉回主线:
89
+ - 必须可被 `doctor` 明确识别
90
+ - 必须有独立 fixture
91
+ - 必须有独立 sample / build / test coverage
92
+ - 来不及 verified 也不能继续停留在灰区
93
+ - sample 策略升级成双层:
94
+ - combined smoke
95
+ - per-capability acceptance
96
+
97
+ ### v1.8.x Capability Board Closeout
98
+
99
+ 目标日期:`2026-06-15`
100
+
101
+ 目标:把 v1.8.0 建好的 intake + promotion 架子收口到可持续执行的证据节奏。
102
+
103
+ - 每个 preview capability 必须单独维护:
104
+ - bundle evidence
105
+ - debug build evidence
106
+ - device acceptance record
107
+ - release acceptance record
108
+ - combined sample 只负责回归烟测,不再替代单 capability 证据
109
+ - `toolkit-config.json`、`doctor-report.json`、roadmap、support matrix、acceptance board 保持同源同步
110
+ - `latest` 仍不承接任何证据未闭环的 capability
111
+
112
+ ### v1.9.0 Bare Workflow Baseline + App Foundation Modules
113
+
114
+ 目标日期:`2026-08-31`
115
+
116
+ 目标:先把 bare workflow 拉进主线,并补齐最容易卡死真实项目的官方基础模块。
117
+
118
+ - bare workflow 正式进入主线里程碑
119
+ - 优先补齐:
120
+ - `expo-secure-store`
121
+ - `expo-asset`
122
+ - `expo-device`
123
+ - `expo-clipboard`
124
+ - `expo-haptics`
125
+ - 高频第三方 native dependencies 开始进入同一 capability catalog
126
+ - 对每个新能力继续坚持:
127
+ - fixture
128
+ - sample route
129
+ - doctor
130
+ - sync-template
131
+ - bundle
132
+ - debug build
133
+ - acceptance 记录
134
+
135
+ ### v1.9.1 Third-party Native Wave A
136
+
137
+ 目标日期:`2026-09-30`
138
+
139
+ 目标:优先清掉最容易挡住“任何 Expo 项目”的高频第三方 native blocker。
140
+
141
+ - 第一批高频第三方 native blocker 进入正式 onboarding:
142
+ - `react-native-gesture-handler`
143
+ - `react-native-safe-area-context`
144
+ - `react-native-screens`
145
+ - `@react-native-async-storage/async-storage`
146
+ - 建立第三方 native capability onboarding checklist:
147
+ - catalog entry
148
+ - shim / adapter strategy
149
+ - doctor diagnostics
150
+ - sample / fixture
151
+ - debug + release evidence
152
+ - 任何进入 Wave A 的 blocker,都不允许继续停留在“矩阵外模糊探索”
153
+
154
+ ### v1.9.2 Third-party Native Wave B + Regression Farm
155
+
156
+ 目标日期:`2026-10-31`
71
157
 
72
- - 建立 capability graduation 流水线:
73
- `doctor --strict`、`doctor --target-tier preview`、`sync-template`、`bundle`、`build-hap --mode debug`、device acceptance、release acceptance
74
- - `DoctorReport`、`toolkit-config.json`、support matrix 全部引入 `runtimeMode` 与 `evidence`
75
- - `expo-file-system`、`expo-image-picker` 优先冲刺 verified 所需的真 runtime 与 device 证据
76
- - `expo-location`、`expo-camera` 从 throw-only shim 推进到可验证的 adapter-backed preview
77
- - 发布节奏改成双轨:
78
- `latest` 只承接完整验收的 verified 能力,`next` 承接 preview fast track
158
+ 目标:扩大第三方 native 覆盖,并建立“任何 Expo 项目”目标需要的持续回归面。
79
159
 
80
- ### v1.9 App Foundation Modules
160
+ - 第二批高频第三方 native 依赖进入主线
161
+ - 引入 project-shape regression farm:
162
+ - managed core
163
+ - managed native heavy
164
+ - bare
165
+ - third-party native heavy
166
+ - 至少维护一组“未知项目导入 -> doctor 分类 -> nextActions -> sample/build gate”的持续回归案例
167
+ - 继续收口 preview capability 的 release evidence,尽量减少 `v2.0.0` 前的残留灰区
81
168
 
82
- 目标日期:`2026-07-31`
169
+ ### v1.10.0 Any-project Intake Freeze
83
170
 
84
- 目标:优先补齐最能解锁真实 Managed/CNG Expo 项目的基础官方模块。
171
+ 目标日期:`2026-11-15`
85
172
 
86
- - 依序接入:`expo-secure-store`、`expo-device`、`expo-asset`、`expo-clipboard`、`expo-haptics`
87
- - 每个能力都必须同时具备:
88
- fixture、sample route、doctor、sync-template、bundle、debug build、acceptance 记录
89
- - 完成 `expo-location`、`expo-camera` verified 晋升
90
- - 如果某项能力无法在同版晋升,则必须明确记录阻塞点与缺失证据,而不是模糊停留
173
+ 目标:先冻结“任何 Expo 项目”入口面的产品故事,确保不存在无法分类、无法给下一步、无法明确阻断原因的项目。
91
174
 
92
- ### v2.0 Managed/CNG Core Expo Coverage
175
+ - `doctor` 对任意 Expo 项目必须满足:
176
+ - 能分类
177
+ - 能指出 blocker 属于官方模块、bare workflow、还是第三方 native gap
178
+ - 能输出有顺序的 remediation actions
179
+ - roadmap / support matrix / acceptance board 与实际 catalog 保持零漂移
180
+ - 对外叙事从“更多项目开始进入主线”升级成“任何项目都有明确处理路径”
93
181
 
94
- 目标日期:`2026-10-31`
182
+ ### v1.11.0 Final Blocker Burn-down
95
183
 
96
- 定义:在单一 `verified` runtime matrix 下,把主流 `Managed/CNG Expo` 生产项目推进到完整可用、可构建、可交付。
184
+ 目标日期:`2026-11-30`
97
185
 
98
- - 官方高频 Expo 能力覆盖达到可交付水平
99
- - `latest` 只包含完整验收能力
100
- - release readiness 路径完整闭环
101
- - roadmap 对外叙事从“sample 可跑”升级为“核心 Managed/CNG Expo 项目可稳定交付”
186
+ 目标:把还会阻止“任何 Expo 项目可靠打包”的剩余高频 blocker 清到只剩极少数明确已知例外。
102
187
 
103
- 这是最早可以对外宣告“主目标收官”的时间点。
188
+ - 逐项清掉剩余高频 blocker:
189
+ - adapter 不稳定
190
+ - bare workflow release gap
191
+ - 高频第三方 native 缺失 release evidence
192
+ - 自动生成 sidecar 与真实项目 drift
193
+ - 对仍无法在 `v2.0.0` 前关闭的项,必须提前明确降级策略与替代路径
194
+ - `v2.0.0` 不接受“先宣布完成,再慢慢补长尾”
104
195
 
105
- ### v2.x Long-tail Native Module Extension
196
+ ### v2.0.0 Any Expo Project Reliable Packaging
106
197
 
107
- 目标日期:`2027-03-31` 前启动
198
+ 目标日期:`2026-12-31`
108
199
 
109
- 目标:把“任何 Expo 项目都能打包成鸿蒙 APP”从内建承诺,转成有边界、可扩展、可持续维护的 extension model。
200
+ 定义:任何 Expo 项目,只要依赖已经存在于主线 capability catalog 内,就能可靠打包成鸿蒙 App;`v2.0.0` 本身就是基本接近最完美、最最终的形态。
110
201
 
111
- - 引入第三方 native module extension / onboarding 模型
112
- - 建立 capability registry、adapter、fixture、acceptance 规范
113
- - 把未知或未验证第三方包从“公开承诺”切换为“显式扩展”
114
- - 不对未验证长尾模块提前给出正式可用声明
202
+ - 不再只覆盖“主流项目”,而是把“任何 Expo 项目都能可靠打包成鸿蒙 App”作为正式版本定义
203
+ - `Managed/CNG` bare workflow 都必须进入稳定交付路径
204
+ - 高频官方 Expo 模块与高频第三方 native blockers 必须已经进入同一公开 capability catalog
205
+ - `doctor -> sync-template -> bundle -> build-hap --mode debug|release` 必须对主线 catalog 内项目形成稳定闭环
206
+ - 只允许极少数、明确列名、已有替代策略的已知例外存在;不允许存在大类空白地带
115
207
 
116
208
  ## 长期方向
117
209
 
118
- - 先做到 `Managed/CNG Core Expo Coverage`
119
- - 再通过 `Long-tail Native Module Extension` 逼近“任意 Expo 项目”
120
- - 推进速度依赖 `next` 快速收敛反馈,但公开承诺继续由 `latest` 控制
210
+ - 所有输入最终都进入同一个 `mainline capability catalog`
211
+ - `v2.0.0` 就是“任何 Expo 项目可靠打包成鸿蒙 App”的正式目标版本
212
+ - `latest` 只由完整验收能力驱动
213
+ - `next` 用来加速收敛主线 backlog,而不是制造伪 verified 叙事
214
+ - roadmap 的关键问题不再是“要不要把某类项目算进主线”,而是“它何时进入主线、当前缺哪格 evidence、下一步怎么 unblock”
121
215
 
122
216
  ## 路线约束
123
217
 
124
- - `v2.0` 前继续坚持单一 verified runtime matrix
125
- - `v2.0` 前不把 bare Expo 拉进主线,只保留 `Managed/CNG`
218
+ - 当前公开 `verified` runtime matrix 仍然只有 `expo55-rnoh082-ui-stack`
126
219
  - `doctor --strict` 继续只代表 `verified`
127
- - `latest` 只承诺完整验收的 `verified` 能力
220
+ - `latest` 只承诺完整验收的 `verified`
128
221
  - `next` 可以承接 preview fast track,但不得伪装成 verified
222
+ - 没有 fixture、sample、文档、构建证据、人工验收记录的能力,不得晋升到 `verified`
129
223
  - roadmap、support matrix、README、acceptance 记录必须在同一 PR 同步更新
130
- - 没有 sample、fixture、文档、构建证据、人工验收记录的能力,不得晋升到 `verified`
224
+ - 目标扩大不等于当前对外承诺扩大;任何公开边界放宽都必须先有证据闭环
131
225
 
132
226
  ## 风险与顺延条件
133
227
 
134
- - 收官前提是从 `2026-03-31` 起持续具备 Harmony 模拟器或真机验证条件,并保持周级别验收节奏
135
- - 如果 Expo SDK / RNOH 在 `2026-05-15` 前发生大矩阵漂移,`v2.0` 收官点应顺延到 `2026-12-31` 左右
228
+ - 继续依赖 Harmony 模拟器或真机的稳定验证条件
229
+ - 继续依赖周级别的 acceptance 节奏,而不是只做 sample 展示
230
+ - 如果 Expo SDK / RNOH 在 `2026-05-15` 前发生较大矩阵漂移,`v1.8.x` 收口和 `v2.0.0` 时间点都需要相应顺延
@@ -1,11 +1,19 @@
1
- # v1.7 支持矩阵
1
+ # v1.8 支持矩阵
2
2
 
3
- `v1.7` 继续采用 tiered support model,但从现在开始把“层级”与“晋升距离”拆开表达:
3
+ `v1.8` 继续采用 tiered support model,但从现在开始把“层级”与“晋升距离”拆开表达:
4
4
 
5
5
  - `supportTier` 表示公开承诺层级
6
6
  - `runtimeMode` 表示当前运行时接入形态:`shim`、`adapter`、`verified`
7
7
  - `evidence` 表示距离 verified 还缺哪些证据格
8
8
  - `evidenceSource` 表示每个证据格来自自动化、人工记录,还是当前无证据
9
+ - `coverageProfile` 表示当前项目属于哪条主线输入形态
10
+ - `nextActions` 表示 toolkit 为当前项目生成的有序下一步
11
+
12
+ 同时,`v1.8.x` 的 roadmap 已经改成单一 `mainline capability catalog` 方向:
13
+
14
+ - bare workflow 与第三方 native package 不再被描述成“收官后才考虑的另一条 extension 终局”
15
+ - `v2.0.0` 本身被定义成“任何 Expo 项目都能可靠打包成鸿蒙 App”的目标版本
16
+ - 但当前公开承诺依旧没有放宽;它们只是进入同一主线 backlog,而不是已经进入 `verified`
9
17
 
10
18
  `doctor --strict` 继续只代表 `verified`。
11
19
 
@@ -84,7 +92,7 @@
84
92
  - `runtimeMode=verified`:说明能力已进入正式承诺,且证据闭环完成
85
93
  <!-- GENERATED:support-matrix-capability-telemetry:end -->
86
94
 
87
- 为了避免把“只差真机”与“当前子 API 还没实现到位”混在一起,`v1.7.x` 文档额外使用以下标记:
95
+ 为了避免把“只差真机”与“当前子 API 还没实现到位”混在一起,`v1.8.x` 文档额外使用以下标记:
88
96
 
89
97
  - `🟡 可用待晋升`:当前子集已经有可信实现,样例和模拟器路径可用;下一步主要缺真机 / release 证据
90
98
  - `⛔ 完全不支持`:能力仍在 catalog 外,或根本不在当前公开承诺范围
@@ -105,6 +113,7 @@
105
113
  - 当前四项 preview capability 都已经完成 preview baseline 的 bundle / debug build / route walkthrough
106
114
  - 四项 preview capability 都已经进入 `adapter` 路径;`device=yes[manual-doc]` 只表示已有人工设备验收记录,仍不代表 verified 或 release-ready
107
115
  - 当前 preview baseline 的默认 evidenceSource 固定为:`bundle/debugBuild=automated`、`device=manual-doc`、`release=none`
116
+ - `v1.8.x` 开始,combined sample smoke 只负责总回归;每项 capability 还必须单独维护 device / release acceptance 记录,见 [acceptance/v1.8.x-capability-board.md](../acceptance/v1.8.x-capability-board.md)
108
117
  - `expo-file-system`
109
118
  - `🟡` 当前主路径是 UTF-8/base64 sandbox I/O、append/partial read、`getInfoAsync({ md5: true })` 与 `downloadAsync`
110
119
  - `expo-image-picker`
@@ -169,12 +178,18 @@ expo-harmony doctor --project-root /path/to/app --target-tier preview
169
178
  - `capabilities[].evidenceSource.debugBuild`
170
179
  - `capabilities[].evidenceSource.device`
171
180
  - `capabilities[].evidenceSource.release`
181
+ - `coverageProfile`
182
+ - `dependencies[].gapCategory`
183
+ - `nextActions[]`
172
184
 
173
185
  判读方式:
174
186
 
175
187
  - 任一 `evidence.* = false`,表示晋升到 verified 前仍缺证据
176
188
  - `evidenceSource.device = manual-doc`,表示该证据来自人工验收记录,不应被表述成自动化设备验证
177
189
  - `runtimeMode !== verified`,表示即使已有部分证据,也仍不属于正式承诺
190
+ - `coverageProfile=bare`,表示当前项目已落入 bare workflow 主线规划,但不等于当前 matrix 已正式支持 bare
191
+ - `gapCategory=third-party-native-gap`,表示问题不应再被模糊描述成普通 matrix drift,而是主线 blocker
192
+ - `nextActions[]` 的顺序即 toolkit 当前建议的处理顺序
178
193
 
179
194
  当前公开阻断 issue code 继续包括:
180
195
 
@@ -208,6 +223,7 @@ toolkit 受管的核心产物仍包括:
208
223
  - `entry/src/main/module.json5` 的 `requestPermissions` 由 toolkit 根据已启用能力自动补齐
209
224
  - preview / experimental 能力的 Metro import path 通过 `.expo-harmony/shims/<package>` 接管
210
225
  - `toolkit-config.json` 会记录 capability 的 `runtimeMode`、`evidence` 与 `evidenceSource`
226
+ - `toolkit-config.json` 也会记录当前项目的 `coverageProfile` 与有序 `nextActions`
211
227
  - verified UI stack 依赖继续走现有 autolinking + `.har` 管理逻辑
212
228
 
213
229
  ## Release Tracks
@@ -236,8 +252,13 @@ toolkit 受管的核心产物仍包括:
236
252
  - release signing / release HAP 验收完成
237
253
  - 文档、矩阵、roadmap、acceptance 记录同 PR 更新
238
254
 
239
- ## 当前不在短期输入范围
255
+ ## 当前仍未进入正式公开承诺
240
256
 
241
257
  - bare Expo
242
258
  - 多 Expo / RNOH 并行 verified 矩阵
243
259
  - 任意第三方 native package 的即时正式承诺
260
+
261
+ 说明:
262
+
263
+ - 上述项目并没有从 roadmap 消失;它们会逐步进入同一条主线 capability catalog
264
+ - 这份 support matrix 只记录当前已经公开验证到哪一步,而不是未来愿景本身
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-harmony-toolkit",
3
- "version": "1.7.3",
3
+ "version": "1.8.1",
4
4
  "description": "面向 Managed/CNG Expo 项目的 HarmonyOS 迁移、准入检查与 UI-stack 构建工具链",
5
5
  "main": "app.plugin.js",
6
6
  "types": "build/index.d.ts",