@multiplatform.one/platform 7.10.0 → 7.15.0

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 (57) hide show
  1. package/README.md +67 -0
  2. package/dist/cjs/config/devOrigin.cjs +42 -0
  3. package/dist/cjs/config/devOriginSource.cjs +32 -0
  4. package/dist/cjs/config/index.cjs +1 -0
  5. package/dist/cjs/file/index.cjs +30 -0
  6. package/dist/cjs/file/saveFile.cjs +47 -0
  7. package/dist/cjs/file/types.cjs +56 -0
  8. package/dist/cjs/utils/downloadFile.cjs +10 -4
  9. package/dist/esm/config/devOrigin.mjs +16 -0
  10. package/dist/esm/config/devOrigin.mjs.map +1 -0
  11. package/dist/esm/config/devOriginSource.mjs +7 -0
  12. package/dist/esm/config/devOriginSource.mjs.map +1 -0
  13. package/dist/esm/config/index.mjs +1 -0
  14. package/dist/esm/config/index.mjs.map +1 -1
  15. package/dist/esm/file/index.mjs +4 -0
  16. package/dist/esm/file/index.mjs.map +1 -0
  17. package/dist/esm/file/saveFile.mjs +22 -0
  18. package/dist/esm/file/saveFile.mjs.map +1 -0
  19. package/dist/esm/file/types.mjs +30 -0
  20. package/dist/esm/file/types.mjs.map +1 -0
  21. package/dist/esm/utils/downloadFile.mjs +10 -4
  22. package/dist/esm/utils/downloadFile.mjs.map +1 -1
  23. package/file.ts +1 -0
  24. package/package.json +18 -4
  25. package/src/config/config.native.ts +2 -1
  26. package/src/config/devOrigin.spec.ts +79 -0
  27. package/src/config/devOrigin.ts +45 -0
  28. package/src/config/devOriginSource.native.ts +9 -0
  29. package/src/config/devOriginSource.ts +5 -0
  30. package/src/config/index.ts +1 -0
  31. package/src/file/index.ts +3 -0
  32. package/src/file/saveFile.native.spec.ts +245 -0
  33. package/src/file/saveFile.native.ts +83 -0
  34. package/src/file/saveFile.spec.ts +98 -0
  35. package/src/file/saveFile.ts +25 -0
  36. package/src/file/saveFile.unavailable.native.spec.ts +24 -0
  37. package/src/file/types.ts +74 -0
  38. package/src/utils/downloadFile.spec.ts +54 -0
  39. package/src/utils/downloadFile.ts +11 -4
  40. package/types/config/config.native.d.ts.map +1 -1
  41. package/types/config/devOrigin.d.ts +28 -0
  42. package/types/config/devOrigin.d.ts.map +1 -0
  43. package/types/config/devOriginSource.d.ts +3 -0
  44. package/types/config/devOriginSource.d.ts.map +1 -0
  45. package/types/config/devOriginSource.native.d.ts +3 -0
  46. package/types/config/devOriginSource.native.d.ts.map +1 -0
  47. package/types/config/index.d.ts +1 -0
  48. package/types/config/index.d.ts.map +1 -1
  49. package/types/file/index.d.ts +4 -0
  50. package/types/file/index.d.ts.map +1 -0
  51. package/types/file/saveFile.d.ts +3 -0
  52. package/types/file/saveFile.d.ts.map +1 -0
  53. package/types/file/saveFile.native.d.ts +3 -0
  54. package/types/file/saveFile.native.d.ts.map +1 -0
  55. package/types/file/types.d.ts +34 -0
  56. package/types/file/types.d.ts.map +1 -0
  57. package/types/utils/downloadFile.d.ts.map +1 -1
package/README.md CHANGED
@@ -52,6 +52,73 @@ export function saveFile(contents: string) {
52
52
  Flags are evaluated once at module load and are safe to use in render logic
53
53
  and module scope.
54
54
 
55
+ ## Save bytes to a file
56
+
57
+ Import the opt-in subpath from a user action:
58
+
59
+ ```ts
60
+ import { FileSaveError, saveFile } from "@multiplatform.one/platform/file";
61
+
62
+ try {
63
+ const result = await saveFile({
64
+ bytes: new TextEncoder().encode("café\n"),
65
+ filename: "example.txt",
66
+ mimeType: "text/plain; charset=utf-8",
67
+ });
68
+ // Render the returned status, not a blanket success message.
69
+ } catch (error) {
70
+ if (error instanceof FileSaveError) {
71
+ console.error(error.code, error.cause, error.cleanupError);
72
+ }
73
+ }
74
+ ```
75
+
76
+ | Result | What it establishes |
77
+ | --- | --- |
78
+ | `written` | Native byte writing completed at `uri`, with `byteLength` and `requestedFilename`. The actual `filename` is optional. Cloud synchronization is not confirmed. |
79
+ | `download-requested` | A browser download was activated. Browser completion or cancellation is unknown. |
80
+ | `not-saved`, reason `cancelled-or-refused` | The native picker reported cancellation or refused directory access. No file was created. iOS reports these with the same code. |
81
+
82
+ `WrittenFile.requestedFilename` always records the supplied filename. The optional
83
+ `filename` reports the actual name only for an iOS file URI. Android content URIs
84
+ omit it: Expo 55.0.26 derives `File.name` from the URI, which can contain an opaque
85
+ document ID instead of a provider display name. Consumers must distinguish the
86
+ requested name from an actual name and use `uri` for the saved destination.
87
+
88
+ Errors reject with `FileSaveError`: `invalid-input`, `unavailable`, `busy`,
89
+ `picker-failed` or `write-failed`. The original failure is in `cause`.
90
+ `cleanupError` records a second failure removing a partially written new file.
91
+ Only `ERR_PICKER_CANCELLED` and `ERR_FILE_PICKING_CANCELLED` are classified as
92
+ `not-saved`; other picker errors remain observable.
93
+
94
+ Native iOS/Android requires `expo-file-system` (`~55.0.26`, tested against
95
+ 55.0.26) in the app and its native binary. Install it explicitly in the native
96
+ host. JavaScript or an OTA update cannot add a missing native module. The module
97
+ loads only when `saveFile` is invoked, and isn't exported through the platform
98
+ root. Other hosts require browser download support or return `unavailable`.
99
+
100
+ Native opens the system directory picker and creates a new child through the
101
+ selected provider. It doesn't request overwrite. A provider can refuse an
102
+ existing name or choose a suffix. `requestedFilename` doesn't confirm which name
103
+ the provider chose; `filename` is omitted when that metadata isn't established.
104
+ Successful files belong to the user and are **never automatically deleted**.
105
+ On write failure, cleanup attempts to delete only the newly created child.
106
+ No shared cache file or share sheet is involved.
107
+
108
+ Pass a `Uint8Array`, a nonempty single filename without separators, dot/dot-dot
109
+ or control characters, and a MIME type. Unicode filenames and empty files are
110
+ valid. Bytes are copied before awaiting the directory picker, including the
111
+ exact range of a sliced view. Native creation uses the MIME type without
112
+ parameters; the browser Blob retains the supplied type.
113
+
114
+ The existing root `downloadFile` and `downloadBlob` remain synchronous `void`
115
+ helpers. Blob URLs are released after 60 seconds; this delay manages resources
116
+ and does not indicate that a download completed.
117
+
118
+ `Platform/FileSave` is a generic Storybook specimen for parent browser/native
119
+ checks. Unit tests model native boundaries. Packed ESM/CJS and Metro checks
120
+ are package preflight, not real-device save evidence or registry publication.
121
+
55
122
  ## License
56
123
 
57
124
  Apache-2.0
@@ -0,0 +1,42 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
21
+ value: true
22
+ }), mod);
23
+ var devOrigin_exports = {};
24
+ __export(devOrigin_exports, {
25
+ devHostFromHostUri: () => devHostFromHostUri,
26
+ resolveDevOrigin: () => resolveDevOrigin
27
+ });
28
+ module.exports = __toCommonJS(devOrigin_exports);
29
+ var import_devOriginSource = require("./devOriginSource.cjs");
30
+ const loopbackOrigin = /^([a-z][a-z0-9+.-]*:\/\/)(localhost|127\.0\.0\.1)(?=[:/?#]|$)/i;
31
+ function devHostFromHostUri(hostUri) {
32
+ if (!hostUri) return void 0;
33
+ const authority = hostUri.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").split(/[/?#]/, 1)[0];
34
+ const host = authority.startsWith("[") ? authority.slice(0, authority.indexOf("]") + 1) : authority.split(":", 1)[0];
35
+ return host || void 0;
36
+ }
37
+ function resolveDevOrigin(url, source = (0, import_devOriginSource.devOriginSource)()) {
38
+ if (source.isWeb) return url;
39
+ const host = devHostFromHostUri(source.hostUri);
40
+ if (!host) return url;
41
+ return url.replace(loopbackOrigin, `$1${host}`);
42
+ }
@@ -0,0 +1,32 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
21
+ value: true
22
+ }), mod);
23
+ var devOriginSource_exports = {};
24
+ __export(devOriginSource_exports, {
25
+ devOriginSource: () => devOriginSource
26
+ });
27
+ module.exports = __toCommonJS(devOriginSource_exports);
28
+ function devOriginSource() {
29
+ return {
30
+ isWeb: true
31
+ };
32
+ }
@@ -28,5 +28,6 @@ __export(config_exports, {
28
28
  module.exports = __toCommonJS(config_exports);
29
29
  var import_config = require("./config.cjs");
30
30
  __reExport(config_exports, require("./config.cjs"), module.exports);
31
+ __reExport(config_exports, require("./devOrigin.cjs"), module.exports);
31
32
  __reExport(config_exports, require("./runtimeConfig.cjs"), module.exports);
32
33
  const config = new import_config.Config();
@@ -0,0 +1,30 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
21
+ value: true
22
+ }), mod);
23
+ var file_exports = {};
24
+ __export(file_exports, {
25
+ FileSaveError: () => import_types.FileSaveError,
26
+ saveFile: () => import_saveFile.saveFile
27
+ });
28
+ module.exports = __toCommonJS(file_exports);
29
+ var import_saveFile = require("./saveFile.cjs");
30
+ var import_types = require("./types.cjs");
@@ -0,0 +1,47 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
21
+ value: true
22
+ }), mod);
23
+ var saveFile_exports = {};
24
+ __export(saveFile_exports, {
25
+ saveFile: () => saveFile
26
+ });
27
+ module.exports = __toCommonJS(saveFile_exports);
28
+ var import_downloadFile = require("../utils/downloadFile.cjs");
29
+ var import_types = require("./types.cjs");
30
+ async function saveFile(options) {
31
+ const input = (0, import_types.prepareFileSave)(options);
32
+ if (typeof document === "undefined" || !document.body || typeof Blob === "undefined" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function" || typeof URL.revokeObjectURL !== "function" || !("download" in document.createElement("a"))) {
33
+ throw new import_types.FileSaveError("unavailable", "Browser file downloads are unavailable.");
34
+ }
35
+ try {
36
+ (0, import_downloadFile.downloadBlob)(new Blob([input.bytes], {
37
+ type: input.mimeType
38
+ }), input.filename);
39
+ } catch (cause) {
40
+ throw new import_types.FileSaveError("write-failed", "The browser download could not be requested.", {
41
+ cause
42
+ });
43
+ }
44
+ return {
45
+ status: "download-requested"
46
+ };
47
+ }
@@ -0,0 +1,56 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: () => from[key],
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
21
+ value: true
22
+ }), mod);
23
+ var types_exports = {};
24
+ __export(types_exports, {
25
+ FileSaveError: () => FileSaveError,
26
+ prepareFileSave: () => prepareFileSave
27
+ });
28
+ module.exports = __toCommonJS(types_exports);
29
+ class FileSaveError extends Error {
30
+ code;
31
+ cleanupError;
32
+ constructor(code, message, options) {
33
+ super(message, options);
34
+ this.name = "FileSaveError";
35
+ this.code = code;
36
+ this.cleanupError = options?.cleanupError;
37
+ }
38
+ }
39
+ function hasControlCharacters(value) {
40
+ return Array.from(value).some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
41
+ }
42
+ function prepareFileSave(options) {
43
+ if (!options || !(options.bytes instanceof Uint8Array) || typeof options.filename !== "string" || !options.filename.trim() || options.filename === "." || options.filename === ".." || /[/\\]/.test(options.filename) || hasControlCharacters(options.filename) || typeof options.mimeType !== "string" || hasControlCharacters(options.mimeType)) {
44
+ throw new FileSaveError("invalid-input", "Provide bytes, a single filename and a MIME type.");
45
+ }
46
+ const nativeMimeType = options.mimeType.split(";", 1)[0].trim();
47
+ if (!/^[\w!#$%&'*+.^`|~-]+\/[\w!#$%&'*+.^`|~-]+$/.test(nativeMimeType)) {
48
+ throw new FileSaveError("invalid-input", "Provide a MIME type with a type and subtype.");
49
+ }
50
+ return {
51
+ bytes: Uint8Array.from(options.bytes),
52
+ filename: options.filename,
53
+ mimeType: options.mimeType,
54
+ nativeMimeType
55
+ };
56
+ }
@@ -31,11 +31,17 @@ function downloadFile(url, filename) {
31
31
  a.href = url;
32
32
  a.download = filename;
33
33
  document.body.appendChild(a);
34
- a.click();
35
- document.body.removeChild(a);
34
+ try {
35
+ a.click();
36
+ } finally {
37
+ a.remove();
38
+ }
36
39
  }
37
40
  function downloadBlob(blob, filename) {
38
41
  const url = URL.createObjectURL(blob);
39
- downloadFile(url, filename);
40
- URL.revokeObjectURL(url);
42
+ try {
43
+ downloadFile(url, filename);
44
+ } finally {
45
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
46
+ }
41
47
  }
@@ -0,0 +1,16 @@
1
+ import { devOriginSource } from "./devOriginSource.mjs";
2
+ const loopbackOrigin = /^([a-z][a-z0-9+.-]*:\/\/)(localhost|127\.0\.0\.1)(?=[:/?#]|$)/i;
3
+ function devHostFromHostUri(hostUri) {
4
+ if (!hostUri) return void 0;
5
+ const authority = hostUri.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").split(/[/?#]/, 1)[0];
6
+ const host = authority.startsWith("[") ? authority.slice(0, authority.indexOf("]") + 1) : authority.split(":", 1)[0];
7
+ return host || void 0;
8
+ }
9
+ function resolveDevOrigin(url, source = devOriginSource()) {
10
+ if (source.isWeb) return url;
11
+ const host = devHostFromHostUri(source.hostUri);
12
+ if (!host) return url;
13
+ return url.replace(loopbackOrigin, `$1${host}`);
14
+ }
15
+ export { devHostFromHostUri, resolveDevOrigin };
16
+ //# sourceMappingURL=devOrigin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["devOriginSource","loopbackOrigin","devHostFromHostUri","hostUri","authority","replace","split","host","startsWith","slice","indexOf","resolveDevOrigin","url","source","isWeb"],"sources":["../../../src/config/devOrigin.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,eAAA,QAAuB;AAgBhC,MAAMC,cAAA,GAAiB;AAGhB,SAASC,mBAAmBC,OAAA,EAAiD;EAClF,IAAI,CAACA,OAAA,EAAS,OAAO;EACrB,MAAMC,SAAA,GAAYD,OAAA,CAAQE,OAAA,CAAQ,4BAA4B,EAAE,EAAEC,KAAA,CAAM,SAAS,CAAC,EAAE,CAAC;EACrF,MAAMC,IAAA,GAAOH,SAAA,CAAUI,UAAA,CAAW,GAAG,IACjCJ,SAAA,CAAUK,KAAA,CAAM,GAAGL,SAAA,CAAUM,OAAA,CAAQ,GAAG,IAAI,CAAC,IAC7CN,SAAA,CAAUE,KAAA,CAAM,KAAK,CAAC,EAAE,CAAC;EAC7B,OAAOC,IAAA,IAAQ;AACjB;AAaO,SAASI,iBAAiBC,GAAA,EAAaC,MAAA,GAA0Bb,eAAA,CAAgB,GAAW;EACjG,IAAIa,MAAA,CAAOC,KAAA,EAAO,OAAOF,GAAA;EACzB,MAAML,IAAA,GAAOL,kBAAA,CAAmBW,MAAA,CAAOV,OAAO;EAC9C,IAAI,CAACI,IAAA,EAAM,OAAOK,GAAA;EAClB,OAAOA,GAAA,CAAIP,OAAA,CAAQJ,cAAA,EAAgB,KAAKM,IAAI,EAAE;AAChD","ignoreList":[]}
@@ -0,0 +1,7 @@
1
+ function devOriginSource() {
2
+ return {
3
+ isWeb: true
4
+ };
5
+ }
6
+ export { devOriginSource };
7
+ //# sourceMappingURL=devOriginSource.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["devOriginSource","isWeb"],"sources":["../../../src/config/devOriginSource.ts"],"sourcesContent":[null],"mappings":"AAEO,SAASA,gBAAA,EAAmC;EACjD,OAAO;IAAEC,KAAA,EAAO;EAAK;AACvB","ignoreList":[]}
@@ -1,5 +1,6 @@
1
1
  import { Config } from "./config.mjs";
2
2
  export * from "./config.mjs";
3
+ export * from "./devOrigin.mjs";
3
4
  export * from "./runtimeConfig.mjs";
4
5
  const config = new Config();
5
6
  export { config };
@@ -1 +1 @@
1
- {"version":3,"names":["Config","config"],"sources":["../../../src/config/index.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,MAAA,QAAc;AAKvB,cAAc;AACd,cAAc;AAJd,MAAMC,MAAA,GAAS,IAAID,MAAA,CAAO","ignoreList":[]}
1
+ {"version":3,"names":["Config","config"],"sources":["../../../src/config/index.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,MAAA,QAAc;AAKvB,cAAc;AACd,cAAc;AACd,cAAc;AALd,MAAMC,MAAA,GAAS,IAAID,MAAA,CAAO","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ import { saveFile } from "./saveFile.mjs";
2
+ import { FileSaveError } from "./types.mjs";
3
+ export { FileSaveError, saveFile };
4
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["saveFile","FileSaveError"],"sources":["../../../src/file/index.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,QAAA,QAAgB;AACzB,SAASC,aAAA,QAAqB","ignoreList":[]}
@@ -0,0 +1,22 @@
1
+ import { downloadBlob } from "../utils/downloadFile.mjs";
2
+ import { FileSaveError, prepareFileSave } from "./types.mjs";
3
+ async function saveFile(options) {
4
+ const input = prepareFileSave(options);
5
+ if (typeof document === "undefined" || !document.body || typeof Blob === "undefined" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function" || typeof URL.revokeObjectURL !== "function" || !("download" in document.createElement("a"))) {
6
+ throw new FileSaveError("unavailable", "Browser file downloads are unavailable.");
7
+ }
8
+ try {
9
+ downloadBlob(new Blob([input.bytes], {
10
+ type: input.mimeType
11
+ }), input.filename);
12
+ } catch (cause) {
13
+ throw new FileSaveError("write-failed", "The browser download could not be requested.", {
14
+ cause
15
+ });
16
+ }
17
+ return {
18
+ status: "download-requested"
19
+ };
20
+ }
21
+ export { saveFile };
22
+ //# sourceMappingURL=saveFile.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["downloadBlob","FileSaveError","prepareFileSave","saveFile","options","input","document","body","Blob","URL","createObjectURL","revokeObjectURL","createElement","bytes","type","mimeType","filename","cause","status"],"sources":["../../../src/file/saveFile.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,YAAA,QAAoB;AAC7B,SAASC,aAAA,EAA0DC,eAAA,QAAuB;AAE1F,eAAsBC,SAASC,OAAA,EAAmD;EAChF,MAAMC,KAAA,GAAQH,eAAA,CAAgBE,OAAO;EACrC,IACE,OAAOE,QAAA,KAAa,eACpB,CAACA,QAAA,CAASC,IAAA,IACV,OAAOC,IAAA,KAAS,eAChB,OAAOC,GAAA,KAAQ,eACf,OAAOA,GAAA,CAAIC,eAAA,KAAoB,cAC/B,OAAOD,GAAA,CAAIE,eAAA,KAAoB,cAC/B,EAAE,cAAcL,QAAA,CAASM,aAAA,CAAc,GAAG,IAC1C;IACA,MAAM,IAAIX,aAAA,CAAc,eAAe,yCAAyC;EAClF;EACA,IAAI;IACFD,YAAA,CAAa,IAAIQ,IAAA,CAAK,CAACH,KAAA,CAAMQ,KAAK,GAAG;MAAEC,IAAA,EAAMT,KAAA,CAAMU;IAAS,CAAC,GAAGV,KAAA,CAAMW,QAAQ;EAChF,SAASC,KAAA,EAAO;IACd,MAAM,IAAIhB,aAAA,CAAc,gBAAgB,gDAAgD;MACtFgB;IACF,CAAC;EACH;EACA,OAAO;IAAEC,MAAA,EAAQ;EAAqB;AACxC","ignoreList":[]}
@@ -0,0 +1,30 @@
1
+ class FileSaveError extends Error {
2
+ code;
3
+ cleanupError;
4
+ constructor(code, message, options) {
5
+ super(message, options);
6
+ this.name = "FileSaveError";
7
+ this.code = code;
8
+ this.cleanupError = options?.cleanupError;
9
+ }
10
+ }
11
+ function hasControlCharacters(value) {
12
+ return Array.from(value).some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127);
13
+ }
14
+ function prepareFileSave(options) {
15
+ if (!options || !(options.bytes instanceof Uint8Array) || typeof options.filename !== "string" || !options.filename.trim() || options.filename === "." || options.filename === ".." || /[/\\]/.test(options.filename) || hasControlCharacters(options.filename) || typeof options.mimeType !== "string" || hasControlCharacters(options.mimeType)) {
16
+ throw new FileSaveError("invalid-input", "Provide bytes, a single filename and a MIME type.");
17
+ }
18
+ const nativeMimeType = options.mimeType.split(";", 1)[0].trim();
19
+ if (!/^[\w!#$%&'*+.^`|~-]+\/[\w!#$%&'*+.^`|~-]+$/.test(nativeMimeType)) {
20
+ throw new FileSaveError("invalid-input", "Provide a MIME type with a type and subtype.");
21
+ }
22
+ return {
23
+ bytes: Uint8Array.from(options.bytes),
24
+ filename: options.filename,
25
+ mimeType: options.mimeType,
26
+ nativeMimeType
27
+ };
28
+ }
29
+ export { FileSaveError, prepareFileSave };
30
+ //# sourceMappingURL=types.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["FileSaveError","Error","code","cleanupError","constructor","message","options","name","hasControlCharacters","value","Array","from","some","character","charCodeAt","prepareFileSave","bytes","Uint8Array","filename","trim","test","mimeType","nativeMimeType","split"],"sources":["../../../src/file/types.ts"],"sourcesContent":[null],"mappings":"AA0BO,MAAMA,aAAA,SAAsBC,KAAA,CAAM;EAC9BC,IAAA;EACAC,YAAA;EAETC,YACEF,IAAA,EACAG,OAAA,EACAC,OAAA,EACA;IACA,MAAMD,OAAA,EAASC,OAAO;IACtB,KAAKC,IAAA,GAAO;IACZ,KAAKL,IAAA,GAAOA,IAAA;IACZ,KAAKC,YAAA,GAAeG,OAAA,EAASH,YAAA;EAC/B;AACF;AAEA,SAASK,qBAAqBC,KAAA,EAAe;EAC3C,OAAOC,KAAA,CAAMC,IAAA,CAAKF,KAAK,EAAEG,IAAA,CACtBC,SAAA,IAAcA,SAAA,CAAUC,UAAA,CAAW,CAAC,IAAI,MAAMD,SAAA,CAAUC,UAAA,CAAW,CAAC,MAAM,GAC7E;AACF;AAEO,SAASC,gBAAgBT,OAAA,EAA0B;EACxD,IACE,CAACA,OAAA,IACD,EAAEA,OAAA,CAAQU,KAAA,YAAiBC,UAAA,KAC3B,OAAOX,OAAA,CAAQY,QAAA,KAAa,YAC5B,CAACZ,OAAA,CAAQY,QAAA,CAASC,IAAA,CAAK,KACvBb,OAAA,CAAQY,QAAA,KAAa,OACrBZ,OAAA,CAAQY,QAAA,KAAa,QACrB,QAAQE,IAAA,CAAKd,OAAA,CAAQY,QAAQ,KAC7BV,oBAAA,CAAqBF,OAAA,CAAQY,QAAQ,KACrC,OAAOZ,OAAA,CAAQe,QAAA,KAAa,YAC5Bb,oBAAA,CAAqBF,OAAA,CAAQe,QAAQ,GACrC;IACA,MAAM,IAAIrB,aAAA,CAAc,iBAAiB,mDAAmD;EAC9F;EACA,MAAMsB,cAAA,GAAiBhB,OAAA,CAAQe,QAAA,CAASE,KAAA,CAAM,KAAK,CAAC,EAAE,CAAC,EAAEJ,IAAA,CAAK;EAC9D,IAAI,CAAC,6CAA6CC,IAAA,CAAKE,cAAc,GAAG;IACtE,MAAM,IAAItB,aAAA,CAAc,iBAAiB,8CAA8C;EACzF;EACA,OAAO;IACLgB,KAAA,EAAOC,UAAA,CAAWN,IAAA,CAAKL,OAAA,CAAQU,KAAK;IACpCE,QAAA,EAAUZ,OAAA,CAAQY,QAAA;IAClBG,QAAA,EAAUf,OAAA,CAAQe,QAAA;IAClBC;EACF;AACF","ignoreList":[]}
@@ -3,13 +3,19 @@ function downloadFile(url, filename) {
3
3
  a.href = url;
4
4
  a.download = filename;
5
5
  document.body.appendChild(a);
6
- a.click();
7
- document.body.removeChild(a);
6
+ try {
7
+ a.click();
8
+ } finally {
9
+ a.remove();
10
+ }
8
11
  }
9
12
  function downloadBlob(blob, filename) {
10
13
  const url = URL.createObjectURL(blob);
11
- downloadFile(url, filename);
12
- URL.revokeObjectURL(url);
14
+ try {
15
+ downloadFile(url, filename);
16
+ } finally {
17
+ setTimeout(() => URL.revokeObjectURL(url), 6e4);
18
+ }
13
19
  }
14
20
  export { downloadBlob, downloadFile };
15
21
  //# sourceMappingURL=downloadFile.mjs.map
@@ -1 +1 @@
1
- {"version":3,"names":["downloadFile","url","filename","a","document","createElement","href","download","body","appendChild","click","removeChild","downloadBlob","blob","URL","createObjectURL","revokeObjectURL"],"sources":["../../../src/utils/downloadFile.ts"],"sourcesContent":[null],"mappings":"AAAO,SAASA,aAAaC,GAAA,EAAaC,QAAA,EAAwB;EAChE,MAAMC,CAAA,GAAIC,QAAA,CAASC,aAAA,CAAc,GAAG;EACpCF,CAAA,CAAEG,IAAA,GAAOL,GAAA;EACTE,CAAA,CAAEI,QAAA,GAAWL,QAAA;EACbE,QAAA,CAASI,IAAA,CAAKC,WAAA,CAAYN,CAAC;EAC3BA,CAAA,CAAEO,KAAA,CAAM;EACRN,QAAA,CAASI,IAAA,CAAKG,WAAA,CAAYR,CAAC;AAC7B;AAEO,SAASS,aAAaC,IAAA,EAAYX,QAAA,EAAwB;EAC/D,MAAMD,GAAA,GAAMa,GAAA,CAAIC,eAAA,CAAgBF,IAAI;EACpCb,YAAA,CAAaC,GAAA,EAAKC,QAAQ;EAC1BY,GAAA,CAAIE,eAAA,CAAgBf,GAAG;AACzB","ignoreList":[]}
1
+ {"version":3,"names":["downloadFile","url","filename","a","document","createElement","href","download","body","appendChild","click","remove","downloadBlob","blob","URL","createObjectURL","setTimeout","revokeObjectURL"],"sources":["../../../src/utils/downloadFile.ts"],"sourcesContent":[null],"mappings":"AAAO,SAASA,aAAaC,GAAA,EAAaC,QAAA,EAAwB;EAChE,MAAMC,CAAA,GAAIC,QAAA,CAASC,aAAA,CAAc,GAAG;EACpCF,CAAA,CAAEG,IAAA,GAAOL,GAAA;EACTE,CAAA,CAAEI,QAAA,GAAWL,QAAA;EACbE,QAAA,CAASI,IAAA,CAAKC,WAAA,CAAYN,CAAC;EAC3B,IAAI;IACFA,CAAA,CAAEO,KAAA,CAAM;EACV,UAAE;IACAP,CAAA,CAAEQ,MAAA,CAAO;EACX;AACF;AAEO,SAASC,aAAaC,IAAA,EAAYX,QAAA,EAAwB;EAC/D,MAAMD,GAAA,GAAMa,GAAA,CAAIC,eAAA,CAAgBF,IAAI;EACpC,IAAI;IACFb,YAAA,CAAaC,GAAA,EAAKC,QAAQ;EAC5B,UAAE;IAEAc,UAAA,CAAW,MAAMF,GAAA,CAAIG,eAAA,CAAgBhB,GAAG,GAAG,GAAM;EACnD;AACF","ignoreList":[]}
package/file.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/file/index";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/platform",
3
- "version": "7.10.0",
3
+ "version": "7.15.0",
4
4
  "description": "Platform detection flags and utilities for multiplatform.one",
5
5
  "keywords": [
6
6
  "detection",
@@ -12,11 +12,12 @@
12
12
  "author": "BitSpur",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://gitlab.com/bitspur/frappe/multiplatform.one.git",
15
+ "url": "git+https://git.corp.bitspur.com/multiplatform.one/mpo.git",
16
16
  "directory": "public/platform"
17
17
  },
18
18
  "source": "src/index.ts",
19
19
  "files": [
20
+ "file.ts",
20
21
  "dist",
21
22
  "src",
22
23
  "types",
@@ -31,6 +32,14 @@
31
32
  "./rnw-overrides": {
32
33
  "types": "./src/rnw_overrides.d.ts"
33
34
  },
35
+ "./file": {
36
+ "source": "./src/file/index.ts",
37
+ "react-native": "./src/file/index.ts",
38
+ "types": "./types/file/index.d.ts",
39
+ "import": "./dist/esm/file/index.mjs",
40
+ "require": "./dist/cjs/file/index.cjs",
41
+ "default": "./dist/cjs/file/index.cjs"
42
+ },
34
43
  ".": {
35
44
  "source": "./src/index.ts",
36
45
  "react-native": "./src/index.ts",
@@ -47,24 +56,29 @@
47
56
  "@tamagui/constants": "2.7.6"
48
57
  },
49
58
  "devDependencies": {
59
+ "@multiplatform.one/config": "7.15.0",
50
60
  "@tamagui/build": "2.7.6",
51
61
  "@types/chrome": "^0.1.40",
52
62
  "@types/firefox-webext-browser": "^143.0.0",
53
63
  "@vitest/coverage-v8": "^4.1.5",
64
+ "expo-file-system": "55.0.26",
54
65
  "happy-dom": "^20.9.0",
55
66
  "react": "19.2.5",
56
67
  "typescript": "~5.9.3",
57
- "vitest": "^4.1.5",
58
- "@multiplatform.one/config": "7.10.0"
68
+ "vitest": "^4.1.5"
59
69
  },
60
70
  "peerDependencies": {
61
71
  "expo-constants": "~55.0.15",
72
+ "expo-file-system": "~55.0.26",
62
73
  "react": "^19.1.0"
63
74
  },
64
75
  "peerDependenciesMeta": {
65
76
  "expo-constants": {
66
77
  "optional": true
67
78
  },
79
+ "expo-file-system": {
80
+ "optional": true
81
+ },
68
82
  "react": {
69
83
  "optional": true
70
84
  }
@@ -1,4 +1,5 @@
1
1
  import constants from "expo-constants";
2
+ import { resolveDevOrigin } from "./devOrigin";
2
3
  import type { IConfig } from "./types";
3
4
 
4
5
  export class Config implements IConfig {
@@ -14,7 +15,7 @@ export class Config implements IConfig {
14
15
  private _resolveConfig() {
15
16
  return Object.keys(this._config).reduce<Record<string, string>>((config, key) => {
16
17
  const value = this._lookupEnv(key) || this._config[key];
17
- if (typeof value !== "undefined") config[key] = value;
18
+ if (typeof value !== "undefined") config[key] = resolveDevOrigin(value);
18
19
  return config;
19
20
  }, {});
20
21
  }
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { devHostFromHostUri, resolveDevOrigin } from "./devOrigin";
3
+
4
+ const phone = { isWeb: false, hostUri: "10.0.0.5:3456" };
5
+
6
+ describe("resolveDevOrigin", () => {
7
+ it("returns the url unchanged on web, even with a hostUri", () => {
8
+ expect(
9
+ resolveDevOrigin("http://localhost:8000", { isWeb: true, hostUri: "10.0.0.5:3456" }),
10
+ ).toBe("http://localhost:8000");
11
+ });
12
+
13
+ it("rewrites a localhost origin to the hostUri host on native", () => {
14
+ expect(resolveDevOrigin("http://localhost:8000", phone)).toBe("http://10.0.0.5:8000");
15
+ });
16
+
17
+ it("rewrites the 127.0.0.1 spelling too", () => {
18
+ expect(resolveDevOrigin("http://127.0.0.1:8000", phone)).toBe("http://10.0.0.5:8000");
19
+ });
20
+
21
+ it("keeps the configured port, path, query and scheme", () => {
22
+ expect(resolveDevOrigin("http://localhost:8080/auth/realms/myrealm?x=1#f", phone)).toBe(
23
+ "http://10.0.0.5:8080/auth/realms/myrealm?x=1#f",
24
+ );
25
+ expect(resolveDevOrigin("ws://localhost:9000/socket.io", phone)).toBe(
26
+ "ws://10.0.0.5:9000/socket.io",
27
+ );
28
+ expect(resolveDevOrigin("http://localhost", phone)).toBe("http://10.0.0.5");
29
+ });
30
+
31
+ it("leaves non-loopback hosts untouched", () => {
32
+ expect(resolveDevOrigin("http://192.168.1.20:8000", phone)).toBe("http://192.168.1.20:8000");
33
+ expect(resolveDevOrigin("https://bench.example.com", phone)).toBe("https://bench.example.com");
34
+ expect(resolveDevOrigin("http://localhost.example.com:8000", phone)).toBe(
35
+ "http://localhost.example.com:8000",
36
+ );
37
+ });
38
+
39
+ it("leaves the url alone when there is no hostUri (release build)", () => {
40
+ expect(resolveDevOrigin("http://localhost:8000", { isWeb: false })).toBe(
41
+ "http://localhost:8000",
42
+ );
43
+ expect(resolveDevOrigin("http://localhost:8000", { isWeb: false, hostUri: "" })).toBe(
44
+ "http://localhost:8000",
45
+ );
46
+ });
47
+
48
+ it("passes values that are not urls through untouched", () => {
49
+ expect(resolveDevOrigin("8080", phone)).toBe("8080");
50
+ expect(resolveDevOrigin("localhost:8000", phone)).toBe("localhost:8000");
51
+ expect(resolveDevOrigin("", phone)).toBe("");
52
+ });
53
+
54
+ it("defaults to the web source in this environment", () => {
55
+ expect(resolveDevOrigin("http://localhost:8000")).toBe("http://localhost:8000");
56
+ });
57
+ });
58
+
59
+ describe("devHostFromHostUri", () => {
60
+ it("takes the host out of host:port", () => {
61
+ expect(devHostFromHostUri("10.0.0.5:3456")).toBe("10.0.0.5");
62
+ expect(devHostFromHostUri("dev.local")).toBe("dev.local");
63
+ });
64
+
65
+ it("strips a scheme and a path when a manifest carries them", () => {
66
+ expect(devHostFromHostUri("http://10.0.0.5:3456/index.bundle")).toBe("10.0.0.5");
67
+ expect(devHostFromHostUri("exp://10.0.0.5:3456")).toBe("10.0.0.5");
68
+ });
69
+
70
+ it("keeps an IPv6 literal bracketed", () => {
71
+ expect(devHostFromHostUri("[fd00::2]:3456")).toBe("[fd00::2]");
72
+ });
73
+
74
+ it("returns undefined for nothing", () => {
75
+ expect(devHostFromHostUri(undefined)).toBeUndefined();
76
+ expect(devHostFromHostUri("")).toBeUndefined();
77
+ expect(devHostFromHostUri(":3456")).toBeUndefined();
78
+ });
79
+ });