@multiplatform.one/platform 7.87.1 → 7.88.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/platform",
3
- "version": "7.87.1",
3
+ "version": "7.88.1",
4
4
  "description": "Platform detection flags and utilities for multiplatform.one",
5
5
  "keywords": [
6
6
  "detection",
@@ -56,7 +56,7 @@
56
56
  "@tamagui/constants": "2.7.6"
57
57
  },
58
58
  "devDependencies": {
59
- "@multiplatform.one/config": "7.87.1",
59
+ "@multiplatform.one/config": "7.88.1",
60
60
  "@tamagui/build": "2.7.6",
61
61
  "@types/chrome": "^0.3.5",
62
62
  "@types/firefox-webext-browser": "^143.0.1",
@@ -96,6 +96,50 @@ describe("native byte save (native boundary modeled)", () => {
96
96
  expect(file.delete).not.toHaveBeenCalled();
97
97
  });
98
98
 
99
+ it("names the iOS file from its URI when expo's File.name comes back empty (FZ-9198)", async () => {
100
+ native.platform.isIos = true;
101
+ native.platform.isAndroid = false;
102
+ directory.uri = "file:///Shared/File%20Provider%20Storage/";
103
+ file.uri = "file:///Shared/File%20Provider%20Storage/Pokemon_report.csv";
104
+ file.name = "";
105
+ const { saveFile } = await api();
106
+ await expect(saveFile({ ...options(), filename: "Pokemon_report.csv" })).resolves.toEqual({
107
+ status: "written",
108
+ uri: file.uri,
109
+ requestedFilename: "Pokemon_report.csv",
110
+ filename: "Pokemon_report.csv",
111
+ byteLength: 4,
112
+ });
113
+ });
114
+
115
+ it("keeps both on iOS: a taken name gets the next free copy number instead of failing (FZ-9198)", async () => {
116
+ native.platform.isIos = true;
117
+ native.platform.isAndroid = false;
118
+ directory.uri = "file:///picked/";
119
+ const taken = ["file:///picked/Pokemon_report.csv", "file:///picked/Pokemon_report%202.csv"];
120
+ Object.assign(directory, { list: vi.fn(() => taken.map((uri) => ({ uri }))) });
121
+ directory.createFile.mockImplementation((name: string) => {
122
+ if (taken.includes(`file:///picked/${encodeURIComponent(name)}`)) {
123
+ throw new Error(`File '${name}' already exists`);
124
+ }
125
+ file.uri = `file:///picked/${encodeURIComponent(name)}`;
126
+ file.name = "";
127
+ return file;
128
+ });
129
+ const { saveFile } = await api();
130
+ await expect(saveFile({ ...options(), filename: "Pokemon_report.csv" })).resolves.toEqual({
131
+ status: "written",
132
+ uri: "file:///picked/Pokemon_report%203.csv",
133
+ requestedFilename: "Pokemon_report.csv",
134
+ filename: "Pokemon_report 3.csv",
135
+ byteLength: 4,
136
+ });
137
+ expect(directory.createFile).toHaveBeenCalledExactlyOnceWith(
138
+ "Pokemon_report 3.csv",
139
+ "application/octet-stream",
140
+ );
141
+ });
142
+
99
143
  it("omits an opaque Android document ID without reading it as a filename", async () => {
100
144
  file.uri = "content://example.documents/tree/root/document/42";
101
145
  const readName = vi.fn(() => "42");
@@ -53,10 +53,14 @@ export async function saveFile(options: FileSaveOptions): Promise<FileSaveResult
53
53
 
54
54
  let file: File | undefined;
55
55
  try {
56
- file = directory.createFile(input.filename, input.nativeMimeType);
56
+ const name = platform.isIos
57
+ ? freeName(input.filename, takenNames(directory))
58
+ : input.filename;
59
+ file = directory.createFile(name, input.nativeMimeType);
57
60
  const uri = file.uri;
58
61
  // Android content URIs can contain opaque IDs instead of provider display names.
59
- const filename = platform.isIos && uri.startsWith("file://") ? { filename: file.name } : {};
62
+ const filename =
63
+ platform.isIos && uri.startsWith("file://") ? { filename: lastSegment(uri) || name } : {};
60
64
  file.write(input.bytes);
61
65
  return {
62
66
  status: "written",
@@ -81,3 +85,32 @@ export async function saveFile(options: FileSaveOptions): Promise<FileSaveResult
81
85
  busy = false;
82
86
  }
83
87
  }
88
+
89
+ function lastSegment(uri: string): string {
90
+ const segment = uri.replace(/\/+$/, "").split("/").pop() ?? "";
91
+ try {
92
+ return decodeURIComponent(segment);
93
+ } catch {
94
+ return segment;
95
+ }
96
+ }
97
+
98
+ function takenNames(directory: Directory): Set<string> {
99
+ try {
100
+ return new Set(directory.list().map((entry) => lastSegment(entry.uri)));
101
+ } catch {
102
+ return new Set();
103
+ }
104
+ }
105
+
106
+ function freeName(requested: string, taken: Set<string>): string {
107
+ if (!taken.has(requested)) return requested;
108
+ const dot = requested.lastIndexOf(".");
109
+ const stem = dot > 0 ? requested.slice(0, dot) : requested;
110
+ const extension = dot > 0 ? requested.slice(dot) : "";
111
+ for (let copy = 2; copy < 1000; copy++) {
112
+ const candidate = `${stem} ${copy}${extension}`;
113
+ if (!taken.has(candidate)) return candidate;
114
+ }
115
+ return requested;
116
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"saveFile.native.d.ts","sourceRoot":"","sources":["../../src/file/saveFile.native.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAmB,MAAM,SAAS,CAAC;AAIpG,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA4EhF"}
1
+ {"version":3,"file":"saveFile.native.d.ts","sourceRoot":"","sources":["../../src/file/saveFile.native.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAmB,MAAM,SAAS,CAAC;AAIpG,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAgFhF"}