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 +9 -6
- package/README.md +9 -6
- package/build/core/constants.d.ts +1 -1
- package/build/core/constants.js +1 -1
- package/build/core/dependencyInspection.js +20 -2
- package/build/core/project.d.ts +3 -1
- package/build/core/project.js +47 -0
- package/build/core/report.js +159 -11
- package/build/core/template.d.ts +2 -1
- package/build/core/template.js +14 -5
- package/build/data/capabilities.d.ts +6 -2
- package/build/data/capabilities.js +10 -8
- package/build/data/dependencyCatalog.js +24 -0
- package/build/data/publicDocs.d.ts +1 -1
- package/build/docs/render.js +3 -1
- package/build/plugin.js +4 -0
- package/build/types.d.ts +18 -0
- package/docs/cli-build.md +1 -1
- package/docs/official-native-capabilities-sample.md +10 -1
- package/docs/roadmap.md +146 -51
- package/docs/support-matrix.md +25 -4
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
207
|
+
`v1.8.x` 的 preview walkthrough sample,用来承接 `expo-file-system`、`expo-image-picker`、`expo-location`、`expo-camera` 的核心支持子集、permission、bundle、debug 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.
|
|
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";
|
package/build/core/constants.js
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/build/core/project.d.ts
CHANGED
|
@@ -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;
|
package/build/core/project.js
CHANGED
|
@@ -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
|
+
}
|
package/build/core/report.js
CHANGED
|
@@ -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
|
|
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)(
|
|
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,
|
|
45
|
-
const
|
|
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
|
|
379
|
-
if (
|
|
380
|
-
return
|
|
522
|
+
const catalogRecord = dependencyCatalog_1.DEPENDENCY_CATALOG[dependencyName];
|
|
523
|
+
if (catalogRecord) {
|
|
524
|
+
return (0, capabilities_1.isSupportTierAllowed)(catalogRecord.supportTier, targetTier);
|
|
381
525
|
}
|
|
382
|
-
|
|
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
|
+
}
|
package/build/core/template.d.ts
CHANGED
|
@@ -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>;
|
package/build/core/template.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
7
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
11
|
+
export declare const PUBLIC_CURRENT_VERSION = "1.8.1";
|
package/build/docs/render.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
- `latest` 只承诺已经完成完整验收的 `verified` 能力
|
|
11
|
-
- 长尾第三方 native module 不再挤进同一条公开承诺,而是进入 `extension model`
|
|
13
|
+
这意味着:
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
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
|
-
-
|
|
63
|
+
- `doctor` 增加 `buildabilityRisk`
|
|
64
|
+
- 文档生成链收口到 marker block + renderer source data
|
|
63
65
|
|
|
64
66
|
## 当前主线
|
|
65
67
|
|
|
66
|
-
### v1.8
|
|
68
|
+
### v1.8.0 Intake Hardening + Parallel Promotion
|
|
67
69
|
|
|
68
70
|
目标日期:`2026-05-15`
|
|
69
71
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
### v1.10.0 Any-project Intake Freeze
|
|
83
170
|
|
|
84
|
-
|
|
171
|
+
目标日期:`2026-11-15`
|
|
85
172
|
|
|
86
|
-
|
|
87
|
-
- 每个能力都必须同时具备:
|
|
88
|
-
fixture、sample route、doctor、sync-template、bundle、debug build、acceptance 记录
|
|
89
|
-
- 完成 `expo-location`、`expo-camera` verified 晋升
|
|
90
|
-
- 如果某项能力无法在同版晋升,则必须明确记录阻塞点与缺失证据,而不是模糊停留
|
|
173
|
+
目标:先冻结“任何 Expo 项目”入口面的产品故事,确保不存在无法分类、无法给下一步、无法明确阻断原因的项目。
|
|
91
174
|
|
|
92
|
-
|
|
175
|
+
- `doctor` 对任意 Expo 项目必须满足:
|
|
176
|
+
- 能分类
|
|
177
|
+
- 能指出 blocker 属于官方模块、bare workflow、还是第三方 native gap
|
|
178
|
+
- 能输出有顺序的 remediation actions
|
|
179
|
+
- roadmap / support matrix / acceptance board 与实际 catalog 保持零漂移
|
|
180
|
+
- 对外叙事从“更多项目开始进入主线”升级成“任何项目都有明确处理路径”
|
|
93
181
|
|
|
94
|
-
|
|
182
|
+
### v1.11.0 Final Blocker Burn-down
|
|
95
183
|
|
|
96
|
-
|
|
184
|
+
目标日期:`2026-11-30`
|
|
97
185
|
|
|
98
|
-
|
|
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.
|
|
196
|
+
### v2.0.0 Any Expo Project Reliable Packaging
|
|
106
197
|
|
|
107
|
-
目标日期:`
|
|
198
|
+
目标日期:`2026-12-31`
|
|
108
199
|
|
|
109
|
-
|
|
200
|
+
定义:任何 Expo 项目,只要依赖已经存在于主线 capability catalog 内,就能可靠打包成鸿蒙 App;`v2.0.0` 本身就是基本接近最完美、最最终的形态。
|
|
110
201
|
|
|
111
|
-
-
|
|
112
|
-
-
|
|
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
|
-
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
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
|
-
- `
|
|
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
|
-
-
|
|
224
|
+
- 目标扩大不等于当前对外承诺扩大;任何公开边界放宽都必须先有证据闭环
|
|
131
225
|
|
|
132
226
|
## 风险与顺延条件
|
|
133
227
|
|
|
134
|
-
-
|
|
135
|
-
-
|
|
228
|
+
- 继续依赖 Harmony 模拟器或真机的稳定验证条件
|
|
229
|
+
- 继续依赖周级别的 acceptance 节奏,而不是只做 sample 展示
|
|
230
|
+
- 如果 Expo SDK / RNOH 在 `2026-05-15` 前发生较大矩阵漂移,`v1.8.x` 收口和 `v2.0.0` 时间点都需要相应顺延
|
package/docs/support-matrix.md
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
# v1.
|
|
1
|
+
# v1.8 支持矩阵
|
|
2
2
|
|
|
3
|
-
`v1.
|
|
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.
|
|
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 只记录当前已经公开验证到哪一步,而不是未来愿景本身
|