@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.
- package/README.md +67 -0
- package/dist/cjs/config/devOrigin.cjs +42 -0
- package/dist/cjs/config/devOriginSource.cjs +32 -0
- package/dist/cjs/config/index.cjs +1 -0
- package/dist/cjs/file/index.cjs +30 -0
- package/dist/cjs/file/saveFile.cjs +47 -0
- package/dist/cjs/file/types.cjs +56 -0
- package/dist/cjs/utils/downloadFile.cjs +10 -4
- package/dist/esm/config/devOrigin.mjs +16 -0
- package/dist/esm/config/devOrigin.mjs.map +1 -0
- package/dist/esm/config/devOriginSource.mjs +7 -0
- package/dist/esm/config/devOriginSource.mjs.map +1 -0
- package/dist/esm/config/index.mjs +1 -0
- package/dist/esm/config/index.mjs.map +1 -1
- package/dist/esm/file/index.mjs +4 -0
- package/dist/esm/file/index.mjs.map +1 -0
- package/dist/esm/file/saveFile.mjs +22 -0
- package/dist/esm/file/saveFile.mjs.map +1 -0
- package/dist/esm/file/types.mjs +30 -0
- package/dist/esm/file/types.mjs.map +1 -0
- package/dist/esm/utils/downloadFile.mjs +10 -4
- package/dist/esm/utils/downloadFile.mjs.map +1 -1
- package/file.ts +1 -0
- package/package.json +18 -4
- package/src/config/config.native.ts +2 -1
- package/src/config/devOrigin.spec.ts +79 -0
- package/src/config/devOrigin.ts +45 -0
- package/src/config/devOriginSource.native.ts +9 -0
- package/src/config/devOriginSource.ts +5 -0
- package/src/config/index.ts +1 -0
- package/src/file/index.ts +3 -0
- package/src/file/saveFile.native.spec.ts +245 -0
- package/src/file/saveFile.native.ts +83 -0
- package/src/file/saveFile.spec.ts +98 -0
- package/src/file/saveFile.ts +25 -0
- package/src/file/saveFile.unavailable.native.spec.ts +24 -0
- package/src/file/types.ts +74 -0
- package/src/utils/downloadFile.spec.ts +54 -0
- package/src/utils/downloadFile.ts +11 -4
- package/types/config/config.native.d.ts.map +1 -1
- package/types/config/devOrigin.d.ts +28 -0
- package/types/config/devOrigin.d.ts.map +1 -0
- package/types/config/devOriginSource.d.ts +3 -0
- package/types/config/devOriginSource.d.ts.map +1 -0
- package/types/config/devOriginSource.native.d.ts +3 -0
- package/types/config/devOriginSource.native.d.ts.map +1 -0
- package/types/config/index.d.ts +1 -0
- package/types/config/index.d.ts.map +1 -1
- package/types/file/index.d.ts +4 -0
- package/types/file/index.d.ts.map +1 -0
- package/types/file/saveFile.d.ts +3 -0
- package/types/file/saveFile.d.ts.map +1 -0
- package/types/file/saveFile.native.d.ts +3 -0
- package/types/file/saveFile.native.d.ts.map +1 -0
- package/types/file/types.d.ts +34 -0
- package/types/file/types.d.ts.map +1 -0
- 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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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 @@
|
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Config","config"],"sources":["../../../src/config/index.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,MAAA,QAAc;AAKvB,cAAc;AACd,cAAc;
|
|
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 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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","
|
|
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.
|
|
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://
|
|
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
|
+
});
|