@pistonite/pure 0.28.0 → 0.29.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/LICENSE +1 -1
- package/README.md +23 -4
- package/dist/log/index.js +57 -0
- package/dist/log/index.js.map +1 -0
- package/dist/memory/index.js +92 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/result/index.js +29 -0
- package/dist/result/index.js.map +1 -0
- package/dist/sync/index.js +252 -0
- package/dist/sync/index.js.map +1 -0
- package/package.json +22 -13
- package/src/env.d.ts +1 -0
- package/src/log/index.ts +36 -11
- package/src/log/logger.ts +93 -115
- package/src/memory/cell.ts +21 -11
- package/src/memory/emp.ts +34 -23
- package/src/memory/idgen.test.ts +1 -1
- package/src/memory/index.ts +1 -4
- package/src/memory/persist.ts +9 -12
- package/src/result/index.ts +12 -4
- package/src/sync/batch.test.ts +1 -1
- package/src/sync/batch.ts +12 -17
- package/src/sync/capture.ts +2 -0
- package/src/sync/debounce.test.ts +1 -1
- package/src/sync/debounce.ts +12 -15
- package/src/sync/index.ts +2 -3
- package/src/sync/latest.test.ts +1 -1
- package/src/sync/latest.ts +19 -16
- package/src/sync/once.test.ts +1 -1
- package/src/sync/once.ts +13 -8
- package/src/sync/serial.test.ts +1 -1
- package/src/sync/serial.ts +14 -12
- package/src/sync/util.ts +2 -2
- package/src/fs/FsError.ts +0 -55
- package/src/fs/FsFile.ts +0 -67
- package/src/fs/FsFileImpl.ts +0 -219
- package/src/fs/FsFileMgr.ts +0 -29
- package/src/fs/FsFileStandalone.ts +0 -21
- package/src/fs/FsFileStandaloneImplFileAPI.ts +0 -54
- package/src/fs/FsFileStandaloneImplHandleAPI.ts +0 -147
- package/src/fs/FsFileSystem.ts +0 -71
- package/src/fs/FsFileSystemInternal.ts +0 -30
- package/src/fs/FsImplEntryAPI.ts +0 -149
- package/src/fs/FsImplFileAPI.ts +0 -116
- package/src/fs/FsImplHandleAPI.ts +0 -199
- package/src/fs/FsOpen.ts +0 -271
- package/src/fs/FsOpenFile.ts +0 -256
- package/src/fs/FsPath.ts +0 -137
- package/src/fs/FsSave.ts +0 -216
- package/src/fs/FsSupportStatus.ts +0 -87
- package/src/fs/index.ts +0 -123
- package/src/log/internal.ts +0 -14
- package/src/memory/async_erc.ts +0 -186
- package/src/memory/erc.test.ts +0 -258
- package/src/memory/erc.ts +0 -320
- package/src/pref/dark.ts +0 -151
- package/src/pref/device.ts +0 -118
- package/src/pref/index.ts +0 -13
- package/src/pref/inject_style.ts +0 -22
- package/src/pref/locale.ts +0 -296
package/src/fs/FsOpenFile.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { errstr } from "../result";
|
|
2
|
-
|
|
3
|
-
import { fsErr, FsErr, fsFail, type FsResult } from "./FsError.ts";
|
|
4
|
-
import type { FsFileStandalone } from "./FsFileStandalone.ts";
|
|
5
|
-
import { FsFileStandaloneImplFileAPI } from "./FsFileStandaloneImplFileAPI.ts";
|
|
6
|
-
import { FsFileStandaloneImplHandleAPI } from "./FsFileStandaloneImplHandleAPI.ts";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Prompt user to open a file
|
|
10
|
-
*
|
|
11
|
-
* FileSystemAccess API is used on supported platforms, which allows writing after
|
|
12
|
-
* user grants the permission once at the time of writing to the file. If failed or not supported,
|
|
13
|
-
* the DOM input element is used as a fallback.
|
|
14
|
-
*/
|
|
15
|
-
export const fsOpenFile = async (
|
|
16
|
-
options?: FsFileOpenOptions,
|
|
17
|
-
): Promise<FsResult<FsFileStandalone>> => {
|
|
18
|
-
const result = await fsOpenFileInternal(false, options || {});
|
|
19
|
-
if (result.err) {
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
if (!result.val.length) {
|
|
23
|
-
return { err: fsErr(FsErr.UserAbort, "No files selected") };
|
|
24
|
-
}
|
|
25
|
-
return { val: result.val[0] };
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Prompt user to open multiple files
|
|
30
|
-
*
|
|
31
|
-
* FileSystemAccess API is used on supported platforms, which allows writing after
|
|
32
|
-
* user grants the permission once at the time of writing to the file. If failed or not supported,
|
|
33
|
-
* the DOM input element is used as a fallback.
|
|
34
|
-
*
|
|
35
|
-
* The returned array is guaranteed to have at least 1 file.
|
|
36
|
-
*/
|
|
37
|
-
export const fsOpenFileMultiple = async (
|
|
38
|
-
options?: FsFileOpenOptions,
|
|
39
|
-
): Promise<FsResult<FsFileStandalone[]>> => {
|
|
40
|
-
const result = await fsOpenFileInternal(true, options || {});
|
|
41
|
-
if (result.err) {
|
|
42
|
-
return result;
|
|
43
|
-
}
|
|
44
|
-
if (!result.val.length) {
|
|
45
|
-
return { err: fsErr(FsErr.UserAbort, "No files selected") };
|
|
46
|
-
}
|
|
47
|
-
return result;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const fsOpenFileInternal = async (
|
|
51
|
-
multiple: boolean,
|
|
52
|
-
options: FsFileOpenOptions,
|
|
53
|
-
): Promise<FsResult<FsFileStandalone[]>> => {
|
|
54
|
-
if (isFileSystemAccessAPISupportedForStandaloneFileOpen()) {
|
|
55
|
-
const result = await fsOpenFileWithFileSystemAccessAPI(multiple, options);
|
|
56
|
-
if (result.val || result.err.code === FsErr.UserAbort) {
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
// fallback if FileSystemAccessAPI is not supported or fails
|
|
61
|
-
return fsOpenFileWithFileAPI(multiple, options);
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const fsOpenFileWithFileAPI = async (
|
|
65
|
-
multiple: boolean,
|
|
66
|
-
{ types }: FsFileOpenOptions,
|
|
67
|
-
): Promise<FsResult<FsFileStandalone[]>> => {
|
|
68
|
-
const element = document.createElement("input");
|
|
69
|
-
element.type = "file";
|
|
70
|
-
if (multiple) {
|
|
71
|
-
element.multiple = true;
|
|
72
|
-
}
|
|
73
|
-
if (types?.length) {
|
|
74
|
-
const accept = new Set<string>();
|
|
75
|
-
const len = types.length;
|
|
76
|
-
for (let i = 0; i < len; i++) {
|
|
77
|
-
const acceptArray = types[i].accept;
|
|
78
|
-
const acceptLen = acceptArray.length;
|
|
79
|
-
for (let j = 0; j < acceptLen; j++) {
|
|
80
|
-
const acceptValue = acceptArray[j];
|
|
81
|
-
if (typeof acceptValue === "string") {
|
|
82
|
-
accept.add(acceptValue);
|
|
83
|
-
} else {
|
|
84
|
-
const { mime, extensions } = acceptValue;
|
|
85
|
-
if (mime) {
|
|
86
|
-
accept.add(mime);
|
|
87
|
-
}
|
|
88
|
-
if (extensions.length) {
|
|
89
|
-
extensions.forEach((ext) => {
|
|
90
|
-
if (ext) {
|
|
91
|
-
accept.add(ext);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
element.accept = Array.from(accept).join(",");
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
return new Promise((resolve) => {
|
|
102
|
-
element.addEventListener("cancel", () => {
|
|
103
|
-
resolve({
|
|
104
|
-
err: fsErr(FsErr.UserAbort, "cancel listener invoked"),
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
element.addEventListener("change", () => {
|
|
108
|
-
if (!element.files?.length) {
|
|
109
|
-
resolve({
|
|
110
|
-
err: fsErr(FsErr.UserAbort, "no files selected"),
|
|
111
|
-
});
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const array = [];
|
|
115
|
-
for (let i = 0; i < element.files.length; i++) {
|
|
116
|
-
array.push(new FsFileStandaloneImplFileAPI(element.files[i]));
|
|
117
|
-
}
|
|
118
|
-
resolve({ val: array });
|
|
119
|
-
});
|
|
120
|
-
element.click();
|
|
121
|
-
});
|
|
122
|
-
} catch (e) {
|
|
123
|
-
return { err: fsFail(errstr(e)) };
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const fsOpenFileWithFileSystemAccessAPI = async (
|
|
128
|
-
multiple: boolean,
|
|
129
|
-
{ id, types, disallowWildcard }: FsFileOpenOptions,
|
|
130
|
-
): Promise<FsResult<FsFileStandalone[]>> => {
|
|
131
|
-
const convertedTypes = types?.map((type) => {
|
|
132
|
-
const { description, accept } = type;
|
|
133
|
-
const convertedAccept: Record<string, string[]> = {};
|
|
134
|
-
const anyMimeType = [];
|
|
135
|
-
const len = accept.length;
|
|
136
|
-
for (let i = 0; i < len; i++) {
|
|
137
|
-
const acceptValue = accept[i];
|
|
138
|
-
if (typeof acceptValue === "string") {
|
|
139
|
-
if (acceptValue) {
|
|
140
|
-
anyMimeType.push(acceptValue);
|
|
141
|
-
}
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const { mime, extensions } = acceptValue;
|
|
145
|
-
if (!mime || mime === "*/*") {
|
|
146
|
-
anyMimeType.push(...extensions);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (mime in convertedAccept) {
|
|
150
|
-
convertedAccept[mime].push(...extensions);
|
|
151
|
-
} else {
|
|
152
|
-
convertedAccept[mime] = [...extensions];
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (anyMimeType.length) {
|
|
156
|
-
convertedAccept["*/*"] = anyMimeType;
|
|
157
|
-
}
|
|
158
|
-
return { description, accept: convertedAccept };
|
|
159
|
-
});
|
|
160
|
-
try {
|
|
161
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
162
|
-
const output = await (globalThis as any).showOpenFilePicker({
|
|
163
|
-
id,
|
|
164
|
-
excludeAcceptAllOption: types && types.length && disallowWildcard,
|
|
165
|
-
multiple,
|
|
166
|
-
types: convertedTypes,
|
|
167
|
-
});
|
|
168
|
-
const len = output.length;
|
|
169
|
-
const convertedOutput = [];
|
|
170
|
-
for (let i = 0; i < len; i++) {
|
|
171
|
-
convertedOutput.push(new FsFileStandaloneImplHandleAPI(output[i]));
|
|
172
|
-
}
|
|
173
|
-
return { val: convertedOutput };
|
|
174
|
-
} catch (e) {
|
|
175
|
-
if (e && typeof e === "object" && "name" in e) {
|
|
176
|
-
if (e.name === "AbortError") {
|
|
177
|
-
return { err: fsErr(FsErr.UserAbort, "User abort") };
|
|
178
|
-
}
|
|
179
|
-
if (e.name === "SecurityError") {
|
|
180
|
-
return { err: fsErr(FsErr.PermissionDenied, "Security error") };
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return { err: fsFail(errstr(e)) };
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
export type FsFileOpenOptions = {
|
|
188
|
-
/**
|
|
189
|
-
* ID for the file open operation
|
|
190
|
-
*
|
|
191
|
-
* Supported implementation can use this to open the picker in the same
|
|
192
|
-
* directory for the same ID
|
|
193
|
-
*/
|
|
194
|
-
id?: string;
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* If the "*.*" file type should be hidden in the picker.
|
|
198
|
-
*
|
|
199
|
-
* In unsupported implementations, this will be ignored, and the "*.*"
|
|
200
|
-
* file type will always be visible.
|
|
201
|
-
*
|
|
202
|
-
* By default, this is false
|
|
203
|
-
*/
|
|
204
|
-
disallowWildcard?: boolean;
|
|
205
|
-
|
|
206
|
-
/** List of file types to accept */
|
|
207
|
-
types?: FsFileOpenType[];
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
export type FsFileOpenType = {
|
|
211
|
-
/**
|
|
212
|
-
* Optional description for the type, which may display in the file picker
|
|
213
|
-
*
|
|
214
|
-
* In unsupported implementations, this will be ignored.
|
|
215
|
-
*/
|
|
216
|
-
description?: string;
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* List of file mime types or extensions to accept for this file type
|
|
220
|
-
*
|
|
221
|
-
* String elements are file extensions, with the "." prefix. More context
|
|
222
|
-
* can be provided with an object element with mime type and extensions to
|
|
223
|
-
* have better file type descriptions in the picker (if supported).
|
|
224
|
-
*/
|
|
225
|
-
accept: (FsFileOpenTypeAccept | string)[];
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
export type FsFileOpenTypeAccept = {
|
|
229
|
-
/** Optional mime type, which the browser can be used to display file type descriptions */
|
|
230
|
-
mime?: string;
|
|
231
|
-
/** extensions to accept (with the "." prefix) */
|
|
232
|
-
extensions: string[];
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const isFileSystemAccessAPISupportedForStandaloneFileOpen = () => {
|
|
236
|
-
if (!globalThis || !globalThis.isSecureContext) {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
if (
|
|
240
|
-
!("showOpenFilePicker" in globalThis) ||
|
|
241
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
242
|
-
typeof (globalThis as any).showOpenFilePicker !== "function"
|
|
243
|
-
) {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
if (!globalThis.FileSystemFileHandle) {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
if (!globalThis.FileSystemFileHandle.prototype.getFile) {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
if (!globalThis.FileSystemFileHandle.prototype.createWritable) {
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
return true;
|
|
256
|
-
};
|
package/src/fs/FsPath.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path utilities
|
|
3
|
-
*
|
|
4
|
-
* The library has the following path standard:
|
|
5
|
-
* - All paths are relative (without leading /) to the root
|
|
6
|
-
* of the file system (i.e. the uploaded directory)
|
|
7
|
-
* - Paths are always separated by /
|
|
8
|
-
* - Empty string denotes root
|
|
9
|
-
* - Paths cannot lead outside of root
|
|
10
|
-
*
|
|
11
|
-
* @module
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { FsErr, type FsResult, fsErr } from "./FsError.ts";
|
|
15
|
-
|
|
16
|
-
/** Get the root path. Current implementation is empty string. */
|
|
17
|
-
export function fsRoot(): string {
|
|
18
|
-
return "";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Check if a path is the root directory, also handles badly formatted paths like ".///../" */
|
|
22
|
-
export function fsIsRoot(p: string): boolean {
|
|
23
|
-
if (!p) {
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
for (let i = 0; i < p.length; i++) {
|
|
27
|
-
if (p[i] !== "/" || p[i] !== "." || p[i] !== "\\") {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Get the base name of a path (i.e. remove the last component)
|
|
36
|
-
*
|
|
37
|
-
* If this path is the root directory, return InvalidPath.
|
|
38
|
-
*/
|
|
39
|
-
export function fsGetBase(p: string): FsResult<string> {
|
|
40
|
-
if (fsIsRoot(p)) {
|
|
41
|
-
const err = fsErr(FsErr.InvalidPath, "Trying to get the base of root");
|
|
42
|
-
return { err };
|
|
43
|
-
}
|
|
44
|
-
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
45
|
-
if (i < 0) {
|
|
46
|
-
return { val: fsRoot() };
|
|
47
|
-
}
|
|
48
|
-
return { val: p.substring(0, i) };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get the name of a path (i.e. the last component)
|
|
53
|
-
*
|
|
54
|
-
* Returns the last component of the path.
|
|
55
|
-
* Does not include leading or trailing slashes.
|
|
56
|
-
*
|
|
57
|
-
* If this path is the root directory, return IsRoot.
|
|
58
|
-
*/
|
|
59
|
-
export function fsGetName(p: string): FsResult<string> {
|
|
60
|
-
p = stripTrailingSlashes(p);
|
|
61
|
-
if (fsIsRoot(p)) {
|
|
62
|
-
const err = fsErr(FsErr.IsRoot, "Root directory has no name");
|
|
63
|
-
return { err };
|
|
64
|
-
}
|
|
65
|
-
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
66
|
-
if (i < 0) {
|
|
67
|
-
return { val: p };
|
|
68
|
-
}
|
|
69
|
-
return { val: p.substring(i + 1) };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Normalize .. and . in a path
|
|
74
|
-
*
|
|
75
|
-
* Returns InvalidPath if the path tries to escape the root directory.
|
|
76
|
-
*/
|
|
77
|
-
export function fsNormalize(p: string): FsResult<string> {
|
|
78
|
-
let s = fsRoot();
|
|
79
|
-
for (const comp of fsComponents(p)) {
|
|
80
|
-
if (comp === "..") {
|
|
81
|
-
const base = fsGetBase(s);
|
|
82
|
-
if (base.err) {
|
|
83
|
-
return base;
|
|
84
|
-
}
|
|
85
|
-
s = base.val;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
s = fsJoin(s, comp);
|
|
89
|
-
}
|
|
90
|
-
return { val: s };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Join two paths */
|
|
94
|
-
export function fsJoin(p1: string, p2: string): string {
|
|
95
|
-
if (fsIsRoot(p1)) {
|
|
96
|
-
return p2;
|
|
97
|
-
}
|
|
98
|
-
return p1 + "/" + p2;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Iterate through the components of a path. Empty components and . are skipped */
|
|
102
|
-
export function* fsComponents(p: string): Iterable<string> {
|
|
103
|
-
let i = 0;
|
|
104
|
-
while (i < p.length) {
|
|
105
|
-
let nextSlash = p.indexOf("/", i);
|
|
106
|
-
if (nextSlash < 0) {
|
|
107
|
-
nextSlash = p.length;
|
|
108
|
-
}
|
|
109
|
-
let nextBackslash = p.indexOf("\\", i);
|
|
110
|
-
if (nextBackslash < 0) {
|
|
111
|
-
nextBackslash = p.length;
|
|
112
|
-
}
|
|
113
|
-
let j = Math.min(nextSlash, nextBackslash);
|
|
114
|
-
if (j < 0) {
|
|
115
|
-
j = p.length;
|
|
116
|
-
}
|
|
117
|
-
const c = p.substring(i, j);
|
|
118
|
-
if (c && c !== ".") {
|
|
119
|
-
yield c;
|
|
120
|
-
}
|
|
121
|
-
i = j + 1;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Remove trailing slashes from a path */
|
|
126
|
-
function stripTrailingSlashes(p: string): string {
|
|
127
|
-
let i = p.length - 1;
|
|
128
|
-
for (; i >= 0; i--) {
|
|
129
|
-
if (p[i] !== "/" && p[i] !== "\\") {
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (i === p.length - 1) {
|
|
134
|
-
return p;
|
|
135
|
-
}
|
|
136
|
-
return p.substring(0, i + 1);
|
|
137
|
-
}
|
package/src/fs/FsSave.ts
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { ilog } from "../log/internal.ts";
|
|
2
|
-
|
|
3
|
-
import { fsFail, type FsVoid } from "./FsError.ts";
|
|
4
|
-
|
|
5
|
-
/** Save (download) a file using Blob */
|
|
6
|
-
export function fsSave(content: string | Uint8Array<ArrayBuffer>, filename: string): FsVoid {
|
|
7
|
-
const blob = new Blob([content], {
|
|
8
|
-
// maybe lying, but should be fine
|
|
9
|
-
type: "text/plain;charset=utf-8",
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
saveAs(blob, filename);
|
|
14
|
-
return {};
|
|
15
|
-
} catch (e) {
|
|
16
|
-
ilog.error(e);
|
|
17
|
-
return { err: fsFail("save failed") };
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// The following code is adopted from
|
|
22
|
-
// - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/file-saver/index.d.ts
|
|
23
|
-
// under this MIT license:
|
|
24
|
-
/*
|
|
25
|
-
This project is licensed under the MIT license.
|
|
26
|
-
Copyrights are respective of each contributor listed at the beginning of each definition file.
|
|
27
|
-
|
|
28
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
29
|
-
|
|
30
|
-
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
31
|
-
|
|
32
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
type SaveAsFn = (data: Blob | string, filename?: string, options?: SaveAsFnOptions) => void;
|
|
36
|
-
type SaveAsFnOptions = { autoBom: boolean };
|
|
37
|
-
|
|
38
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
39
|
-
|
|
40
|
-
// The following code is vendored from
|
|
41
|
-
// - https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js
|
|
42
|
-
// under this MIT license:
|
|
43
|
-
/*
|
|
44
|
-
* FileSaver.js
|
|
45
|
-
* A saveAs() FileSaver implementation.
|
|
46
|
-
*
|
|
47
|
-
* By Eli Grey, http://eligrey.com
|
|
48
|
-
*
|
|
49
|
-
* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
|
|
50
|
-
* source : http://purl.eligrey.com/github/FileSaver.js
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
// adoption note: this is now ECMA standard - https://github.com/tc39/proposal-global
|
|
54
|
-
// The one and only way of getting global scope in all environments
|
|
55
|
-
// https://stackoverflow.com/q/3277182/1008999
|
|
56
|
-
// const _global =
|
|
57
|
-
// typeof window === "object" && window.window === window
|
|
58
|
-
// ? window
|
|
59
|
-
// : typeof self === "object" && self.self === self
|
|
60
|
-
// ? self
|
|
61
|
-
// : typeof global === "object" && global.global === global
|
|
62
|
-
// ? global
|
|
63
|
-
// : this;
|
|
64
|
-
|
|
65
|
-
const download = (url: string | URL, name?: string, opts?: SaveAsFnOptions) => {
|
|
66
|
-
const xhr = new XMLHttpRequest();
|
|
67
|
-
xhr.open("GET", url);
|
|
68
|
-
xhr.responseType = "blob";
|
|
69
|
-
xhr.onload = function () {
|
|
70
|
-
saveAs(xhr.response, name, opts);
|
|
71
|
-
};
|
|
72
|
-
xhr.onerror = function () {
|
|
73
|
-
console.error("could not download file");
|
|
74
|
-
};
|
|
75
|
-
xhr.send();
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const corsEnabled = (url: string | URL) => {
|
|
79
|
-
const xhr = new XMLHttpRequest();
|
|
80
|
-
// use sync to avoid popup blocker
|
|
81
|
-
xhr.open("HEAD", url, false);
|
|
82
|
-
try {
|
|
83
|
-
xhr.send();
|
|
84
|
-
} catch {
|
|
85
|
-
// ignore
|
|
86
|
-
}
|
|
87
|
-
return xhr.status >= 200 && xhr.status <= 299;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// adoption note: we drop support for old browsers, and just use click()
|
|
91
|
-
// original note: `a.click()` doesn't work for all browsers (#465)
|
|
92
|
-
// const click = (node: Node) => {
|
|
93
|
-
// try {
|
|
94
|
-
// node.dispatchEvent(new MouseEvent('click'))
|
|
95
|
-
// } catch {
|
|
96
|
-
// const evt = document.createEvent('MouseEvents')
|
|
97
|
-
// evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
|
|
98
|
-
// 20, false, false, false, false, 0, null)
|
|
99
|
-
// node.dispatchEvent(evt)
|
|
100
|
-
// }
|
|
101
|
-
// }
|
|
102
|
-
|
|
103
|
-
// adoption note: converted to check at call time, no need to
|
|
104
|
-
// do this check at boot load
|
|
105
|
-
const saveAs: SaveAsFn = (blob, name?, opts?) => {
|
|
106
|
-
// adoption note: this is likely to throw if window is not defined.. ?
|
|
107
|
-
if (typeof window !== "object" || window !== globalThis) {
|
|
108
|
-
// probably in some web worker
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
// Detect WebView inside a native macOS app by ruling out all browsers
|
|
112
|
-
// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
|
|
113
|
-
// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
|
|
114
|
-
const isMacOSWebView =
|
|
115
|
-
globalThis.navigator &&
|
|
116
|
-
/Macintosh/.test(navigator.userAgent) &&
|
|
117
|
-
/AppleWebKit/.test(navigator.userAgent) &&
|
|
118
|
-
!/Safari/.test(navigator.userAgent);
|
|
119
|
-
// adoption note: removed blob.name, and pulled this line outside
|
|
120
|
-
name = name || "download";
|
|
121
|
-
|
|
122
|
-
if ("download" in HTMLAnchorElement.prototype && !isMacOSWebView) {
|
|
123
|
-
const URL = globalThis.URL || globalThis.webkitURL;
|
|
124
|
-
// Namespace is used to prevent conflict w/ Chrome Poper Blocker extension (Issue #561)
|
|
125
|
-
const a = document.createElementNS(
|
|
126
|
-
"http://www.w3.org/1999/xhtml",
|
|
127
|
-
"a",
|
|
128
|
-
) as HTMLAnchorElement;
|
|
129
|
-
|
|
130
|
-
a.download = name;
|
|
131
|
-
a.rel = "noopener"; // tabnabbing
|
|
132
|
-
|
|
133
|
-
// TODO: detect chrome extensions & packaged apps
|
|
134
|
-
// a.target = '_blank'
|
|
135
|
-
|
|
136
|
-
if (typeof blob === "string") {
|
|
137
|
-
// Support regular links
|
|
138
|
-
a.href = blob;
|
|
139
|
-
if (a.origin !== location.origin) {
|
|
140
|
-
// adoption note: removed turnery and changed click()
|
|
141
|
-
if (corsEnabled(a.href)) {
|
|
142
|
-
download(blob, name, opts);
|
|
143
|
-
} else {
|
|
144
|
-
a.target = "_blank";
|
|
145
|
-
a.click();
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
// adoption note: changed click()
|
|
149
|
-
a.click();
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
// Support blobs
|
|
153
|
-
a.href = URL.createObjectURL(blob);
|
|
154
|
-
// adoption note: not sure why 40s
|
|
155
|
-
setTimeout(() => {
|
|
156
|
-
URL.revokeObjectURL(a.href);
|
|
157
|
-
}, 4e4); // 40s
|
|
158
|
-
// adoption note: changed click()
|
|
159
|
-
setTimeout(() => {
|
|
160
|
-
a.click();
|
|
161
|
-
}, 0);
|
|
162
|
-
}
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// adoption note: drop IE support
|
|
167
|
-
|
|
168
|
-
// Fallback to using FileReader and a popup
|
|
169
|
-
// Open a popup immediately do go around popup blocker
|
|
170
|
-
// Mostly only available on user interaction and the fileReader is async so...
|
|
171
|
-
let popup = (window as any).popup || open("", "_blank");
|
|
172
|
-
if (popup) {
|
|
173
|
-
popup.document.title = popup.document.body.innerText = "downloading...";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (typeof blob === "string") {
|
|
177
|
-
return download(blob, name, opts);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const force = blob.type === "application/octet-stream";
|
|
181
|
-
// adoption note: add any
|
|
182
|
-
const isSafari =
|
|
183
|
-
/constructor/i.test((globalThis as any).HTMLElement) || (globalThis as any).safari;
|
|
184
|
-
const isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent);
|
|
185
|
-
|
|
186
|
-
if (
|
|
187
|
-
(isChromeIOS || (force && isSafari) || isMacOSWebView) &&
|
|
188
|
-
typeof FileReader !== "undefined"
|
|
189
|
-
) {
|
|
190
|
-
// Safari doesn't allow downloading of blob URLs
|
|
191
|
-
const reader = new FileReader();
|
|
192
|
-
reader.onloadend = function () {
|
|
193
|
-
let url = reader.result as string;
|
|
194
|
-
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, "data:attachment/file;");
|
|
195
|
-
if (popup) {
|
|
196
|
-
popup.location.href = url;
|
|
197
|
-
} else {
|
|
198
|
-
(location as any) = url;
|
|
199
|
-
}
|
|
200
|
-
popup = null; // reverse-tabnabbing #460
|
|
201
|
-
};
|
|
202
|
-
reader.readAsDataURL(blob);
|
|
203
|
-
} else {
|
|
204
|
-
const URL = globalThis.URL || globalThis.webkitURL;
|
|
205
|
-
const url = URL.createObjectURL(blob);
|
|
206
|
-
if (popup) {
|
|
207
|
-
popup.location = url;
|
|
208
|
-
} else {
|
|
209
|
-
location.href = url;
|
|
210
|
-
}
|
|
211
|
-
popup = null; // reverse-tabnabbing #460
|
|
212
|
-
setTimeout(function () {
|
|
213
|
-
URL.revokeObjectURL(url);
|
|
214
|
-
}, 4e4); // 40s
|
|
215
|
-
}
|
|
216
|
-
};
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/** What is supported by the current environment */
|
|
2
|
-
export type FsSupportStatus = {
|
|
3
|
-
/** Returned by window.isSecureContext */
|
|
4
|
-
isSecureContext: boolean;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The implementation for FsFileSystem used
|
|
8
|
-
*
|
|
9
|
-
* See README.md for more information
|
|
10
|
-
*/
|
|
11
|
-
implementation: "File" | "FileSystemAccess" | "FileEntry";
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
/** Get which implementation will be used for the current environment */
|
|
15
|
-
export function fsGetSupportStatus(): FsSupportStatus {
|
|
16
|
-
if (isFileSystemAccessSupported()) {
|
|
17
|
-
return {
|
|
18
|
-
isSecureContext: globalThis.isSecureContext,
|
|
19
|
-
implementation: "FileSystemAccess",
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
if (isFileEntrySupported()) {
|
|
23
|
-
return {
|
|
24
|
-
isSecureContext: globalThis.isSecureContext,
|
|
25
|
-
implementation: "FileEntry",
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
isSecureContext: !!globalThis && globalThis.isSecureContext,
|
|
31
|
-
implementation: "File",
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isFileSystemAccessSupported() {
|
|
36
|
-
if (!globalThis) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
if (!globalThis.isSecureContext) {
|
|
40
|
-
// In Chrome, you can still access the APIs but they just crash the page entirely
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
if (!globalThis.FileSystemDirectoryHandle) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!globalThis.FileSystemFileHandle) {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// since TSlib doesn't have these, let's check here
|
|
52
|
-
|
|
53
|
-
// @ts-expect-error FileSystemDirectoryHandle should have a values() method
|
|
54
|
-
if (!globalThis.FileSystemDirectoryHandle.prototype.values) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// @ts-expect-error window should have showDirectoryPicker
|
|
59
|
-
if (!globalThis.showDirectoryPicker) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function isFileEntrySupported(): boolean {
|
|
67
|
-
if (!globalThis) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Chrome/Edge has this but it's named DirectoryEntry
|
|
72
|
-
// AND, they don't work (I forgot how exactly they don't work)
|
|
73
|
-
|
|
74
|
-
if (navigator && navigator.userAgent && navigator.userAgent.includes("Chrome")) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!globalThis.FileSystemDirectoryEntry) {
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!globalThis.FileSystemFileEntry) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return true;
|
|
87
|
-
}
|