expo-harmony-toolkit 1.9.0 → 1.9.2

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.9.0-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.9.2-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%20%2B%20bare%20intake-059669?style=flat-square">
15
15
  </p>
@@ -24,7 +24,7 @@
24
24
  </div>
25
25
 
26
26
  > [!IMPORTANT]
27
- > `v1.9.0` keeps the `verified + preview + experimental` model and brings bare workflow intake, five app foundation modules, and the `react-native-gesture-handler` formal slice onto the mainline tracker. The public promise remains tighter: `latest` only carries fully accepted `verified` capabilities, while `next` is reserved for preview fast-track work. The expanded goal still does not widen the current `verified` boundary yet.
27
+ > `v1.9.2` keeps the `v1.9.1` build-hap security baseline and brings `@react-native-async-storage/async-storage`, `react-native-screens`, and `react-native-safe-area-context` into Third-party Native Wave A formal experimental onboarding. The public promise remains tighter: `latest` only carries fully accepted `verified` capabilities, and Wave A does not widen the current `verified` boundary.
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,13 +48,13 @@
48
48
  <!-- GENERATED:readme-current-status:start -->
49
49
  | Item | Status |
50
50
  | --- | --- |
51
- | Current version | `v1.9.0` |
51
+ | Current version | `v1.9.2` |
52
52
  | Support model | `verified + preview + experimental` |
53
53
  | Public `verified` matrix | `expo55-rnoh082-ui-stack` |
54
54
  | Supported input | Managed/CNG Expo projects; bare workflow intake baseline |
55
55
  | `verified` JS/UI capabilities | `expo-router`, `expo-linking`, `expo-constants`, `react-native-reanimated`, `react-native-svg` |
56
56
  | `preview` native capabilities | `expo-file-system`, `expo-image-picker`, `expo-location`, `expo-camera`, `expo-secure-store`, `expo-asset`, `expo-device`, `expo-clipboard`, `expo-haptics` |
57
- | `experimental` capabilities | `expo-notifications`, `react-native-gesture-handler` |
57
+ | `experimental` capabilities | `expo-notifications`, `react-native-gesture-handler`, `@react-native-async-storage/async-storage`, `react-native-screens`, `react-native-safe-area-context` |
58
58
  | Release tracks | `latest` = fully accepted `verified` only; `next` = preview fast track |
59
59
  | Capability telemetry | `runtimeMode` + `evidence(...)` + `evidenceSource(...)` + `coverageProfile` + `nextActions` |
60
60
  | Build path | `doctor -> init -> bundle -> build-hap` |
@@ -71,6 +71,7 @@
71
71
  - `expo-secure-store`, `expo-asset`, `expo-device`, `expo-clipboard`, and `expo-haptics` remain the `preview` shim baseline
72
72
  - `expo-notifications`
73
73
  - `react-native-gesture-handler` remains a formal experimental slice
74
+ - `@react-native-async-storage/async-storage`, `react-native-screens`, and `react-native-safe-area-context` remain Wave A experimental onboarding
74
75
  - multiple public matrices
75
76
 
76
77
  </details>
@@ -176,7 +177,7 @@ Common decision points:
176
177
  <!-- GENERATED:readme-support-matrix:start -->
177
178
  - `verified`: the only public matrix remains `expo55-rnoh082-ui-stack`
178
179
  - `preview`: `expo-file-system`, `expo-image-picker`, `expo-location`, `expo-camera`, `expo-secure-store`, `expo-asset`, `expo-device`, `expo-clipboard`, `expo-haptics`
179
- - `experimental`: `expo-notifications`, `react-native-gesture-handler`
180
+ - `experimental`: `expo-notifications`, `react-native-gesture-handler`, `@react-native-async-storage/async-storage`, `react-native-screens`, `react-native-safe-area-context`
180
181
 
181
182
  `doctor --strict` still means `verified` only. `doctor --target-tier preview` allows the same runtime matrix plus preview-tier capabilities, but that does not promote them into the formal public promise.
182
183
 
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.9.0-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.9.2-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%20%2B%20bare%20intake-059669?style=flat-square">
15
15
  </p>
@@ -24,7 +24,7 @@
24
24
  </div>
25
25
 
26
26
  > [!IMPORTANT]
27
- > `v1.9.0` 延续 `verified + preview + experimental` 三层支持模型,并把 bare workflow intake、五个 app foundation modules 与 `react-native-gesture-handler` formal slice 纳入主线追踪。当前公开承诺仍然收紧为:`latest` 只承诺完整验收的 `verified` 能力,`next` 用于 preview fast track。目标扩大不等于当前 `verified` 边界已经放宽。
27
+ > `v1.9.2` 延续 `v1.9.1` build-hap 安全基线,并把 `@react-native-async-storage/async-storage`、`react-native-screens`、`react-native-safe-area-context` 纳入 Third-party Native Wave A formal experimental onboarding。当前公开承诺仍然收紧为:`latest` 只承诺完整验收的 `verified` 能力,Wave A 不放宽当前 `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,13 +48,13 @@
48
48
  <!-- GENERATED:readme-current-status:start -->
49
49
  | 项目 | 说明 |
50
50
  | --- | --- |
51
- | 当前版本 | `v1.9.0` |
51
+ | 当前版本 | `v1.9.2` |
52
52
  | 支持模型 | `verified + preview + experimental` |
53
53
  | 唯一 `verified` 公开矩阵 | `expo55-rnoh082-ui-stack` |
54
54
  | 输入范围 | Managed/CNG Expo 项目;bare workflow intake baseline |
55
55
  | `verified` JS/UI 能力 | `expo-router`、`expo-linking`、`expo-constants`、`react-native-reanimated`、`react-native-svg` |
56
56
  | `preview` 原生能力 | `expo-file-system`、`expo-image-picker`、`expo-location`、`expo-camera`、`expo-secure-store`、`expo-asset`、`expo-device`、`expo-clipboard`、`expo-haptics` |
57
- | `experimental` 能力 | `expo-notifications`、`react-native-gesture-handler` |
57
+ | `experimental` 能力 | `expo-notifications`、`react-native-gesture-handler`、`@react-native-async-storage/async-storage`、`react-native-screens`、`react-native-safe-area-context` |
58
58
  | 发布轨 | `latest` = fully accepted verified only;`next` = preview fast track |
59
59
  | capability 遥测 | `runtimeMode` + `evidence(...)` + `evidenceSource(...)` + `coverageProfile` + `nextActions` |
60
60
  | 构建链 | `doctor -> init -> bundle -> build-hap` |
@@ -71,6 +71,7 @@
71
71
  - `expo-secure-store`、`expo-asset`、`expo-device`、`expo-clipboard`、`expo-haptics` 仍只属于 `preview` shim baseline
72
72
  - `expo-notifications`
73
73
  - `react-native-gesture-handler` 仍只属于 formal experimental slice
74
+ - `@react-native-async-storage/async-storage`、`react-native-screens`、`react-native-safe-area-context` 仍只属于 Wave A experimental onboarding
74
75
  - 多矩阵并行支持
75
76
 
76
77
  </details>
@@ -176,7 +177,7 @@ pnpm exec expo-harmony build-hap --mode release
176
177
  <!-- GENERATED:readme-support-matrix:start -->
177
178
  - `verified`:唯一公开矩阵仍是 `expo55-rnoh082-ui-stack`
178
179
  - `preview`:`expo-file-system`, `expo-image-picker`, `expo-location`, `expo-camera`, `expo-secure-store`, `expo-asset`, `expo-device`, `expo-clipboard`, `expo-haptics`
179
- - `experimental`:`expo-notifications`, `react-native-gesture-handler`
180
+ - `experimental`:`expo-notifications`, `react-native-gesture-handler`, `@react-native-async-storage/async-storage`, `react-native-screens`, `react-native-safe-area-context`
180
181
 
181
182
  `doctor --strict` 继续只代表 `verified`。`doctor --target-tier preview` 会在同一 runtime matrix 下额外放行 preview 能力,但这不等于它们已经进入正式承诺。
182
183
 
package/build/cli.js CHANGED
@@ -40,6 +40,7 @@ async function run(argv = process.argv) {
40
40
  .description('Build a Harmony HAP from the validated sidecar and bundle outputs')
41
41
  .option('-p, --project-root <path>', 'path to the Expo project')
42
42
  .option('--mode <mode>', 'build mode: debug or release', 'debug')
43
+ .option('--no-har-normalize', 'let ohpm consume file:*.har dependencies without extracting local HARs')
43
44
  .action(buildHap_1.runBuildHapCommand);
44
45
  program
45
46
  .command('init')
@@ -1,5 +1,6 @@
1
1
  export interface BuildHapCommandOptions {
2
2
  projectRoot?: string;
3
3
  mode?: 'debug' | 'release';
4
+ harNormalize?: boolean;
4
5
  }
5
6
  export declare function runBuildHapCommand(options: BuildHapCommandOptions): Promise<void>;
@@ -17,6 +17,7 @@ async function runBuildHapCommand(options) {
17
17
  }
18
18
  const report = await (0, build_1.buildHapProject)(projectRoot, {
19
19
  mode,
20
+ skipHarNormalize: options.harNormalize === false,
20
21
  });
21
22
  await (0, metadata_1.writeBuildReport)(projectRoot, report);
22
23
  if (report.status === 'failed') {
@@ -83,9 +83,10 @@ async function extractHarArchiveToDirectory(archivePath, extractionRoot, package
83
83
  if (!(await fs_extra_1.default.pathExists(archivePath))) {
84
84
  throw new Error(`Local Harmony archive not found: ${archivePath}`);
85
85
  }
86
- const destinationPath = path_1.default.join(extractionRoot, `${sanitizePackageName(packageName)}-${path_1.default.basename(archivePath, path_1.default.extname(archivePath))}`);
86
+ const destinationPath = path_1.default.join(extractionRoot, `${sanitizePackageName(packageName)}-${sanitizePathComponent(path_1.default.basename(archivePath, path_1.default.extname(archivePath)))}`);
87
87
  const stagingRoot = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'expo-harmony-har-'));
88
88
  try {
89
+ await validateHarArchiveMemberNames(archivePath);
89
90
  await execFileAsync('tar', ['-xzf', archivePath, '-C', stagingRoot], {
90
91
  maxBuffer: 20 * 1024 * 1024,
91
92
  });
@@ -93,6 +94,7 @@ async function extractHarArchiveToDirectory(archivePath, extractionRoot, package
93
94
  const sourceRoot = (await fs_extra_1.default.pathExists(path_1.default.join(packagedRoot, 'oh-package.json5')))
94
95
  ? packagedRoot
95
96
  : stagingRoot;
97
+ await validateExtractedHarTree(sourceRoot, stagingRoot);
96
98
  if (!(await fs_extra_1.default.pathExists(path_1.default.join(sourceRoot, 'oh-package.json5')))) {
97
99
  throw new Error(`oh-package.json5 not found after extracting ${archivePath}`);
98
100
  }
@@ -115,7 +117,61 @@ async function extractHarArchiveToDirectory(archivePath, extractionRoot, package
115
117
  }
116
118
  }
117
119
  function sanitizePackageName(packageName) {
118
- return packageName.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
120
+ return sanitizePathComponent(packageName);
121
+ }
122
+ function sanitizePathComponent(value) {
123
+ return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'archive';
124
+ }
125
+ async function validateHarArchiveMemberNames(archivePath) {
126
+ const { stdout } = await execFileAsync('tar', ['-tzf', archivePath], {
127
+ maxBuffer: 20 * 1024 * 1024,
128
+ });
129
+ for (const rawMemberName of stdout.split('\n')) {
130
+ const memberName = rawMemberName.trim();
131
+ if (!memberName) {
132
+ continue;
133
+ }
134
+ const segments = memberName.split('/').filter(Boolean);
135
+ if (path_1.default.isAbsolute(memberName) ||
136
+ memberName.includes('\\') ||
137
+ segments.some((segment) => segment === '..')) {
138
+ throw new Error(`Unsafe path in local Harmony archive: ${memberName}`);
139
+ }
140
+ }
141
+ }
142
+ async function validateExtractedHarTree(rootPath, stagingRoot) {
143
+ const stagingRealPath = await fs_extra_1.default.realpath(stagingRoot);
144
+ const rootRealPath = await fs_extra_1.default.realpath(rootPath);
145
+ if (!isPathInside(rootRealPath, stagingRealPath)) {
146
+ throw new Error(`Extracted local Harmony archive root escaped the staging directory: ${rootPath}`);
147
+ }
148
+ await walkExtractedHarTree(rootPath, rootRealPath);
149
+ }
150
+ async function walkExtractedHarTree(currentPath, rootRealPath) {
151
+ const currentStats = await fs_extra_1.default.lstat(currentPath);
152
+ if (currentStats.isSymbolicLink()) {
153
+ throw new Error(`Symbolic links are not allowed in local Harmony archives: ${currentPath}`);
154
+ }
155
+ if (currentStats.isFile() && currentStats.nlink > 1) {
156
+ throw new Error(`Hard links are not allowed in local Harmony archives: ${currentPath}`);
157
+ }
158
+ if (!currentStats.isDirectory() && !currentStats.isFile()) {
159
+ throw new Error(`Unsupported file type in local Harmony archive: ${currentPath}`);
160
+ }
161
+ const currentRealPath = await fs_extra_1.default.realpath(currentPath);
162
+ if (!isPathInside(currentRealPath, rootRealPath)) {
163
+ throw new Error(`Extracted local Harmony archive entry escaped the package directory: ${currentPath}`);
164
+ }
165
+ if (!currentStats.isDirectory()) {
166
+ return;
167
+ }
168
+ for (const entryName of await fs_extra_1.default.readdir(currentPath)) {
169
+ await walkExtractedHarTree(path_1.default.join(currentPath, entryName), rootRealPath);
170
+ }
171
+ }
172
+ function isPathInside(candidatePath, parentPath) {
173
+ const relativePath = path_1.default.relative(parentPath, candidatePath);
174
+ return relativePath === '' || (!relativePath.startsWith('..') && !path_1.default.isAbsolute(relativePath));
119
175
  }
120
176
  async function readNormalizedLocalHarPackageMetadata(directoryPath, packageName) {
121
177
  await ensureNormalizedLocalHarModuleJson5(directoryPath);
@@ -331,7 +387,7 @@ async function alignRnohCodegenWithNormalizedLocalPackage(harmonyProjectRoot, lo
331
387
  if (!relativeRnohModulePath.startsWith('.')) {
332
388
  relativeRnohModulePath = `./${relativeRnohModulePath}`;
333
389
  }
334
- const updatedHvigorFile = originalHvigorFile.replace(/rnohModulePath:\s*['"][^'"]+['"]/, `rnohModulePath: '${relativeRnohModulePath}'`);
390
+ const updatedHvigorFile = originalHvigorFile.replace(/rnohModulePath:\s*['"][^'"]+['"]/, `rnohModulePath: ${JSON.stringify(relativeRnohModulePath)}`);
335
391
  if (updatedHvigorFile !== originalHvigorFile) {
336
392
  await fs_extra_1.default.writeFile(hvigorFilePath, updatedHvigorFile);
337
393
  }
@@ -2,6 +2,7 @@ import type { BuildReport } from '../types';
2
2
  import type { CommandRunner } from './build/commands';
3
3
  export type { CommandRunner, CommandRunnerResult } from './build/commands';
4
4
  export { renderBuildReport } from './build/reporting';
5
+ export declare const SKIP_HAR_NORMALIZE_WARNING = "Local HAR normalization is disabled; ohpm will consume file:*.har dependencies directly and normalized build-profile/RNOH compatibility steps will be skipped.";
5
6
  interface BundleProjectOptions {
6
7
  env?: NodeJS.ProcessEnv;
7
8
  runner?: CommandRunner;
@@ -11,6 +12,7 @@ interface BuildHapProjectOptions {
11
12
  mode: 'debug' | 'release';
12
13
  env?: NodeJS.ProcessEnv;
13
14
  runner?: CommandRunner;
15
+ skipHarNormalize?: boolean;
14
16
  }
15
17
  export declare function bundleProject(projectRoot: string, options?: BundleProjectOptions): Promise<BuildReport>;
16
18
  export declare function buildHapProject(projectRoot: string, options: BuildHapProjectOptions): Promise<BuildReport>;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.renderBuildReport = void 0;
6
+ exports.SKIP_HAR_NORMALIZE_WARNING = exports.renderBuildReport = void 0;
7
7
  exports.bundleProject = bundleProject;
8
8
  exports.buildHapProject = buildHapProject;
9
9
  exports.getDesiredHarmonyScripts = getDesiredHarmonyScripts;
@@ -18,8 +18,10 @@ const localHar_1 = require("./build/localHar");
18
18
  const commands_1 = require("./build/commands");
19
19
  const reporting_1 = require("./build/reporting");
20
20
  const rnohCompatibility_1 = require("./build/rnohCompatibility");
21
+ const signing_1 = require("./signing");
21
22
  var reporting_2 = require("./build/reporting");
22
23
  Object.defineProperty(exports, "renderBuildReport", { enumerable: true, get: function () { return reporting_2.renderBuildReport; } });
24
+ exports.SKIP_HAR_NORMALIZE_WARNING = 'Local HAR normalization is disabled; ohpm will consume file:*.har dependencies directly and normalized build-profile/RNOH compatibility steps will be skipped.';
23
25
  async function bundleProject(projectRoot, options = {}) {
24
26
  const runtimeEnv = options.env ?? process.env;
25
27
  const runCommand = options.runner ?? commands_1.defaultCommandRunner;
@@ -193,6 +195,7 @@ async function bundleProject(projectRoot, options = {}) {
193
195
  async function buildHapProject(projectRoot, options) {
194
196
  const runtimeEnv = options.env ?? process.env;
195
197
  const runCommand = options.runner ?? commands_1.defaultCommandRunner;
198
+ const skipHarNormalize = options.skipHarNormalize === true || runtimeEnv.EXPO_HARMONY_SKIP_HAR_NORMALIZE === '1';
196
199
  const loadedProject = await (0, project_1.loadProject)(projectRoot);
197
200
  const envReport = await (0, env_1.buildEnvReport)(loadedProject.projectRoot, {
198
201
  env: runtimeEnv,
@@ -202,6 +205,9 @@ async function buildHapProject(projectRoot, options) {
202
205
  ...envReport.warnings.filter((warning) => !warning.startsWith('Harmony sidecar files are not present yet.')),
203
206
  ...relevantEnvAdvisories.map((issue) => `${issue.code}: ${issue.message}`),
204
207
  ];
208
+ if (skipHarNormalize) {
209
+ warnings.push(exports.SKIP_HAR_NORMALIZE_WARNING);
210
+ }
205
211
  const blockingIssues = [];
206
212
  if (!envReport.hvigorPath) {
207
213
  blockingIssues.push({
@@ -286,39 +292,42 @@ async function buildHapProject(projectRoot, options) {
286
292
  let restoreNormalizedRnohCliAutolinkingTemplates = async () => { };
287
293
  let restoreNormalizedRnohCodegenAlignment = async () => { };
288
294
  let restoreNormalizedHarmonyPackageJsons = async () => { };
289
- try {
290
- const normalizedLocalHarDependencies = await (0, localHar_1.normalizeLocalHarDependencies)(harmonyProjectRoot);
291
- normalizedLocalHarPackages = normalizedLocalHarDependencies.packages;
292
- restoreNormalizedDependencies = normalizedLocalHarDependencies.restore;
293
- restoreNormalizedLocalHarModules = await (0, localHar_1.ensureHarmonyBuildProfileSupportsNormalizedLocalDeps)(harmonyProjectRoot, normalizedLocalHarDependencies.packages);
294
- restoreNormalizedRnohCliAutolinkingTemplates =
295
- await (0, rnohCompatibility_1.normalizeProjectRnohCliAutolinkingTemplates)(loadedProject.projectRoot);
296
- restoreNormalizedHarmonyPackageJsons =
297
- await (0, autolinking_1.normalizeKnownHarmonyPackageJsons)(loadedProject.projectRoot);
298
- }
299
- catch (error) {
300
- const message = error instanceof Error ? error.message : String(error);
301
- return (0, reporting_1.createBuildReport)({
302
- projectRoot: loadedProject.projectRoot,
303
- command: 'build-hap',
304
- mode: options.mode,
305
- status: 'failed',
306
- harmonyProjectRoot,
307
- entryFile: bundleReport.entryFile,
308
- bundleOutputPath: bundleReport.bundleOutputPath,
309
- assetsDestPath: bundleReport.assetsDestPath,
310
- artifactPaths: [],
311
- blockingIssues: [
312
- {
313
- code: 'build.hap.failed',
314
- message,
315
- },
316
- ],
317
- warnings: [...warnings, ...bundleReport.warnings],
318
- steps,
319
- });
295
+ let restoreLocalSigningBuildProfile = async () => { };
296
+ if (!skipHarNormalize) {
297
+ try {
298
+ const normalizedLocalHarDependencies = await (0, localHar_1.normalizeLocalHarDependencies)(harmonyProjectRoot);
299
+ normalizedLocalHarPackages = normalizedLocalHarDependencies.packages;
300
+ restoreNormalizedDependencies = normalizedLocalHarDependencies.restore;
301
+ restoreNormalizedLocalHarModules = await (0, localHar_1.ensureHarmonyBuildProfileSupportsNormalizedLocalDeps)(harmonyProjectRoot, normalizedLocalHarDependencies.packages);
302
+ restoreNormalizedRnohCliAutolinkingTemplates =
303
+ await (0, rnohCompatibility_1.normalizeProjectRnohCliAutolinkingTemplates)(loadedProject.projectRoot);
304
+ restoreNormalizedHarmonyPackageJsons =
305
+ await (0, autolinking_1.normalizeKnownHarmonyPackageJsons)(loadedProject.projectRoot);
306
+ }
307
+ catch (error) {
308
+ const message = error instanceof Error ? error.message : String(error);
309
+ return (0, reporting_1.createBuildReport)({
310
+ projectRoot: loadedProject.projectRoot,
311
+ command: 'build-hap',
312
+ mode: options.mode,
313
+ status: 'failed',
314
+ harmonyProjectRoot,
315
+ entryFile: bundleReport.entryFile,
316
+ bundleOutputPath: bundleReport.bundleOutputPath,
317
+ assetsDestPath: bundleReport.assetsDestPath,
318
+ artifactPaths: [],
319
+ blockingIssues: [
320
+ {
321
+ code: 'build.hap.failed',
322
+ message,
323
+ },
324
+ ],
325
+ warnings: [...warnings, ...bundleReport.warnings],
326
+ steps,
327
+ });
328
+ }
329
+ restoreNormalizedRnohCodegenAlignment = await (0, localHar_1.alignRnohCodegenWithNormalizedLocalPackage)(harmonyProjectRoot, normalizedLocalHarPackages);
320
330
  }
321
- restoreNormalizedRnohCodegenAlignment = await (0, localHar_1.alignRnohCodegenWithNormalizedLocalPackage)(harmonyProjectRoot, normalizedLocalHarPackages);
322
331
  const ohpmCommand = (0, commands_1.buildInvocation)(envReport.ohpmPath, ['install', '--all']);
323
332
  try {
324
333
  const ohpmResult = await runCommand(ohpmCommand.file, ohpmCommand.args, {
@@ -349,11 +358,16 @@ async function buildHapProject(projectRoot, options) {
349
358
  steps,
350
359
  });
351
360
  }
352
- if (!normalizedLocalHarPackages.some((localPackage) => localPackage.packageName === '@rnoh/react-native-openharmony')) {
353
- await (0, localHar_1.ensureRnohGeneratedTsShim)(harmonyProjectRoot);
361
+ if (!skipHarNormalize) {
362
+ if (!normalizedLocalHarPackages.some((localPackage) => localPackage.packageName === '@rnoh/react-native-openharmony')) {
363
+ await (0, localHar_1.ensureRnohGeneratedTsShim)(harmonyProjectRoot);
364
+ }
365
+ await (0, localHar_1.patchRnohGeneratedCodegenForNormalizedLocalPackage)(harmonyProjectRoot);
354
366
  }
355
- await (0, localHar_1.patchRnohGeneratedCodegenForNormalizedLocalPackage)(harmonyProjectRoot);
356
367
  await (0, localHar_1.patchKnownHarmonyAdapterCodegenOutputs)(harmonyProjectRoot);
368
+ if (options.mode === 'release') {
369
+ restoreLocalSigningBuildProfile = await temporarilyMergeLocalSigningIntoBuildProfile(loadedProject.projectRoot, harmonyProjectRoot);
370
+ }
357
371
  const hvigorCommand = (0, commands_1.buildInvocation)(envReport.hvigorPath, [
358
372
  'assembleHap',
359
373
  '--no-daemon',
@@ -407,6 +421,7 @@ async function buildHapProject(projectRoot, options) {
407
421
  });
408
422
  }
409
423
  finally {
424
+ await restoreLocalSigningBuildProfile();
410
425
  await restoreNormalizedHarmonyPackageJsons();
411
426
  await restoreNormalizedRnohCodegenAlignment();
412
427
  await restoreNormalizedRnohCliAutolinkingTemplates();
@@ -414,6 +429,25 @@ async function buildHapProject(projectRoot, options) {
414
429
  await restoreNormalizedDependencies();
415
430
  }
416
431
  }
432
+ async function temporarilyMergeLocalSigningIntoBuildProfile(projectRoot, harmonyProjectRoot) {
433
+ const signingLocalConfig = await (0, signing_1.readSigningLocalConfig)(projectRoot);
434
+ if (!signingLocalConfig) {
435
+ return async () => { };
436
+ }
437
+ const buildProfilePath = path_1.default.join(harmonyProjectRoot, 'build-profile.json5');
438
+ if (!(await fs_extra_1.default.pathExists(buildProfilePath))) {
439
+ return async () => { };
440
+ }
441
+ const originalContents = await fs_extra_1.default.readFile(buildProfilePath, 'utf8');
442
+ const nextContents = (0, signing_1.mergeSigningLocalConfigIntoBuildProfile)(originalContents, signingLocalConfig, { includeSecrets: true });
443
+ if (nextContents === originalContents) {
444
+ return async () => { };
445
+ }
446
+ await fs_extra_1.default.writeFile(buildProfilePath, nextContents);
447
+ return async () => {
448
+ await fs_extra_1.default.writeFile(buildProfilePath, originalContents);
449
+ };
450
+ }
417
451
  function getDesiredHarmonyScripts(projectRoot) {
418
452
  return (0, project_1.loadProject)(projectRoot).then((loadedProject) => (0, template_1.buildDesiredPackageScripts)(loadedProject.packageJson));
419
453
  }
@@ -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.9.0";
3
+ export declare const TOOLKIT_VERSION = "1.9.2";
4
4
  export declare const TEMPLATE_VERSION = "rnoh-0.82.29";
5
5
  export declare const RNOH_VERSION = "0.82.29";
6
6
  export declare const RNOH_CLI_VERSION = "0.82.29";
@@ -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.9.0';
6
+ exports.TOOLKIT_VERSION = '1.9.2';
7
7
  exports.TEMPLATE_VERSION = 'rnoh-0.82.29';
8
8
  exports.RNOH_VERSION = '0.82.29';
9
9
  exports.RNOH_CLI_VERSION = '0.82.29';
@@ -26,11 +26,26 @@ const DEFAULT_RECORD = {
26
26
  const BARE_WORKFLOW_DIRECTORY_NAMES = ['android', 'ios'];
27
27
  const BARE_WORKFLOW_DEPENDENCIES = new Set(['expo-build-properties', 'expo-dev-client']);
28
28
  const FORMAL_EXPERIMENTAL_ADAPTER_PAIRS = [
29
+ {
30
+ canonicalPackageName: '@react-native-async-storage/async-storage',
31
+ adapterPackageName: '@react-native-oh-tpl/async-storage',
32
+ },
33
+ {
34
+ canonicalPackageName: 'react-native-screens',
35
+ adapterPackageName: '@react-native-oh-tpl/react-native-screens',
36
+ },
29
37
  {
30
38
  canonicalPackageName: 'react-native-gesture-handler',
31
39
  adapterPackageName: '@react-native-oh-tpl/react-native-gesture-handler',
32
40
  },
33
41
  ];
42
+ const THIRD_PARTY_WAVE_A_PACKAGE_NAMES = new Set([
43
+ '@react-native-async-storage/async-storage',
44
+ '@react-native-oh-tpl/async-storage',
45
+ 'react-native-screens',
46
+ '@react-native-oh-tpl/react-native-screens',
47
+ 'react-native-safe-area-context',
48
+ ]);
34
49
  async function buildDoctorReport(projectRoot, options = {}) {
35
50
  const loadedProject = await (0, project_1.loadProject)(projectRoot);
36
51
  const doctorConfig = (0, project_1.resolveExpoHarmonyDoctorConfig)(loadedProject.expoConfig);
@@ -477,7 +492,12 @@ function buildNextActions(input) {
477
492
  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.');
478
493
  break;
479
494
  case 'third-party-native-heavy':
480
- 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.');
495
+ if (dependencies.some((dependency) => THIRD_PARTY_WAVE_A_PACKAGE_NAMES.has(dependency.name))) {
496
+ actions.push('Keep Third-party Native Wave A on `doctor --target-tier experimental`: pair async-storage and screens with their Harmony adapters, keep safe-area on the toolkit shim, and close device/release evidence before any promotion.');
497
+ }
498
+ else {
499
+ 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.');
500
+ }
481
501
  break;
482
502
  }
483
503
  if (hasRouterBlockingIssues) {
@@ -3,10 +3,13 @@ type SigningLocalAppFragment = {
3
3
  signingConfigs?: JsonRecord[];
4
4
  products?: JsonRecord[];
5
5
  };
6
+ interface MergeSigningOptions {
7
+ includeSecrets?: boolean;
8
+ }
6
9
  export declare function getSigningLocalPath(projectRoot: string): string;
7
10
  export declare function getSigningLocalExamplePath(projectRoot: string): string;
8
11
  export declare function readSigningLocalConfig(projectRoot: string): Promise<SigningLocalAppFragment | null>;
9
- export declare function mergeSigningLocalConfigIntoBuildProfile(rawBuildProfileContents: string, signingLocalConfig: SigningLocalAppFragment | null): string;
12
+ export declare function mergeSigningLocalConfigIntoBuildProfile(rawBuildProfileContents: string, signingLocalConfig: SigningLocalAppFragment | null, options?: MergeSigningOptions): string;
10
13
  export declare function hasSigningConfiguration(rawBuildProfileContents: string | null, signingLocalConfig?: SigningLocalAppFragment | null): boolean;
11
14
  export declare function hasSigningLocalConfiguration(signingLocalConfig: SigningLocalAppFragment | null): boolean;
12
15
  export declare function renderSigningLocalExampleConfig(): string;
@@ -28,12 +28,12 @@ async function readSigningLocalConfig(projectRoot) {
28
28
  const rawConfig = (await fs_extra_1.default.readJson(signingLocalPath));
29
29
  return normalizeSigningLocalConfig(rawConfig);
30
30
  }
31
- function mergeSigningLocalConfigIntoBuildProfile(rawBuildProfileContents, signingLocalConfig) {
31
+ function mergeSigningLocalConfigIntoBuildProfile(rawBuildProfileContents, signingLocalConfig, options = {}) {
32
32
  if (!signingLocalConfig) {
33
33
  return rawBuildProfileContents;
34
34
  }
35
35
  const parsed = json5_1.default.parse(rawBuildProfileContents);
36
- const merged = mergeSigningLocalConfig(parsed, signingLocalConfig);
36
+ const merged = mergeSigningLocalConfig(parsed, signingLocalConfig, options);
37
37
  return JSON.stringify(merged, null, 2) + '\n';
38
38
  }
39
39
  function hasSigningConfiguration(rawBuildProfileContents, signingLocalConfig = null) {
@@ -99,7 +99,7 @@ function normalizeSigningLocalConfig(rawConfig) {
99
99
  ...(products ? { products } : {}),
100
100
  };
101
101
  }
102
- function mergeSigningLocalConfig(buildProfile, signingLocalConfig) {
102
+ function mergeSigningLocalConfig(buildProfile, signingLocalConfig, options) {
103
103
  const nextBuildProfile = {
104
104
  ...buildProfile,
105
105
  app: isJsonRecord(buildProfile.app)
@@ -109,7 +109,7 @@ function mergeSigningLocalConfig(buildProfile, signingLocalConfig) {
109
109
  : {},
110
110
  };
111
111
  if (Array.isArray(signingLocalConfig.signingConfigs)) {
112
- nextBuildProfile.app.signingConfigs = signingLocalConfig.signingConfigs.map((entry) => ({ ...entry }));
112
+ nextBuildProfile.app.signingConfigs = signingLocalConfig.signingConfigs.map((entry) => cloneSigningConfigForBuildProfile(entry, options));
113
113
  }
114
114
  if (Array.isArray(signingLocalConfig.products)) {
115
115
  const currentProducts = Array.isArray(nextBuildProfile.app.products)
@@ -135,6 +135,21 @@ function mergeSigningLocalConfig(buildProfile, signingLocalConfig) {
135
135
  }
136
136
  return nextBuildProfile;
137
137
  }
138
+ function cloneSigningConfigForBuildProfile(entry, options) {
139
+ const nextEntry = { ...entry };
140
+ if (options.includeSecrets) {
141
+ return nextEntry;
142
+ }
143
+ delete nextEntry.storePassword;
144
+ delete nextEntry.keyPassword;
145
+ if (isJsonRecord(nextEntry.material)) {
146
+ const material = { ...nextEntry.material };
147
+ delete material.storePassword;
148
+ delete material.keyPassword;
149
+ nextEntry.material = material;
150
+ }
151
+ return nextEntry;
152
+ }
138
153
  function hasNonEmptySigningConfigList(signingConfigs) {
139
154
  return Array.isArray(signingConfigs) && signingConfigs.some(isJsonRecord);
140
155
  }
@@ -8,6 +8,7 @@ const camera_1 = require("./renderers/camera");
8
8
  const fileSystem_1 = require("./renderers/fileSystem");
9
9
  const imagePicker_1 = require("./renderers/imagePicker");
10
10
  const location_1 = require("./renderers/location");
11
+ const runtimeShims_1 = require("./runtimeShims");
11
12
  const CAPABILITY_SHIM_RENDERERS = {
12
13
  'expo-file-system': {
13
14
  adapter: fileSystem_1.renderExpoFileSystemHarmonyAdapterShim,
@@ -45,6 +46,10 @@ const CAPABILITY_SHIM_RENDERERS = {
45
46
  adapter: appFoundation_1.renderExpoHapticsPreviewShim,
46
47
  preview: appFoundation_1.renderExpoHapticsPreviewShim,
47
48
  },
49
+ 'react-native-safe-area-context': {
50
+ adapter: runtimeShims_1.renderReactNativeSafeAreaContextHarmonyShim,
51
+ preview: runtimeShims_1.renderReactNativeSafeAreaContextHarmonyShim,
52
+ },
48
53
  };
49
54
  exports.MANAGED_EXPO_HARMONY_MODULE_RENDERERS = [
50
55
  {
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderMetroConfig = renderMetroConfig;
4
4
  function renderMetroConfig(enabledCapabilities) {
5
5
  const previewCapabilityAliases = enabledCapabilities
6
- .filter((capability) => capability.supportTier === 'preview' ||
7
- capability.supportTier === 'experimental')
6
+ .filter((capability) => capability.packageName !== 'react-native-safe-area-context' &&
7
+ (capability.packageName.startsWith('expo-') || capability.runtimeMode === 'shim') &&
8
+ (capability.supportTier === 'preview' ||
9
+ capability.supportTier === 'experimental'))
8
10
  .map((capability) => ` '${capability.packageName}': path.resolve(__dirname, '.expo-harmony/shims/${capability.packageName}'),`)
9
11
  .join('\n');
10
12
  return `const fs = require('fs');
@@ -42,6 +42,18 @@ const APP_FOUNDATION_BASELINE_EVIDENCE_SOURCE = {
42
42
  device: 'none',
43
43
  release: 'none',
44
44
  };
45
+ const THIRD_PARTY_WAVE_A_BASELINE_EVIDENCE = {
46
+ bundle: true,
47
+ debugBuild: true,
48
+ device: false,
49
+ release: false,
50
+ };
51
+ const THIRD_PARTY_WAVE_A_BASELINE_EVIDENCE_SOURCE = {
52
+ bundle: 'automated',
53
+ debugBuild: 'automated',
54
+ device: 'none',
55
+ release: 'none',
56
+ };
45
57
  exports.CAPABILITY_DEFINITIONS = [
46
58
  {
47
59
  id: 'expo-file-system',
@@ -261,6 +273,63 @@ exports.CAPABILITY_DEFINITIONS = [
261
273
  'Record debug HAP evidence before any promotion decision; device and release remain separate gates.',
262
274
  ],
263
275
  },
276
+ {
277
+ id: 'async-storage',
278
+ packageName: '@react-native-async-storage/async-storage',
279
+ status: 'manual',
280
+ supportTier: 'experimental',
281
+ runtimeMode: 'adapter',
282
+ evidence: THIRD_PARTY_WAVE_A_BASELINE_EVIDENCE,
283
+ evidenceSource: THIRD_PARTY_WAVE_A_BASELINE_EVIDENCE_SOURCE,
284
+ note: 'v1.9.2 Wave A tracks Async Storage through its Harmony adapter, but persistence behavior remains experimental until device and release evidence are closed.',
285
+ docsUrl: 'https://github.com/react-native-oh-library/usage-docs',
286
+ nativePackageNames: ['@react-native-oh-tpl/async-storage'],
287
+ harmonyPermissions: [],
288
+ sampleRoute: '/third-party-wave-a/async-storage',
289
+ acceptanceChecklist: [
290
+ 'Install the canonical package with the matching Harmony adapter.',
291
+ 'Bundle a minimal Async Storage read/write/remove surface.',
292
+ 'Keep real persistence, migration, and release HAP runtime behavior pending device evidence.',
293
+ ],
294
+ },
295
+ {
296
+ id: 'react-native-screens',
297
+ packageName: 'react-native-screens',
298
+ status: 'manual',
299
+ supportTier: 'experimental',
300
+ runtimeMode: 'adapter',
301
+ evidence: THIRD_PARTY_WAVE_A_BASELINE_EVIDENCE,
302
+ evidenceSource: THIRD_PARTY_WAVE_A_BASELINE_EVIDENCE_SOURCE,
303
+ note: 'v1.9.2 Wave A tracks Screens through its Harmony adapter metadata, while managed autolinking and navigation-stack runtime behavior remain promotion gaps.',
304
+ docsUrl: 'https://github.com/react-native-oh-library',
305
+ nativePackageNames: ['@react-native-oh-tpl/react-native-screens'],
306
+ harmonyPermissions: [],
307
+ sampleRoute: '/third-party-wave-a/screens',
308
+ acceptanceChecklist: [
309
+ 'Install the canonical package with the matching Harmony adapter.',
310
+ 'Bundle a minimal navigation stack that imports react-native-screens.',
311
+ 'Do not claim verified support until adapter autolinking, device, and release evidence are recorded.',
312
+ ],
313
+ },
314
+ {
315
+ id: 'react-native-safe-area-context',
316
+ packageName: 'react-native-safe-area-context',
317
+ status: 'manual',
318
+ supportTier: 'experimental',
319
+ runtimeMode: 'shim',
320
+ evidence: THIRD_PARTY_WAVE_A_BASELINE_EVIDENCE,
321
+ evidenceSource: THIRD_PARTY_WAVE_A_BASELINE_EVIDENCE_SOURCE,
322
+ note: 'v1.9.2 Wave A formalizes the existing toolkit safe-area shim so app-shell layouts keep bundling while native safe-area measurements stay outside verified.',
323
+ docsUrl: 'https://github.com/react-native-oh-library',
324
+ nativePackageNames: [],
325
+ harmonyPermissions: [],
326
+ sampleRoute: '/third-party-wave-a/safe-area',
327
+ acceptanceChecklist: [
328
+ 'Render SafeAreaProvider, SafeAreaView, and safe-area hooks through the toolkit shim.',
329
+ 'Keep the Metro alias on .expo-harmony/shims/react-native-safe-area-context.',
330
+ 'Validate real native inset measurement separately before any adapter promotion.',
331
+ ],
332
+ },
264
333
  ];
265
334
  exports.CAPABILITY_BY_PACKAGE = Object.fromEntries(exports.CAPABILITY_DEFINITIONS.map((definition) => [definition.packageName, definition]));
266
335
  const SUPPORT_TIER_ORDER = {
@@ -138,19 +138,19 @@ exports.DEPENDENCY_CATALOG = {
138
138
  '@react-native-async-storage/async-storage': {
139
139
  status: 'manual',
140
140
  supportTier: 'experimental',
141
- 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.',
141
+ note: 'v1.9.2 Wave A tracks Async Storage through its Harmony adapter, but persistence behavior still needs device and release evidence before verified promotion.',
142
142
  docsUrl: 'https://github.com/react-native-oh-library',
143
143
  },
144
144
  'react-native-safe-area-context': {
145
- status: 'supported',
145
+ status: 'manual',
146
146
  supportTier: 'experimental',
147
- 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.',
147
+ note: 'v1.9.2 Wave A formalizes the toolkit-managed safe-area shim while real native inset measurement remains outside the verified matrix.',
148
148
  docsUrl: 'https://github.com/react-native-oh-library',
149
149
  },
150
150
  'react-native-screens': {
151
151
  status: 'manual',
152
152
  supportTier: 'experimental',
153
- 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.',
153
+ note: 'v1.9.2 Wave A tracks Screens through its Harmony adapter metadata; navigation-stack runtime behavior still needs device and release evidence.',
154
154
  docsUrl: 'https://github.com/react-native-oh-library',
155
155
  },
156
156
  'react-native-webview': {
@@ -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[];
11
- export declare const PUBLIC_CURRENT_VERSION = "1.9.0";
11
+ export declare const PUBLIC_CURRENT_VERSION = "1.9.2";
package/docs/cli-build.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CLI 构建指南
2
2
 
3
- `v1.9.0` 延续 `verified + preview + experimental` 支持分层,把 bare workflow 放进 intake baseline,并把五个 app foundation modules 纳入 preview shim baseline;`expo55-rnoh082-ui-stack` 仍是唯一 verified 矩阵。
3
+ `v1.9.2` 延续 `verified + preview + experimental` 支持分层,把 Third-party Native Wave A 纳入 formal experimental onboarding;`expo55-rnoh082-ui-stack` 仍是唯一 verified 矩阵。
4
4
 
5
5
  CLI 命令集合不变:
6
6
 
@@ -48,6 +48,9 @@ expo-harmony build-hap --mode debug
48
48
  当前 experimental formal slice:
49
49
 
50
50
  - `react-native-gesture-handler`
51
+ - `@react-native-async-storage/async-storage`
52
+ - `react-native-screens`
53
+ - `react-native-safe-area-context`
51
54
 
52
55
  ## UI stack 依赖安装注意事项
53
56
 
@@ -90,7 +93,7 @@ pnpm install --ignore-scripts
90
93
 
91
94
  额外说明:
92
95
 
93
- - `react-native-gesture-handler` 已进入 formal experimental slice,但仍不属于 verified 公开承诺
96
+ - `react-native-gesture-handler` 与 Wave A 第三方 native packages 已进入 formal experimental onboarding,但仍不属于 verified 公开承诺
94
97
  - bare workflow 当前只有 intake / debug baseline,不代表 release-ready
95
98
  - `doctor --strict` 仍只代表完整验收的 verified 能力
96
99
 
@@ -105,6 +108,27 @@ expo-harmony env --strict
105
108
  expo-harmony build-hap --mode debug
106
109
  ```
107
110
 
111
+ ### HAR normalize opt-out
112
+
113
+ 默认情况下,`build-hap` 会把 root / entry `oh-package.json5` 里的 `file:*.har` 本地依赖解压到 `harmony/expo-harmony-local-deps`,再让 `ohpm` 消费解压后的目录。这条路径仍是默认 verified build path。
114
+
115
+ 如果项目已经确认当前 DevEco / ohpm 能直接消费纯 HAR,可以显式跳过这一步:
116
+
117
+ ```bash
118
+ EXPO_HARMONY_SKIP_HAR_NORMALIZE=1 expo-harmony build-hap --mode debug
119
+ expo-harmony build-hap --mode debug --no-har-normalize
120
+ ```
121
+
122
+ 开启后:
123
+
124
+ - `file:../node_modules/.../*.har` specifier 会保持原样
125
+ - 不生成 `expo-harmony-local-deps`
126
+ - 不临时注册 normalized local HAR modules 到 `build-profile.json5`
127
+ - 不执行依赖 normalized local RNOH 目录的 codegen / path 兜底
128
+ - `ohpm install --all` 与 `hvigor assembleHap` 继续执行
129
+
130
+ 该开关是 escape hatch,不代表 opt-out 路径和默认路径具备完全相同的兼容兜底。
131
+
108
132
  ### Release
109
133
 
110
134
  ```bash
@@ -45,7 +45,7 @@ pnpm run harmony:build:release
45
45
 
46
46
  - `.expo-harmony/signing.local.example.json` 是可直接复制的本地 signing 範本
47
47
  - `.expo-harmony/signing.local.json` 是本地 signing 入口
48
- - toolkit 会把本地 signing 合并进 `harmony/build-profile.json5`
48
+ - toolkit 只会把非密钥 signing 引用写进 `harmony/build-profile.json5`;`storePassword` 与 `keyPassword` 会留在本地 signing 文件,并在 release build 期间临时注入
49
49
  - 如果 signing 没配置,`harmony:env` 会明确报缺失;这属于预期阻断
50
50
 
51
51
  ## 不覆盖的内容
@@ -182,7 +182,7 @@ pnpm run harmony:build:release
182
182
 
183
183
  - `.expo-harmony/signing.local.example.json` 是可直接复制的本地 signing 範本
184
184
  - `.expo-harmony/signing.local.json` 是本地 signing 入口
185
- - toolkit 会把本地 signing 合并进 `harmony/build-profile.json5`
185
+ - toolkit 只会把非密钥 signing 引用写进 `harmony/build-profile.json5`;`storePassword` 与 `keyPassword` 会留在本地 signing 文件,并在 release build 期间临时注入
186
186
  - 即使 release 构建可走通,在没有真机前,这四项能力也仍保持 `preview`
187
187
 
188
188
  ## 当前结论
package/docs/roadmap.md CHANGED
@@ -66,7 +66,7 @@
66
66
 
67
67
  ## 当前主线
68
68
 
69
- `v1.8.x` 已完成 repo 内可收口项,当前主线切到 `v1.9.x`。`v1.9.0` 先落 bare workflow baseline、app foundation modules 与 `react-native-gesture-handler` formal slice;剩余需要真机或 release HAP 的证据继续保留在 v1.8.x capability board,不阻塞 v1.9.x 的 bare / foundation / third-party onboarding 工作。
69
+ `v1.8.x` 已完成 repo 内可收口项,当前主线切到 `v1.9.x`。`v1.9.0` 先落 bare workflow baseline、app foundation modules 与 `react-native-gesture-handler` formal slice;`v1.9.1` 作为 build-hap patch release,补上 HAR normalize opt-out;`v1.9.2` 推进 Third-party Native Wave A,把 async-storage、screens 与 safe-area 拉进 formal experimental onboarding。剩余需要真机或 release HAP 的证据继续保留在 v1.8.x capability board,不阻塞 v1.9.x 的 bare / foundation / third-party onboarding 工作。
70
70
 
71
71
  ### v1.8.0 Intake Hardening + Parallel Promotion
72
72
 
@@ -153,10 +153,33 @@
153
153
  - acceptance 记录
154
154
  - 本阶段不改变 `verified` 边界;foundation modules 以 `preview` + `runtimeMode=shim` 记录,`react-native-gesture-handler` 以 `experimental` + formal adapter slice 记录
155
155
 
156
- ### v1.9.1 Third-party Native Wave A
156
+ ### v1.9.1 Build HAP HAR Normalize Opt-out
157
+
158
+ 目标日期:`2026-06-02`
159
+
160
+ 目标:把 issue #1 中的本地 HAR 解压 opt-out 做成正式 patch release,同时不改变默认 verified build path。
161
+
162
+ - `build-hap` 新增显式 opt-out:
163
+ - `EXPO_HARMONY_SKIP_HAR_NORMALIZE=1`
164
+ - `expo-harmony build-hap --no-har-normalize`
165
+ - 默认继续执行现有 `normalizeLocalHarDependencies` 路径:
166
+ - 解压 `file:*.har` 到 `expo-harmony-local-deps`
167
+ - 临时改写 root / entry `oh-package.json5`
168
+ - 注册 normalized local HAR modules
169
+ - 执行依赖 normalized local package 的 RNOH codegen / path 兜底
170
+ - opt-out 开启后:
171
+ - 保留 `file:../node_modules/.../*.har`
172
+ - 让 `ohpm install --all` 直接消费纯 HAR
173
+ - 在 build report / CLI 文档里明确提示跳过的自动兜底
174
+ - 本阶段只改变 build-hap escape hatch,不扩大 `verified` 能力边界
175
+ - `v1.9.0` 已经存在于 npm `next`,本阶段发布为 `v1.9.1` 并进入 `latest`
176
+
177
+ ### v1.9.2 Third-party Native Wave A
157
178
 
158
179
  目标日期:`2026-09-30`
159
180
 
181
+ 状态:implementation baseline complete;device / release evidence carryover。
182
+
160
183
  目标:优先清掉最容易挡住“任何 Expo 项目”的高频第三方 native blocker。
161
184
 
162
185
  - 第一批高频第三方 native blocker 进入正式 onboarding:
@@ -171,8 +194,13 @@
171
194
  - sample / fixture
172
195
  - debug + release evidence
173
196
  - 任何进入 Wave A 的 blocker,都不允许继续停留在“矩阵外模糊探索”
197
+ - 本阶段发布为 `v1.9.2` 并进入 `latest`,但不改变 `verified` 边界:
198
+ - `@react-native-async-storage/async-storage` 以 `experimental` + `runtimeMode=adapter` 记录
199
+ - `react-native-screens` 以 `experimental` + `runtimeMode=adapter` 记录
200
+ - `react-native-safe-area-context` 以 `experimental` + `runtimeMode=shim` 记录
201
+ - `doctor --strict` 与 `doctor --target-tier preview` 仍阻断 Wave A
174
202
 
175
- ### v1.9.2 Third-party Native Wave B + Regression Farm
203
+ ### v1.9.3 Third-party Native Wave B + Regression Farm
176
204
 
177
205
  目标日期:`2026-10-31`
178
206
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  - DevEco Studio / Harmony SDK 已正确安装
10
10
  - 项目已生成 `harmony/`
11
- - `harmony/build-profile.json5` 中存在可用的 `signingConfigs`
11
+ - `.expo-harmony/signing.local.json` 中存在可用的 `signingConfigs`
12
12
  - AppGallery Connect / 本地签名材料与工程配置一致
13
13
 
14
14
  `.expo-harmony/signing.local.json` 的最小结构与 `.expo-harmony/signing.local.example.json` 一致:
@@ -59,6 +59,8 @@ expo-harmony env
59
59
  - `release` 构建不应视为 ready
60
60
  - 可以直接从 `.expo-harmony/signing.local.example.json` 复制出 `.expo-harmony/signing.local.json` 再填入真实材料
61
61
 
62
+ `expo-harmony init` / `sync-template` 只会把非密钥 signing 引用写入可版本化的 `harmony/build-profile.json5`。`storePassword` 与 `keyPassword` 只应保留在 `.expo-harmony/signing.local.json`;`build-hap --mode release` 会在本地构建期间临时注入这些 secret,并在 Hvigor 结束后恢复 `build-profile.json5`。
63
+
62
64
  ## 推荐流程
63
65
 
64
66
  ### Debug 验证
@@ -14,7 +14,7 @@
14
14
  - bare workflow 与第三方 native package 不再被描述成“收官后才考虑的另一条 extension 终局”
15
15
  - `v2.0.0` 本身被定义成“任何 Expo 项目都能可靠打包成鸿蒙 App”的目标版本
16
16
  - 但当前公开承诺依旧没有放宽;它们只是进入同一主线 backlog,而不是已经进入 `verified`
17
- - `v1.9.0` 新增的是 bare intake baseline、app foundation preview shim baseline 与 `react-native-gesture-handler` formal experimental slice,不是 verified 扩容
17
+ - `v1.9.2` 新增的是 Third-party Native Wave A formal experimental onboarding,不是 verified 扩容
18
18
 
19
19
  `doctor --strict` 继续只代表 `verified`。
20
20
 
@@ -138,6 +138,9 @@
138
138
  | --- | --- | --- | --- | --- |
139
139
  | `expo-notifications` | `experimental` | `shim` | `bundle=no, debugBuild=no, device=no, release=no` | 服务链路与交付故事未打通 |
140
140
  | `react-native-gesture-handler` | `experimental` | `adapter` | `bundle=yes[automated], debugBuild=yes[automated], device=no[none], release=no[none]` | v1.9.0 formal acceptance slice;必须与 `@react-native-oh-tpl/react-native-gesture-handler` 成对接入,仍不在公开 verified 矩阵内 |
141
+ | `@react-native-async-storage/async-storage` | `experimental` | `adapter` | `bundle=yes[automated], debugBuild=yes[automated], device=no[none], release=no[none]` | v1.9.2 Wave A formal onboarding;必须与 `@react-native-oh-tpl/async-storage` 成对接入,持久化行为仍需 device / release evidence |
142
+ | `react-native-screens` | `experimental` | `adapter` | `bundle=yes[automated], debugBuild=yes[automated], device=no[none], release=no[none]` | v1.9.2 Wave A formal onboarding;必须与 `@react-native-oh-tpl/react-native-screens` 成对接入,navigation-stack runtime 仍需 evidence |
143
+ | `react-native-safe-area-context` | `experimental` | `shim` | `bundle=yes[automated], debugBuild=yes[automated], device=no[none], release=no[none]` | v1.9.2 Wave A formal onboarding;当前仍走 toolkit-managed safe-area shim,不切换到 native adapter |
141
144
 
142
145
  ## UI Stack 配对规则
143
146
 
@@ -269,6 +272,7 @@ toolkit 受管的核心产物仍包括:
269
272
 
270
273
  - bare Expo;当前只进入 intake / debug baseline
271
274
  - `expo-secure-store`、`expo-asset`、`expo-device`、`expo-clipboard`、`expo-haptics` 当前仍只是 preview shim baseline
275
+ - `@react-native-async-storage/async-storage`、`react-native-screens`、`react-native-safe-area-context` 当前仍只是 Wave A experimental onboarding
272
276
  - 多 Expo / RNOH 并行 verified 矩阵
273
277
  - 任意第三方 native package 的即时正式承诺
274
278
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-harmony-toolkit",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "面向 Managed/CNG Expo 项目的 HarmonyOS 迁移、准入检查与 UI-stack 构建工具链",
5
5
  "main": "app.plugin.js",
6
6
  "types": "build/index.d.ts",