expo-harmony-toolkit 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.en.md +20 -9
  2. package/README.md +22 -11
  3. package/build/core/autolinking.d.ts +10 -0
  4. package/build/core/autolinking.js +421 -0
  5. package/build/core/build/commands.d.ts +19 -0
  6. package/build/core/build/commands.js +61 -0
  7. package/build/core/build/compatibilityShims.d.ts +1 -0
  8. package/build/core/build/compatibilityShims.js +272 -0
  9. package/build/core/build/localHar.d.ts +13 -0
  10. package/build/core/build/localHar.js +337 -0
  11. package/build/core/build/reporting.d.ts +19 -0
  12. package/build/core/build/reporting.js +67 -0
  13. package/build/core/build/rnohCompatibility.d.ts +1 -0
  14. package/build/core/build/rnohCompatibility.js +38 -0
  15. package/build/core/build.d.ts +4 -12
  16. package/build/core/build.js +70 -762
  17. package/build/core/constants.d.ts +2 -1
  18. package/build/core/constants.js +3 -2
  19. package/build/core/dependencyInspection.d.ts +2 -0
  20. package/build/core/dependencyInspection.js +112 -0
  21. package/build/core/env.js +1 -1
  22. package/build/core/report.js +33 -10
  23. package/build/core/signing.d.ts +2 -0
  24. package/build/core/signing.js +30 -0
  25. package/build/core/template/capabilityRegistry.d.ts +9 -0
  26. package/build/core/template/capabilityRegistry.js +84 -0
  27. package/build/core/template/expoModulesCoreShim.d.ts +2 -0
  28. package/build/core/template/expoModulesCoreShim.js +279 -0
  29. package/build/core/template/metro.d.ts +2 -0
  30. package/build/core/template/metro.js +213 -0
  31. package/build/core/template/nativeFiles.d.ts +8 -0
  32. package/build/core/template/nativeFiles.js +333 -0
  33. package/build/core/template/renderers/camera.d.ts +4 -0
  34. package/build/core/template/renderers/camera.js +612 -0
  35. package/build/core/template/renderers/fileSystem.d.ts +4 -0
  36. package/build/core/template/renderers/fileSystem.js +814 -0
  37. package/build/core/template/renderers/imagePicker.d.ts +4 -0
  38. package/build/core/template/renderers/imagePicker.js +753 -0
  39. package/build/core/template/renderers/location.d.ts +4 -0
  40. package/build/core/template/renderers/location.js +836 -0
  41. package/build/core/template/runtimeShims.d.ts +2 -0
  42. package/build/core/template/runtimeShims.js +192 -0
  43. package/build/core/template/support.d.ts +11 -0
  44. package/build/core/template/support.js +108 -0
  45. package/build/core/template.d.ts +2 -6
  46. package/build/core/template.js +47 -3966
  47. package/build/data/capabilities.js +45 -19
  48. package/build/data/dependencyCatalog.js +10 -0
  49. package/build/data/publicDocs.d.ts +11 -0
  50. package/build/data/publicDocs.js +28 -0
  51. package/build/data/validatedMatrices.js +9 -1
  52. package/build/docs/render.d.ts +9 -0
  53. package/build/docs/render.js +248 -0
  54. package/build/types.d.ts +12 -0
  55. package/docs/cli-build.md +1 -1
  56. package/docs/npm-release.md +1 -0
  57. package/docs/official-app-shell-sample.md +2 -1
  58. package/docs/official-minimal-sample.md +2 -1
  59. package/docs/official-native-capabilities-sample.md +28 -47
  60. package/docs/official-ui-stack-sample.md +2 -1
  61. package/docs/signing-and-release.md +3 -0
  62. package/docs/support-matrix.md +38 -16
  63. package/package.json +13 -5
  64. package/docs/v1.5.1-acceptance.md +0 -385
  65. package/docs/v1.6.0-acceptance.md +0 -193
  66. package/docs/v1.7.0-acceptance.md +0 -111
  67. package/docs/v1.7.1-acceptance.md +0 -111
@@ -0,0 +1,814 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderExpoHarmonyFileSystemTurboModule = renderExpoHarmonyFileSystemTurboModule;
4
+ exports.renderExpoFileSystemHarmonyAdapterShim = renderExpoFileSystemHarmonyAdapterShim;
5
+ exports.renderExpoFileSystemPreviewShim = renderExpoFileSystemPreviewShim;
6
+ function renderExpoHarmonyFileSystemTurboModule() {
7
+ return `import fs from '@ohos.file.fs';
8
+ import { AnyThreadTurboModuleContext, AnyThreadTurboModule } from '@rnoh/react-native-openharmony/ts';
9
+
10
+ type FileInfoOptions = {
11
+ md5?: boolean;
12
+ };
13
+
14
+ type WriteOptions = {
15
+ encoding?: string;
16
+ append?: boolean;
17
+ };
18
+
19
+ type ReadOptions = {
20
+ encoding?: string;
21
+ position?: number;
22
+ length?: number;
23
+ };
24
+
25
+ type MakeDirectoryOptions = {
26
+ intermediates?: boolean;
27
+ };
28
+
29
+ type DeleteOptions = {
30
+ idempotent?: boolean;
31
+ };
32
+
33
+ type FileInfoResult = {
34
+ exists: boolean;
35
+ path: string;
36
+ isDirectory: boolean;
37
+ size?: number;
38
+ modificationTime?: number;
39
+ md5?: string;
40
+ };
41
+
42
+ type DownloadOptions = {
43
+ headers?: Record<string, string>;
44
+ md5?: boolean;
45
+ };
46
+
47
+ type DownloadResult = {
48
+ uri: string;
49
+ status: number;
50
+ headers: Record<string, string>;
51
+ md5?: string;
52
+ };
53
+
54
+ export class ExpoHarmonyFileSystemTurboModule extends AnyThreadTurboModule {
55
+ public static readonly NAME = 'ExpoHarmonyFileSystem';
56
+
57
+ public constructor(ctx: AnyThreadTurboModuleContext) {
58
+ super(ctx);
59
+ this.ensureManagedDirectoriesSync();
60
+ }
61
+
62
+ getConstants(): {
63
+ documentDirectoryPath: string;
64
+ cacheDirectoryPath: string;
65
+ bundleDirectoryPath: string | null;
66
+ } {
67
+ const abilityContext = this.ctx.uiAbilityContext as {
68
+ bundleCodeDir?: string;
69
+ };
70
+
71
+ return {
72
+ documentDirectoryPath: this.documentDirectoryPath,
73
+ cacheDirectoryPath: this.cacheDirectoryPath,
74
+ bundleDirectoryPath:
75
+ typeof abilityContext.bundleCodeDir === 'string' && abilityContext.bundleCodeDir.length > 0
76
+ ? abilityContext.bundleCodeDir
77
+ : null,
78
+ };
79
+ }
80
+
81
+ async getInfo(path: string, options?: FileInfoOptions): Promise<FileInfoResult> {
82
+ const normalizedPath = this.normalizeSandboxPath(path);
83
+ const stat = await this.getStatOrNull(normalizedPath);
84
+
85
+ if (!stat) {
86
+ return {
87
+ exists: false,
88
+ path: normalizedPath,
89
+ isDirectory: false,
90
+ };
91
+ }
92
+
93
+ return {
94
+ exists: true,
95
+ path: normalizedPath,
96
+ isDirectory: stat.isDirectory(),
97
+ size: Number(stat.size),
98
+ modificationTime: Number(stat.mtime),
99
+ md5:
100
+ options?.md5 === true && !stat.isDirectory()
101
+ ? this.computePreviewDigest(await this.readFileBytes(normalizedPath))
102
+ : undefined,
103
+ };
104
+ }
105
+
106
+ async readAsString(path: string, options?: ReadOptions): Promise<string> {
107
+ const normalizedPath = this.normalizeSandboxPath(path);
108
+ const encoding = options?.encoding ?? 'utf8';
109
+ const bytes = await this.readFileBytes(normalizedPath);
110
+ const position = typeof options?.position === 'number' && options.position > 0
111
+ ? Math.floor(options.position)
112
+ : 0;
113
+ const length = typeof options?.length === 'number' && options.length >= 0
114
+ ? Math.floor(options.length)
115
+ : bytes.length - position;
116
+ const slicedBytes = bytes.slice(position, position + length);
117
+
118
+ if (encoding === 'base64') {
119
+ return this.encodeBase64(slicedBytes);
120
+ }
121
+
122
+ return this.decodeUtf8(slicedBytes);
123
+ }
124
+
125
+ async writeAsString(path: string, contents: string, options?: WriteOptions): Promise<void> {
126
+ const normalizedPath = this.normalizeSandboxPath(path);
127
+ const encoding = options?.encoding ?? 'utf8';
128
+
129
+ await this.ensureParentDirectory(normalizedPath);
130
+
131
+ const file = await fs.open(
132
+ normalizedPath,
133
+ fs.OpenMode.READ_WRITE |
134
+ fs.OpenMode.CREATE |
135
+ (options?.append === true ? fs.OpenMode.APPEND : fs.OpenMode.TRUNC),
136
+ );
137
+
138
+ try {
139
+ if (encoding === 'base64') {
140
+ await fs.write(file.fd, this.decodeBase64(contents).buffer);
141
+ } else {
142
+ await fs.write(file.fd, contents);
143
+ }
144
+ } finally {
145
+ await fs.close(file);
146
+ }
147
+ }
148
+
149
+ async deletePath(path: string, options?: DeleteOptions): Promise<void> {
150
+ const normalizedPath = this.normalizeSandboxPath(path);
151
+ await this.deleteInternal(normalizedPath, options?.idempotent === true);
152
+ }
153
+
154
+ async makeDirectory(path: string, options?: MakeDirectoryOptions): Promise<void> {
155
+ const normalizedPath = this.normalizeSandboxPath(path);
156
+ await fs.mkdir(normalizedPath, options?.intermediates === true);
157
+ }
158
+
159
+ async readDirectory(path: string): Promise<string[]> {
160
+ const normalizedPath = this.normalizeSandboxPath(path);
161
+ const stat = await fs.stat(normalizedPath);
162
+
163
+ if (!stat.isDirectory()) {
164
+ throw new Error('readDirectory expects a directory path.');
165
+ }
166
+
167
+ return fs.listFile(normalizedPath);
168
+ }
169
+
170
+ async copy(from: string, to: string): Promise<void> {
171
+ const fromPath = this.normalizeSandboxPath(from);
172
+ const toPath = this.normalizeSandboxPath(to);
173
+ await this.copyInternal(fromPath, toPath);
174
+ }
175
+
176
+ async move(from: string, to: string): Promise<void> {
177
+ const fromPath = this.normalizeSandboxPath(from);
178
+ const toPath = this.normalizeSandboxPath(to);
179
+ const stat = await fs.stat(fromPath);
180
+
181
+ await this.ensureParentDirectory(toPath);
182
+
183
+ if (stat.isDirectory()) {
184
+ await this.copyInternal(fromPath, toPath);
185
+ await this.deleteInternal(fromPath, false);
186
+ return;
187
+ }
188
+
189
+ await fs.moveFile(fromPath, toPath);
190
+ }
191
+
192
+ async download(url: string, destinationPath: string, options?: DownloadOptions): Promise<DownloadResult> {
193
+ const normalizedDestinationPath = this.normalizeSandboxPath(destinationPath);
194
+ await this.ensureParentDirectory(normalizedDestinationPath);
195
+ const fetchFn = (globalThis as {
196
+ fetch?: (input: string, init?: { headers?: Record<string, string> }) => Promise<{
197
+ arrayBuffer: () => Promise<ArrayBuffer>;
198
+ status?: number;
199
+ }>;
200
+ }).fetch;
201
+
202
+ if (typeof fetchFn !== 'function') {
203
+ throw new Error('ExpoHarmonyFileSystem requires global fetch support for downloadAsync.');
204
+ }
205
+
206
+ const response = await fetchFn(url, {
207
+ headers: options?.headers ?? {},
208
+ });
209
+ const responseBuffer = new Uint8Array(await response.arrayBuffer());
210
+ const file = await fs.open(
211
+ normalizedDestinationPath,
212
+ fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC,
213
+ );
214
+
215
+ try {
216
+ await fs.write(file.fd, responseBuffer.buffer);
217
+ } finally {
218
+ await fs.close(file);
219
+ }
220
+
221
+ return {
222
+ uri: normalizedDestinationPath,
223
+ status: Number(response.status ?? 200),
224
+ headers: {},
225
+ md5: options?.md5 === true ? this.computePreviewDigest(responseBuffer) : undefined,
226
+ };
227
+ }
228
+
229
+ private get documentDirectoryPath(): string {
230
+ return \`\${this.ctx.uiAbilityContext.filesDir}/expo-harmony/document\`;
231
+ }
232
+
233
+ private get cacheDirectoryPath(): string {
234
+ return \`\${this.ctx.uiAbilityContext.cacheDir}/expo-harmony/cache\`;
235
+ }
236
+
237
+ private ensureManagedDirectoriesSync(): void {
238
+ this.ensureDirectorySync(this.documentDirectoryPath);
239
+ this.ensureDirectorySync(this.cacheDirectoryPath);
240
+ }
241
+
242
+ private ensureDirectorySync(directoryPath: string): void {
243
+ if (!fs.accessSync(directoryPath)) {
244
+ fs.mkdirSync(directoryPath, true);
245
+ }
246
+ }
247
+
248
+ private async ensureParentDirectory(targetPath: string): Promise<void> {
249
+ const parentPath = this.getParentPath(targetPath);
250
+
251
+ if (!parentPath) {
252
+ return;
253
+ }
254
+
255
+ const parentStat = await this.getStatOrNull(parentPath);
256
+
257
+ if (parentStat) {
258
+ if (!parentStat.isDirectory()) {
259
+ throw new Error(\`Expected parent path to be a directory: \${parentPath}\`);
260
+ }
261
+
262
+ return;
263
+ }
264
+
265
+ await fs.mkdir(parentPath, true);
266
+ }
267
+
268
+ private getParentPath(targetPath: string): string | null {
269
+ let normalizedPath = targetPath;
270
+
271
+ while (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
272
+ normalizedPath = normalizedPath.slice(0, -1);
273
+ }
274
+ const slashIndex = normalizedPath.lastIndexOf('/');
275
+
276
+ if (slashIndex <= 0) {
277
+ return null;
278
+ }
279
+
280
+ return normalizedPath.slice(0, slashIndex);
281
+ }
282
+
283
+ private async deleteInternal(targetPath: string, idempotent: boolean): Promise<void> {
284
+ const stat = await this.getStatOrNull(targetPath);
285
+
286
+ if (!stat) {
287
+ if (idempotent) {
288
+ return;
289
+ }
290
+
291
+ throw new Error(\`No file or directory exists at \${targetPath}.\`);
292
+ }
293
+
294
+ if (stat.isDirectory()) {
295
+ const entries = await fs.listFile(targetPath);
296
+
297
+ for (const entryName of entries) {
298
+ await this.deleteInternal(\`\${targetPath}/\${entryName}\`, idempotent);
299
+ }
300
+
301
+ await fs.rmdir(targetPath);
302
+ return;
303
+ }
304
+
305
+ await fs.unlink(targetPath);
306
+ }
307
+
308
+ private async copyInternal(fromPath: string, toPath: string): Promise<void> {
309
+ const stat = await fs.stat(fromPath);
310
+
311
+ await this.ensureParentDirectory(toPath);
312
+
313
+ if (stat.isDirectory()) {
314
+ await fs.mkdir(toPath, true);
315
+ const entries = await fs.listFile(fromPath);
316
+
317
+ for (const entryName of entries) {
318
+ await this.copyInternal(\`\${fromPath}/\${entryName}\`, \`\${toPath}/\${entryName}\`);
319
+ }
320
+
321
+ return;
322
+ }
323
+
324
+ await fs.copyFile(fromPath, toPath);
325
+ }
326
+
327
+ private async getStatOrNull(targetPath: string): Promise<fs.Stat | null> {
328
+ try {
329
+ return await fs.stat(targetPath);
330
+ } catch (error) {
331
+ if (this.isNoSuchFileError(error)) {
332
+ return null;
333
+ }
334
+
335
+ throw error;
336
+ }
337
+ }
338
+
339
+ private isNoSuchFileError(error: unknown): boolean {
340
+ return (
341
+ typeof error === 'object' &&
342
+ error !== null &&
343
+ 'code' in error &&
344
+ Number((error as { code?: number }).code) === 13900002
345
+ );
346
+ }
347
+
348
+ private async readFileBytes(targetPath: string): Promise<Uint8Array> {
349
+ const stat = await fs.stat(targetPath);
350
+ const file = await fs.open(targetPath, fs.OpenMode.READ_ONLY);
351
+ const buffer = new ArrayBuffer(Number(stat.size ?? 0));
352
+
353
+ try {
354
+ await fs.read(file.fd, buffer);
355
+ return new Uint8Array(buffer);
356
+ } finally {
357
+ await fs.close(file);
358
+ }
359
+ }
360
+
361
+ private decodeUtf8(bytes: Uint8Array): string {
362
+ let encoded = '';
363
+
364
+ for (const byte of bytes) {
365
+ encoded += '%' + byte.toString(16).padStart(2, '0');
366
+ }
367
+
368
+ try {
369
+ return decodeURIComponent(encoded);
370
+ } catch (_error) {
371
+ return Array.from(bytes, (byte) => String.fromCharCode(byte)).join('');
372
+ }
373
+ }
374
+
375
+ private encodeBase64(bytes: Uint8Array): string {
376
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
377
+ let encoded = '';
378
+
379
+ for (let index = 0; index < bytes.length; index += 3) {
380
+ const byte1 = bytes[index] ?? 0;
381
+ const byte2 = bytes[index + 1] ?? 0;
382
+ const byte3 = bytes[index + 2] ?? 0;
383
+ const combined = (byte1 << 16) | (byte2 << 8) | byte3;
384
+
385
+ encoded += alphabet[(combined >> 18) & 63] ?? 'A';
386
+ encoded += alphabet[(combined >> 12) & 63] ?? 'A';
387
+ encoded += index + 1 < bytes.length ? alphabet[(combined >> 6) & 63] ?? 'A' : '=';
388
+ encoded += index + 2 < bytes.length ? alphabet[combined & 63] ?? 'A' : '=';
389
+ }
390
+
391
+ return encoded;
392
+ }
393
+
394
+ private decodeBase64(contents: string): Uint8Array {
395
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
396
+ const sanitizedContents = contents.replace(/\\s+/g, '');
397
+ const bytes: number[] = [];
398
+
399
+ for (let index = 0; index < sanitizedContents.length; index += 4) {
400
+ const chunk = sanitizedContents.slice(index, index + 4);
401
+ const char1 = chunk[0] ?? 'A';
402
+ const char2 = chunk[1] ?? 'A';
403
+ const char3 = chunk[2] ?? 'A';
404
+ const char4 = chunk[3] ?? 'A';
405
+ const value1 = alphabet.indexOf(char1);
406
+ const value2 = alphabet.indexOf(char2);
407
+ const value3 = char3 === '=' ? 0 : alphabet.indexOf(char3);
408
+ const value4 = char4 === '=' ? 0 : alphabet.indexOf(char4);
409
+ const combined =
410
+ ((value1 >= 0 ? value1 : 0) << 18) |
411
+ ((value2 >= 0 ? value2 : 0) << 12) |
412
+ ((value3 >= 0 ? value3 : 0) << 6) |
413
+ (value4 >= 0 ? value4 : 0);
414
+
415
+ bytes.push((combined >> 16) & 255);
416
+
417
+ if (char3 !== '=') {
418
+ bytes.push((combined >> 8) & 255);
419
+ }
420
+
421
+ if (char4 !== '=') {
422
+ bytes.push(combined & 255);
423
+ }
424
+ }
425
+
426
+ return new Uint8Array(bytes);
427
+ }
428
+
429
+ private computePreviewDigest(bytes: Uint8Array): string {
430
+ let hash = 2166136261;
431
+
432
+ for (const byte of bytes) {
433
+ hash ^= byte;
434
+ hash = Math.imul(hash, 16777619);
435
+ }
436
+
437
+ return String(hash >>> 0).padStart(32, '0').slice(-32);
438
+ }
439
+
440
+ private normalizeSandboxPath(inputPath: string): string {
441
+ if (typeof inputPath !== 'string' || inputPath.length === 0) {
442
+ throw new Error('ExpoHarmonyFileSystem expected a non-empty sandbox path.');
443
+ }
444
+
445
+ if (!inputPath.startsWith('/')) {
446
+ throw new Error('ExpoHarmonyFileSystem accepts only absolute sandbox paths.');
447
+ }
448
+
449
+ if (inputPath.includes('/../') || inputPath.endsWith('/..') || inputPath.includes('/./')) {
450
+ throw new Error('ExpoHarmonyFileSystem does not accept relative path segments.');
451
+ }
452
+
453
+ const allowedRoots = [
454
+ this.ctx.uiAbilityContext.filesDir,
455
+ this.ctx.uiAbilityContext.cacheDir,
456
+ ].filter((value): value is string => typeof value === 'string' && value.length > 0);
457
+
458
+ const isAllowed = allowedRoots.some(
459
+ (rootPath) => inputPath === rootPath || inputPath.startsWith(\`\${rootPath}/\`),
460
+ );
461
+
462
+ if (!isAllowed) {
463
+ throw new Error('ExpoHarmonyFileSystem accepts only app sandbox paths.');
464
+ }
465
+
466
+ return inputPath;
467
+ }
468
+ }
469
+ `;
470
+ }
471
+ function renderExpoFileSystemHarmonyAdapterShim(capability) {
472
+ return `'use strict';
473
+
474
+ const { TurboModuleRegistry } = require('react-native');
475
+ const { CodedError } = require('expo-modules-core');
476
+
477
+ const FILE_SCHEME = 'file://';
478
+ const NATIVE_MODULE_NAME = 'ExpoHarmonyFileSystem';
479
+ const NATIVE_MODULE = TurboModuleRegistry.get(NATIVE_MODULE_NAME);
480
+ const NATIVE_CONSTANTS = NATIVE_MODULE?.getConstants ? NATIVE_MODULE.getConstants() : {};
481
+
482
+ function createError(code, message) {
483
+ return new CodedError(code, message);
484
+ }
485
+
486
+ function requireNativeModule(operationName) {
487
+ if (NATIVE_MODULE) {
488
+ return NATIVE_MODULE;
489
+ }
490
+
491
+ throw createError(
492
+ 'ERR_EXPO_HARMONY_NATIVE_MODULE_MISSING',
493
+ '${capability.packageName} expected the ' +
494
+ NATIVE_MODULE_NAME +
495
+ ' TurboModule to be registered, but it was missing while running ' +
496
+ operationName +
497
+ '.',
498
+ );
499
+ }
500
+
501
+ function createUnsupportedError(operationName) {
502
+ return createError(
503
+ 'ERR_EXPO_HARMONY_UNSUPPORTED',
504
+ '${capability.packageName} currently supports UTF-8 sandbox file operations only. Unsupported operation: ' +
505
+ operationName +
506
+ '.',
507
+ );
508
+ }
509
+
510
+ function toFileUri(pathValue, ensureTrailingSlash) {
511
+ if (typeof pathValue !== 'string' || pathValue.length === 0) {
512
+ return null;
513
+ }
514
+
515
+ const normalizedPath = pathValue.startsWith(FILE_SCHEME)
516
+ ? pathValue.slice(FILE_SCHEME.length)
517
+ : pathValue;
518
+ const withScheme = FILE_SCHEME + normalizedPath;
519
+
520
+ if (!ensureTrailingSlash) {
521
+ return withScheme;
522
+ }
523
+
524
+ let normalizedSchemePath = withScheme;
525
+
526
+ while (normalizedSchemePath.endsWith('/')) {
527
+ normalizedSchemePath = normalizedSchemePath.slice(0, -1);
528
+ }
529
+
530
+ return normalizedSchemePath + '/';
531
+ }
532
+
533
+ function normalizeInputPath(inputPath) {
534
+ if (typeof inputPath !== 'string' || inputPath.length === 0) {
535
+ throw createError(
536
+ 'ERR_EXPO_HARMONY_INVALID_URI',
537
+ '${capability.packageName} expected a non-empty file URI.',
538
+ );
539
+ }
540
+
541
+ const normalizedPath = inputPath.startsWith(FILE_SCHEME)
542
+ ? inputPath.slice(FILE_SCHEME.length)
543
+ : inputPath;
544
+
545
+ if (!normalizedPath.startsWith('/')) {
546
+ throw createError(
547
+ 'ERR_EXPO_HARMONY_INVALID_URI',
548
+ '${capability.packageName} supports only absolute file:// URIs inside the app sandbox.',
549
+ );
550
+ }
551
+
552
+ if (
553
+ normalizedPath.includes('/../') ||
554
+ normalizedPath.endsWith('/..') ||
555
+ normalizedPath.includes('/./')
556
+ ) {
557
+ throw createError(
558
+ 'ERR_EXPO_HARMONY_INVALID_URI',
559
+ '${capability.packageName} does not accept relative path segments.',
560
+ );
561
+ }
562
+
563
+ return normalizedPath;
564
+ }
565
+
566
+ function normalizeStringEncoding(rawEncoding) {
567
+ if (rawEncoding == null || rawEncoding === 'utf8') {
568
+ return 'utf8';
569
+ }
570
+
571
+ if (rawEncoding === 'base64') {
572
+ return 'base64';
573
+ }
574
+
575
+ throw createUnsupportedError('encoding=' + String(rawEncoding));
576
+ }
577
+
578
+ function normalizeFileDownloadResult(result, requestedUri) {
579
+ return {
580
+ uri: result?.uri ?? String(requestedUri),
581
+ status:
582
+ typeof result?.status === 'number' && Number.isFinite(result.status)
583
+ ? result.status
584
+ : 200,
585
+ headers: result?.headers && typeof result.headers === 'object' ? result.headers : {},
586
+ md5: typeof result?.md5 === 'string' ? result.md5 : undefined,
587
+ };
588
+ }
589
+
590
+ function normalizeFileInfoResult(requestedUri, nativeResult) {
591
+ if (!nativeResult || nativeResult.exists !== true) {
592
+ return {
593
+ exists: false,
594
+ isDirectory: false,
595
+ uri: String(requestedUri),
596
+ };
597
+ }
598
+
599
+ const normalizedResult = {
600
+ exists: true,
601
+ uri: toFileUri(nativeResult.path, false) ?? String(requestedUri),
602
+ size: Number(nativeResult.size ?? 0),
603
+ isDirectory: nativeResult.isDirectory === true,
604
+ modificationTime: Number(nativeResult.modificationTime ?? 0),
605
+ };
606
+
607
+ if (typeof nativeResult.md5 === 'string' && nativeResult.md5.length > 0) {
608
+ normalizedResult.md5 = nativeResult.md5;
609
+ }
610
+
611
+ return normalizedResult;
612
+ }
613
+
614
+ function normalizeNativeError(error) {
615
+ if (error instanceof Error) {
616
+ return error;
617
+ }
618
+
619
+ if (error && typeof error === 'object') {
620
+ const code =
621
+ typeof error.code === 'number' || typeof error.code === 'string'
622
+ ? String(error.code)
623
+ : null;
624
+ const message =
625
+ typeof error.message === 'string' && error.message.length > 0
626
+ ? error.message
627
+ : typeof error.name === 'string' && error.name.length > 0
628
+ ? error.name
629
+ : JSON.stringify(error);
630
+
631
+ return new Error(code ? '[native:' + code + '] ' + message : message);
632
+ }
633
+
634
+ return new Error(String(error));
635
+ }
636
+
637
+ module.exports = {
638
+ documentDirectory: toFileUri(NATIVE_CONSTANTS.documentDirectoryPath, true),
639
+ cacheDirectory: toFileUri(NATIVE_CONSTANTS.cacheDirectoryPath, true),
640
+ bundleDirectory: toFileUri(NATIVE_CONSTANTS.bundleDirectoryPath, true),
641
+ EncodingType: {
642
+ UTF8: 'utf8',
643
+ Base64: 'base64',
644
+ },
645
+ FileSystemSessionType: {
646
+ BACKGROUND: 0,
647
+ FOREGROUND: 1,
648
+ },
649
+ async getInfoAsync(fileUri, options) {
650
+ const normalizedPath = normalizeInputPath(fileUri);
651
+ try {
652
+ const result = await requireNativeModule('getInfoAsync').getInfo(normalizedPath, {
653
+ md5: options?.md5 === true,
654
+ });
655
+ return normalizeFileInfoResult(fileUri, result);
656
+ } catch (error) {
657
+ throw normalizeNativeError(error);
658
+ }
659
+ },
660
+ async readAsStringAsync(fileUri, options) {
661
+ const normalizedPath = normalizeInputPath(fileUri);
662
+ try {
663
+ return await requireNativeModule('readAsStringAsync').readAsString(normalizedPath, {
664
+ encoding: normalizeStringEncoding(options?.encoding),
665
+ position: options?.position,
666
+ length: options?.length,
667
+ });
668
+ } catch (error) {
669
+ throw normalizeNativeError(error);
670
+ }
671
+ },
672
+ async writeAsStringAsync(fileUri, contents, options) {
673
+ const normalizedPath = normalizeInputPath(fileUri);
674
+ try {
675
+ await requireNativeModule('writeAsStringAsync').writeAsString(
676
+ normalizedPath,
677
+ String(contents),
678
+ {
679
+ encoding: normalizeStringEncoding(options?.encoding),
680
+ append: options?.append === true,
681
+ },
682
+ );
683
+ } catch (error) {
684
+ throw normalizeNativeError(error);
685
+ }
686
+ },
687
+ async deleteAsync(fileUri, options) {
688
+ const normalizedPath = normalizeInputPath(fileUri);
689
+ try {
690
+ await requireNativeModule('deleteAsync').deletePath(normalizedPath, {
691
+ idempotent: options?.idempotent === true,
692
+ });
693
+ } catch (error) {
694
+ throw normalizeNativeError(error);
695
+ }
696
+ },
697
+ async makeDirectoryAsync(fileUri, options) {
698
+ const normalizedPath = normalizeInputPath(fileUri);
699
+ try {
700
+ await requireNativeModule('makeDirectoryAsync').makeDirectory(normalizedPath, {
701
+ intermediates: options?.intermediates === true,
702
+ });
703
+ } catch (error) {
704
+ throw normalizeNativeError(error);
705
+ }
706
+ },
707
+ async readDirectoryAsync(fileUri) {
708
+ const normalizedPath = normalizeInputPath(fileUri);
709
+ try {
710
+ return await requireNativeModule('readDirectoryAsync').readDirectory(normalizedPath);
711
+ } catch (error) {
712
+ throw normalizeNativeError(error);
713
+ }
714
+ },
715
+ async copyAsync(options) {
716
+ const fromPath = normalizeInputPath(options?.from);
717
+ const toPath = normalizeInputPath(options?.to);
718
+ try {
719
+ await requireNativeModule('copyAsync').copy(fromPath, toPath);
720
+ } catch (error) {
721
+ throw normalizeNativeError(error);
722
+ }
723
+ },
724
+ async moveAsync(options) {
725
+ const fromPath = normalizeInputPath(options?.from);
726
+ const toPath = normalizeInputPath(options?.to);
727
+ try {
728
+ await requireNativeModule('moveAsync').move(fromPath, toPath);
729
+ } catch (error) {
730
+ throw normalizeNativeError(error);
731
+ }
732
+ },
733
+ async downloadAsync(url, fileUri, options) {
734
+ const normalizedPath = normalizeInputPath(fileUri);
735
+ try {
736
+ return normalizeFileDownloadResult(
737
+ await requireNativeModule('downloadAsync').download(
738
+ String(url),
739
+ normalizedPath,
740
+ {
741
+ headers: options?.headers ?? {},
742
+ md5: options?.md5 === true,
743
+ },
744
+ ),
745
+ fileUri,
746
+ );
747
+ } catch (error) {
748
+ throw normalizeNativeError(error);
749
+ }
750
+ },
751
+ };
752
+ `;
753
+ }
754
+ function renderExpoFileSystemPreviewShim(capability) {
755
+ return `'use strict';
756
+
757
+ const { CodedError } = require('expo-modules-core');
758
+
759
+ const PREVIEW_MESSAGE =
760
+ '${capability.packageName} is routed through the Expo Harmony ${capability.supportTier} bridge. Bundling is supported, but runtime file-system behavior is not verified yet.';
761
+
762
+ function createPreviewError(operationName) {
763
+ return new CodedError(
764
+ 'ERR_EXPO_HARMONY_PREVIEW',
765
+ PREVIEW_MESSAGE + ' Attempted operation: ' + operationName + '.',
766
+ );
767
+ }
768
+
769
+ async function unavailable(operationName) {
770
+ throw createPreviewError(operationName);
771
+ }
772
+
773
+ module.exports = {
774
+ cacheDirectory: 'file:///expo-harmony/cache/',
775
+ documentDirectory: 'file:///expo-harmony/document/',
776
+ bundleDirectory: 'file:///expo-harmony/bundle/',
777
+ EncodingType: {
778
+ UTF8: 'utf8',
779
+ Base64: 'base64',
780
+ },
781
+ FileSystemSessionType: {
782
+ BACKGROUND: 0,
783
+ FOREGROUND: 1,
784
+ },
785
+ getInfoAsync(path) {
786
+ return unavailable('getInfoAsync(' + String(path) + ')');
787
+ },
788
+ readAsStringAsync(path) {
789
+ return unavailable('readAsStringAsync(' + String(path) + ')');
790
+ },
791
+ writeAsStringAsync(path) {
792
+ return unavailable('writeAsStringAsync(' + String(path) + ')');
793
+ },
794
+ deleteAsync(path) {
795
+ return unavailable('deleteAsync(' + String(path) + ')');
796
+ },
797
+ makeDirectoryAsync(path) {
798
+ return unavailable('makeDirectoryAsync(' + String(path) + ')');
799
+ },
800
+ readDirectoryAsync(path) {
801
+ return unavailable('readDirectoryAsync(' + String(path) + ')');
802
+ },
803
+ copyAsync(options) {
804
+ return unavailable('copyAsync(' + JSON.stringify(options ?? {}) + ')');
805
+ },
806
+ moveAsync(options) {
807
+ return unavailable('moveAsync(' + JSON.stringify(options ?? {}) + ')');
808
+ },
809
+ downloadAsync(url) {
810
+ return unavailable('downloadAsync(' + String(url) + ')');
811
+ },
812
+ };
813
+ `;
814
+ }