@maestro-js/file-storage 1.0.0-alpha.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/dist/index.d.ts +168 -0
- package/dist/index.js +659 -0
- package/package.json +40 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Iso } from 'iso-fns2';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { ReadableStream } from 'node:stream/web';
|
|
4
|
+
import { File } from 'node:buffer';
|
|
5
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
6
|
+
|
|
7
|
+
interface Driver<TemporaryUrlParams> {
|
|
8
|
+
get(path: string): Promise<Buffer>;
|
|
9
|
+
exists(path: string): Promise<boolean>;
|
|
10
|
+
url(path: string): string;
|
|
11
|
+
temporaryUrl(path: string, expiration: Iso.Instant, params?: TemporaryUrlParams): Promise<string>;
|
|
12
|
+
temporaryUploadUrl(path: string, expiration: Iso.Instant): Promise<{
|
|
13
|
+
url: string;
|
|
14
|
+
headers: Headers;
|
|
15
|
+
}>;
|
|
16
|
+
size(path: string): Promise<number>;
|
|
17
|
+
lastModified(path: string): Promise<Iso.Instant>;
|
|
18
|
+
mimeType(path: string): Promise<string | null>;
|
|
19
|
+
path(path: string): string;
|
|
20
|
+
put(path: string, contents: string | Buffer | ReadableStream | Readable): Promise<void>;
|
|
21
|
+
prepend(path: string, contents: string | Buffer | ReadableStream | Readable): Promise<void>;
|
|
22
|
+
append(path: string, contents: string | Buffer | ReadableStream | Readable): Promise<void>;
|
|
23
|
+
copy(source: string, destination: string): Promise<void>;
|
|
24
|
+
move(source: string, destination: string): Promise<void>;
|
|
25
|
+
remove(...paths: string[]): Promise<void>;
|
|
26
|
+
files(directory: string): Promise<string[]>;
|
|
27
|
+
allFiles(directory: string): Promise<string[]>;
|
|
28
|
+
directories(directory: string): Promise<string[]>;
|
|
29
|
+
allDirectories(directory: string): Promise<string[]>;
|
|
30
|
+
makeDirectory(directory: string): Promise<void>;
|
|
31
|
+
removeDirectory(directory: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare function createLocalDriver<TemporaryUrlParams>({ pathPrefix, buildTemporaryUrlsUsing }: {
|
|
35
|
+
pathPrefix?: string;
|
|
36
|
+
buildTemporaryUrlsUsing?(path: string, expiration: Iso.Instant, params?: TemporaryUrlParams): string | Promise<string>;
|
|
37
|
+
}): Driver<TemporaryUrlParams>;
|
|
38
|
+
|
|
39
|
+
declare function createS3Driver({ client, bucket, prefix, url: baseUrl }: {
|
|
40
|
+
client: S3Client;
|
|
41
|
+
bucket: string;
|
|
42
|
+
prefix?: string;
|
|
43
|
+
url?: string;
|
|
44
|
+
}): Driver<void>;
|
|
45
|
+
|
|
46
|
+
declare function create(config: FileStorage.Provider.FileStorageServiceConfig): FileStorage.FileStorageService;
|
|
47
|
+
type KeysWithFallback = keyof FileStorage.Provider.Keys extends never ? {
|
|
48
|
+
default: unknown;
|
|
49
|
+
} : FileStorage.Provider.Keys;
|
|
50
|
+
declare function provider(key: FileStorage.Provider.Key): {
|
|
51
|
+
get: (path: string) => Promise<Buffer>;
|
|
52
|
+
text: (path: string, encoding?: BufferEncoding) => Promise<string>;
|
|
53
|
+
json: (path: string, encoding?: BufferEncoding) => Promise<any>;
|
|
54
|
+
exists: (path: string) => Promise<boolean>;
|
|
55
|
+
missing: (path: string) => Promise<boolean>;
|
|
56
|
+
download: (path: string, filename?: string, headers?: HeadersInit) => Promise<Response>;
|
|
57
|
+
url: (path: string) => string;
|
|
58
|
+
temporaryUrl: (path: string, expires: Iso.Instant, params?: any) => Promise<string>;
|
|
59
|
+
temporaryUploadUrl: (path: string, expires: Iso.Instant) => Promise<{
|
|
60
|
+
url: string;
|
|
61
|
+
headers: Headers;
|
|
62
|
+
}>;
|
|
63
|
+
size: (path: string) => Promise<number>;
|
|
64
|
+
lastModified: (path: string) => Promise<Iso.Instant>;
|
|
65
|
+
mimeType: (path: string) => Promise<string | null>;
|
|
66
|
+
path: (path: string) => string;
|
|
67
|
+
put: (path: string, contents: string | Buffer | Readable | ReadableStream) => Promise<void>;
|
|
68
|
+
prepend: (path: string, contents: string | Buffer | Readable | ReadableStream) => Promise<void>;
|
|
69
|
+
append: (path: string, contents: string | Buffer | Readable | ReadableStream) => Promise<void>;
|
|
70
|
+
copy: (source: string, destination: string) => Promise<void>;
|
|
71
|
+
move: (source: string, destination: string) => Promise<void>;
|
|
72
|
+
putFileAs: (dir: string, file: File, filename: string) => Promise<string>;
|
|
73
|
+
putFile: (dir: string, file: File) => Promise<string>;
|
|
74
|
+
remove: (...paths: string[]) => Promise<void>;
|
|
75
|
+
files: (directory: string) => Promise<string[]>;
|
|
76
|
+
allFiles: (directory: string) => Promise<string[]>;
|
|
77
|
+
directories: (directory: string) => Promise<string[]>;
|
|
78
|
+
allDirectories: (directory: string) => Promise<string[]>;
|
|
79
|
+
makeDirectory: (directory: string) => Promise<void>;
|
|
80
|
+
removeDirectory: (directory: string) => Promise<void>;
|
|
81
|
+
};
|
|
82
|
+
declare const FileStorage: {
|
|
83
|
+
provider: typeof provider;
|
|
84
|
+
drivers: {
|
|
85
|
+
local: typeof createLocalDriver;
|
|
86
|
+
s3: typeof createS3Driver;
|
|
87
|
+
};
|
|
88
|
+
get: (path: string) => Promise<Buffer>;
|
|
89
|
+
text: (path: string, encoding?: BufferEncoding) => Promise<string>;
|
|
90
|
+
json: (path: string, encoding?: BufferEncoding) => Promise<any>;
|
|
91
|
+
exists: (path: string) => Promise<boolean>;
|
|
92
|
+
missing: (path: string) => Promise<boolean>;
|
|
93
|
+
download: (path: string, filename?: string, headers?: HeadersInit) => Promise<Response>;
|
|
94
|
+
url: (path: string) => string;
|
|
95
|
+
temporaryUrl: (path: string, expires: Iso.Instant, params?: any) => Promise<string>;
|
|
96
|
+
temporaryUploadUrl: (path: string, expires: Iso.Instant) => Promise<{
|
|
97
|
+
url: string;
|
|
98
|
+
headers: Headers;
|
|
99
|
+
}>;
|
|
100
|
+
size: (path: string) => Promise<number>;
|
|
101
|
+
lastModified: (path: string) => Promise<Iso.Instant>;
|
|
102
|
+
mimeType: (path: string) => Promise<string | null>;
|
|
103
|
+
path: (path: string) => string;
|
|
104
|
+
put: (path: string, contents: string | Buffer | Readable | ReadableStream) => Promise<void>;
|
|
105
|
+
prepend: (path: string, contents: string | Buffer | Readable | ReadableStream) => Promise<void>;
|
|
106
|
+
append: (path: string, contents: string | Buffer | Readable | ReadableStream) => Promise<void>;
|
|
107
|
+
copy: (source: string, destination: string) => Promise<void>;
|
|
108
|
+
move: (source: string, destination: string) => Promise<void>;
|
|
109
|
+
putFileAs: (dir: string, file: File, filename: string) => Promise<string>;
|
|
110
|
+
putFile: (dir: string, file: File) => Promise<string>;
|
|
111
|
+
remove: (...paths: string[]) => Promise<void>;
|
|
112
|
+
files: (directory: string) => Promise<string[]>;
|
|
113
|
+
allFiles: (directory: string) => Promise<string[]>;
|
|
114
|
+
directories: (directory: string) => Promise<string[]>;
|
|
115
|
+
allDirectories: (directory: string) => Promise<string[]>;
|
|
116
|
+
makeDirectory: (directory: string) => Promise<void>;
|
|
117
|
+
removeDirectory: (directory: string) => Promise<void>;
|
|
118
|
+
Provider: {
|
|
119
|
+
create: typeof create;
|
|
120
|
+
register: (name: FileStorage.Provider.Key, item: FileStorage.FileStorageService) => void;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
declare namespace FileStorage {
|
|
124
|
+
type Driver<T = any> = Driver<T>;
|
|
125
|
+
interface FileStorageService {
|
|
126
|
+
get(path: string): Promise<Buffer>;
|
|
127
|
+
text(path: string, encoding?: BufferEncoding): Promise<string>;
|
|
128
|
+
json(path: string, encoding?: BufferEncoding): Promise<any>;
|
|
129
|
+
exists(path: string): Promise<boolean>;
|
|
130
|
+
missing(path: string): Promise<boolean>;
|
|
131
|
+
download(path: string, filename?: string, headers?: HeadersInit): Promise<Response>;
|
|
132
|
+
url(path: string): string;
|
|
133
|
+
temporaryUrl(path: string, expires: Iso.Instant, params?: any): Promise<string>;
|
|
134
|
+
temporaryUploadUrl(path: string, expires: Iso.Instant): Promise<{
|
|
135
|
+
url: string;
|
|
136
|
+
headers: Headers;
|
|
137
|
+
}>;
|
|
138
|
+
size(path: string): Promise<number>;
|
|
139
|
+
lastModified(path: string): Promise<Iso.Instant>;
|
|
140
|
+
mimeType(path: string): Promise<string | null>;
|
|
141
|
+
path(path: string): string;
|
|
142
|
+
put(path: string, contents: string | Buffer | Readable | ReadableStream): Promise<void>;
|
|
143
|
+
prepend(path: string, contents: string | Buffer | Readable | ReadableStream): Promise<void>;
|
|
144
|
+
append(path: string, contents: string | Buffer | Readable | ReadableStream): Promise<void>;
|
|
145
|
+
copy(source: string, destination: string): Promise<void>;
|
|
146
|
+
move(source: string, destination: string): Promise<void>;
|
|
147
|
+
putFileAs(dir: string, file: File, filename: string): Promise<string>;
|
|
148
|
+
putFile(dir: string, file: File): Promise<string>;
|
|
149
|
+
remove(...paths: string[]): Promise<void>;
|
|
150
|
+
files(directory: string): Promise<string[]>;
|
|
151
|
+
allFiles(directory: string): Promise<string[]>;
|
|
152
|
+
directories(directory: string): Promise<string[]>;
|
|
153
|
+
allDirectories(directory: string): Promise<string[]>;
|
|
154
|
+
makeDirectory(directory: string): Promise<void>;
|
|
155
|
+
removeDirectory(directory: string): Promise<void>;
|
|
156
|
+
}
|
|
157
|
+
namespace Provider {
|
|
158
|
+
interface FileStorageServiceConfig {
|
|
159
|
+
driver: Driver<any>;
|
|
160
|
+
}
|
|
161
|
+
type Key = keyof KeysWithFallback;
|
|
162
|
+
interface Keys {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
type HeadersInit = string[][] | Record<string, string | ReadonlyArray<string>> | Headers;
|
|
167
|
+
|
|
168
|
+
export { FileStorage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import "iso-fns2";
|
|
3
|
+
import { parse as parsePath, join as joinPath } from "path";
|
|
4
|
+
import "stream";
|
|
5
|
+
import "stream/web";
|
|
6
|
+
|
|
7
|
+
// src/file-storage-types.ts
|
|
8
|
+
import "stream";
|
|
9
|
+
import "stream/web";
|
|
10
|
+
|
|
11
|
+
// src/index.ts
|
|
12
|
+
import "buffer";
|
|
13
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
14
|
+
import mimeTypes from "mime-types";
|
|
15
|
+
|
|
16
|
+
// src/sanitize-filename.ts
|
|
17
|
+
var illegalRe = /[\/\?<>\\:\*\|"]/g;
|
|
18
|
+
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
|
19
|
+
var reservedRe = /^\.+$/;
|
|
20
|
+
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
|
21
|
+
var windowsTrailingRe = /[\. ]+$/;
|
|
22
|
+
function sanitizeFilename(input) {
|
|
23
|
+
if (typeof input !== "string") {
|
|
24
|
+
throw new Error("Input must be string");
|
|
25
|
+
}
|
|
26
|
+
const sanitized = input.replace(illegalRe, "").replace(controlRe, "").replace(reservedRe, "").replace(windowsReservedRe, "").replace(windowsTrailingRe, "");
|
|
27
|
+
return truncate(sanitized, 255);
|
|
28
|
+
}
|
|
29
|
+
function truncate(string, byteLength) {
|
|
30
|
+
if (typeof string !== "string") {
|
|
31
|
+
throw new Error("Input must be string");
|
|
32
|
+
}
|
|
33
|
+
const charLength = string.length;
|
|
34
|
+
let curByteLength = 0;
|
|
35
|
+
let codePoint;
|
|
36
|
+
let segment;
|
|
37
|
+
for (let i = 0; i < charLength; i += 1) {
|
|
38
|
+
codePoint = string.charCodeAt(i);
|
|
39
|
+
segment = string[i];
|
|
40
|
+
if (isHighSurrogate(codePoint) && isLowSurrogate(string.charCodeAt(i + 1))) {
|
|
41
|
+
i += 1;
|
|
42
|
+
segment += string[i];
|
|
43
|
+
}
|
|
44
|
+
curByteLength += Buffer.byteLength(segment);
|
|
45
|
+
if (curByteLength === byteLength) {
|
|
46
|
+
return string.slice(0, i + 1);
|
|
47
|
+
} else if (curByteLength > byteLength) {
|
|
48
|
+
return string.slice(0, i - segment.length + 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return string;
|
|
52
|
+
}
|
|
53
|
+
function isHighSurrogate(codePoint) {
|
|
54
|
+
return codePoint >= 55296 && codePoint <= 56319;
|
|
55
|
+
}
|
|
56
|
+
function isLowSurrogate(codePoint) {
|
|
57
|
+
return codePoint >= 56320 && codePoint <= 57343;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/index.ts
|
|
61
|
+
import { ServiceRegistry } from "@maestro-js/service-registry";
|
|
62
|
+
|
|
63
|
+
// src/local-driver.ts
|
|
64
|
+
import fs from "fs/promises";
|
|
65
|
+
import { createWriteStream, createReadStream } from "fs";
|
|
66
|
+
import * as nodePath from "path";
|
|
67
|
+
import "iso-fns2";
|
|
68
|
+
import promiseStream from "stream/promises";
|
|
69
|
+
import { randomUUID } from "crypto";
|
|
70
|
+
import os from "os";
|
|
71
|
+
import "stream/web";
|
|
72
|
+
import stream from "stream";
|
|
73
|
+
import { lookup } from "mime-types";
|
|
74
|
+
function createLocalDriver({
|
|
75
|
+
pathPrefix = "storage",
|
|
76
|
+
buildTemporaryUrlsUsing
|
|
77
|
+
}) {
|
|
78
|
+
return {
|
|
79
|
+
get(path) {
|
|
80
|
+
return fs.readFile(nodePath.join(pathPrefix, path));
|
|
81
|
+
},
|
|
82
|
+
async exists(path) {
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(nodePath.join(pathPrefix, path), fs.constants.F_OK);
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
url(path) {
|
|
91
|
+
return nodePath.join(pathPrefix, path);
|
|
92
|
+
},
|
|
93
|
+
async temporaryUrl(path, expiration, params) {
|
|
94
|
+
if (buildTemporaryUrlsUsing) {
|
|
95
|
+
return await buildTemporaryUrlsUsing(nodePath.join(pathPrefix, path), expiration, params);
|
|
96
|
+
} else {
|
|
97
|
+
throw new Error('Temporary url is not supported by this local driver. See "buildTemporaryUrlsUsing" option.');
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
async temporaryUploadUrl(path, expiration) {
|
|
101
|
+
throw new Error("Temporary upload url is not supported by local driver");
|
|
102
|
+
},
|
|
103
|
+
async size(path) {
|
|
104
|
+
const stat = await fs.stat(nodePath.join(pathPrefix, path));
|
|
105
|
+
return stat.size;
|
|
106
|
+
},
|
|
107
|
+
async lastModified(path) {
|
|
108
|
+
const stat = await fs.stat(nodePath.join(pathPrefix, path));
|
|
109
|
+
return stat.mtime.toISOString();
|
|
110
|
+
},
|
|
111
|
+
async mimeType(path) {
|
|
112
|
+
return lookup(path) || null;
|
|
113
|
+
},
|
|
114
|
+
path(path) {
|
|
115
|
+
return nodePath.resolve(nodePath.join(pathPrefix, path));
|
|
116
|
+
},
|
|
117
|
+
async put(path, contents) {
|
|
118
|
+
await fs.writeFile(nodePath.join(pathPrefix, path), contents);
|
|
119
|
+
},
|
|
120
|
+
async prepend(path, contents) {
|
|
121
|
+
await prepend(nodePath.join(pathPrefix, path), contents);
|
|
122
|
+
},
|
|
123
|
+
async append(path, contents) {
|
|
124
|
+
if (Buffer.isBuffer(contents) || typeof contents == "string") {
|
|
125
|
+
await fs.appendFile(nodePath.join(pathPrefix, path), contents);
|
|
126
|
+
} else {
|
|
127
|
+
const writeStream2 = createWriteStream(nodePath.join(pathPrefix, path), { flags: "a" });
|
|
128
|
+
await promiseStream.pipeline(contents, writeStream2);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
async copy(source, destination) {
|
|
132
|
+
await fs.copyFile(nodePath.join(pathPrefix, source), nodePath.join(pathPrefix, destination));
|
|
133
|
+
},
|
|
134
|
+
async move(source, destination) {
|
|
135
|
+
await fs.rename(nodePath.join(pathPrefix, source), nodePath.join(pathPrefix, destination));
|
|
136
|
+
},
|
|
137
|
+
async remove(...paths) {
|
|
138
|
+
await Promise.all(paths.map((p) => fs.rm(nodePath.join(pathPrefix, p))));
|
|
139
|
+
},
|
|
140
|
+
async files(directory) {
|
|
141
|
+
const results = await fs.readdir(nodePath.join(pathPrefix, directory), { withFileTypes: true });
|
|
142
|
+
return results.filter((r) => r.isFile()).map((r) => nodePath.join(r.parentPath, r.name));
|
|
143
|
+
},
|
|
144
|
+
async allFiles(directory) {
|
|
145
|
+
const results = await fs.readdir(nodePath.join(pathPrefix, directory), { recursive: true, withFileTypes: true });
|
|
146
|
+
return results.filter((r) => r.isFile()).map((r) => nodePath.join(r.parentPath, r.name));
|
|
147
|
+
},
|
|
148
|
+
async directories(directory) {
|
|
149
|
+
const results = await fs.readdir(nodePath.join(pathPrefix, directory), { withFileTypes: true });
|
|
150
|
+
return results.filter((r) => r.isDirectory()).map((r) => nodePath.join(r.parentPath, r.name));
|
|
151
|
+
},
|
|
152
|
+
async allDirectories(directory) {
|
|
153
|
+
const results = await fs.readdir(nodePath.join(pathPrefix, directory), { recursive: true, withFileTypes: true });
|
|
154
|
+
return results.filter((r) => r.isDirectory()).map((r) => nodePath.join(r.parentPath, r.name));
|
|
155
|
+
},
|
|
156
|
+
async makeDirectory(directory) {
|
|
157
|
+
await fs.mkdir(nodePath.join(pathPrefix, directory), { recursive: true });
|
|
158
|
+
},
|
|
159
|
+
async removeDirectory(directory) {
|
|
160
|
+
await fs.rmdir(nodePath.join(pathPrefix, directory), { recursive: true });
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function prepend(filename, contents) {
|
|
165
|
+
let bomFound = false;
|
|
166
|
+
let bomPlaced = false;
|
|
167
|
+
const checkStripBomTransformer = new stream.Transform({
|
|
168
|
+
transform(chunk, _, callback) {
|
|
169
|
+
let fileData = chunk;
|
|
170
|
+
if (!bomFound) {
|
|
171
|
+
bomFound = hasBOM(fileData);
|
|
172
|
+
fileData = hasBOM(fileData) ? stripBOM(fileData) : fileData;
|
|
173
|
+
}
|
|
174
|
+
callback(null, Buffer.from(fileData));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const checkPrependBomTransformer = new stream.Transform({
|
|
178
|
+
transform(chunk, _, callback) {
|
|
179
|
+
let fileData = chunk.toString();
|
|
180
|
+
if (bomFound && !bomPlaced) {
|
|
181
|
+
fileData = prependBOM(fileData);
|
|
182
|
+
bomPlaced = true;
|
|
183
|
+
}
|
|
184
|
+
callback(null, Buffer.from(fileData));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
const temporaryFile = await tempWrite(contents);
|
|
188
|
+
try {
|
|
189
|
+
await promiseStream.pipeline(
|
|
190
|
+
createReadStream(filename),
|
|
191
|
+
checkStripBomTransformer,
|
|
192
|
+
createWriteStream(temporaryFile, { flags: "a" })
|
|
193
|
+
);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (typeof error === "object" && error && error["code"] === "ENOENT" && error["path"] === filename) {
|
|
196
|
+
await fs.writeFile(filename, contents);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
await promiseStream.pipeline(createReadStream(temporaryFile), checkPrependBomTransformer, createWriteStream(filename));
|
|
202
|
+
await fs.unlink(temporaryFile);
|
|
203
|
+
}
|
|
204
|
+
async function tempWrite(fileContent) {
|
|
205
|
+
const temporaryPath = await tempfile();
|
|
206
|
+
await fs.mkdir(nodePath.dirname(temporaryPath), { recursive: true });
|
|
207
|
+
if (Buffer.isBuffer(fileContent) || typeof fileContent === "string") {
|
|
208
|
+
await fs.writeFile(temporaryPath, fileContent);
|
|
209
|
+
} else {
|
|
210
|
+
await writeStream(temporaryPath, fileContent);
|
|
211
|
+
}
|
|
212
|
+
return temporaryPath;
|
|
213
|
+
}
|
|
214
|
+
async function tempfile(filePath = "") {
|
|
215
|
+
return nodePath.join(await tempDirectory, randomUUID(), filePath);
|
|
216
|
+
}
|
|
217
|
+
async function writeStream(filePath, data) {
|
|
218
|
+
return promiseStream.pipeline(data, createWriteStream(filePath));
|
|
219
|
+
}
|
|
220
|
+
var tempDirectory = fs.realpath(os.tmpdir());
|
|
221
|
+
function hasBOM(text) {
|
|
222
|
+
return text.toString().charCodeAt(0) === 65279;
|
|
223
|
+
}
|
|
224
|
+
function prependBOM(text) {
|
|
225
|
+
return "\uFEFF" + text;
|
|
226
|
+
}
|
|
227
|
+
function stripBOM(text) {
|
|
228
|
+
return text.toString().slice(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/s3-driver.ts
|
|
232
|
+
import {
|
|
233
|
+
GetObjectCommand,
|
|
234
|
+
HeadObjectCommand,
|
|
235
|
+
PutObjectCommand,
|
|
236
|
+
CopyObjectCommand,
|
|
237
|
+
DeleteObjectCommand,
|
|
238
|
+
DeleteObjectsCommand,
|
|
239
|
+
ListObjectsV2Command
|
|
240
|
+
} from "@aws-sdk/client-s3";
|
|
241
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
242
|
+
import "iso-fns2";
|
|
243
|
+
import "stream";
|
|
244
|
+
import { ReadableStream as ReadableStream2 } from "stream/web";
|
|
245
|
+
import { lookup as lookup2 } from "mime-types";
|
|
246
|
+
function createS3Driver({
|
|
247
|
+
client,
|
|
248
|
+
bucket,
|
|
249
|
+
prefix = "",
|
|
250
|
+
url: baseUrl
|
|
251
|
+
}) {
|
|
252
|
+
return {
|
|
253
|
+
async get(path) {
|
|
254
|
+
const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: resolveKey(prefix, path) }));
|
|
255
|
+
return Buffer.from(await response.Body.transformToByteArray());
|
|
256
|
+
},
|
|
257
|
+
async exists(path) {
|
|
258
|
+
try {
|
|
259
|
+
await client.send(new HeadObjectCommand({ Bucket: bucket, Key: resolveKey(prefix, path) }));
|
|
260
|
+
return true;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error.name === "NotFound" || error.$metadata?.httpStatusCode === 404) return false;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
url(path) {
|
|
267
|
+
const key = resolveKey(prefix, path);
|
|
268
|
+
if (baseUrl) return `${baseUrl.replace(/\/$/, "")}/${key}`;
|
|
269
|
+
return `https://${bucket}.s3.amazonaws.com/${key}`;
|
|
270
|
+
},
|
|
271
|
+
async temporaryUrl(path, expiration) {
|
|
272
|
+
const command = new GetObjectCommand({ Bucket: bucket, Key: resolveKey(prefix, path) });
|
|
273
|
+
return await getSignedUrl(client, command, { expiresIn: expirationToSeconds(expiration) });
|
|
274
|
+
},
|
|
275
|
+
async temporaryUploadUrl(path, expiration) {
|
|
276
|
+
const key = resolveKey(prefix, path);
|
|
277
|
+
const contentType = lookup2(path) || "application/octet-stream";
|
|
278
|
+
const command = new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: contentType });
|
|
279
|
+
const url = await getSignedUrl(client, command, { expiresIn: expirationToSeconds(expiration) });
|
|
280
|
+
return { url, headers: new Headers({ "Content-Type": contentType }) };
|
|
281
|
+
},
|
|
282
|
+
async size(path) {
|
|
283
|
+
const response = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: resolveKey(prefix, path) }));
|
|
284
|
+
return response.ContentLength;
|
|
285
|
+
},
|
|
286
|
+
async lastModified(path) {
|
|
287
|
+
const response = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: resolveKey(prefix, path) }));
|
|
288
|
+
return response.LastModified.toISOString();
|
|
289
|
+
},
|
|
290
|
+
async mimeType(path) {
|
|
291
|
+
const response = await client.send(new HeadObjectCommand({ Bucket: bucket, Key: resolveKey(prefix, path) }));
|
|
292
|
+
return response.ContentType ?? null;
|
|
293
|
+
},
|
|
294
|
+
path(path) {
|
|
295
|
+
return `s3://${bucket}/${resolveKey(prefix, path)}`;
|
|
296
|
+
},
|
|
297
|
+
async put(path, contents) {
|
|
298
|
+
const key = resolveKey(prefix, path);
|
|
299
|
+
const contentType = lookup2(path) || "application/octet-stream";
|
|
300
|
+
const body = await toBuffer(contents);
|
|
301
|
+
await client.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: contentType }));
|
|
302
|
+
},
|
|
303
|
+
async prepend(path, contents) {
|
|
304
|
+
const key = resolveKey(prefix, path);
|
|
305
|
+
let existing = Buffer.alloc(0);
|
|
306
|
+
try {
|
|
307
|
+
const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
308
|
+
existing = Buffer.from(await response.Body.transformToByteArray());
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error.name !== "NoSuchKey" && error.$metadata?.httpStatusCode !== 404) throw error;
|
|
311
|
+
}
|
|
312
|
+
const prepended = Buffer.concat([await toBuffer(contents), existing]);
|
|
313
|
+
const contentType = lookup2(path) || "application/octet-stream";
|
|
314
|
+
await client.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: prepended, ContentType: contentType }));
|
|
315
|
+
},
|
|
316
|
+
async append(path, contents) {
|
|
317
|
+
const key = resolveKey(prefix, path);
|
|
318
|
+
let existing = Buffer.alloc(0);
|
|
319
|
+
try {
|
|
320
|
+
const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
321
|
+
existing = Buffer.from(await response.Body.transformToByteArray());
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error.name !== "NoSuchKey" && error.$metadata?.httpStatusCode !== 404) throw error;
|
|
324
|
+
}
|
|
325
|
+
const appended = Buffer.concat([existing, await toBuffer(contents)]);
|
|
326
|
+
const contentType = lookup2(path) || "application/octet-stream";
|
|
327
|
+
await client.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: appended, ContentType: contentType }));
|
|
328
|
+
},
|
|
329
|
+
async copy(source, destination) {
|
|
330
|
+
await client.send(
|
|
331
|
+
new CopyObjectCommand({
|
|
332
|
+
Bucket: bucket,
|
|
333
|
+
CopySource: `${bucket}/${resolveKey(prefix, source)}`,
|
|
334
|
+
Key: resolveKey(prefix, destination)
|
|
335
|
+
})
|
|
336
|
+
);
|
|
337
|
+
},
|
|
338
|
+
async move(source, destination) {
|
|
339
|
+
await this.copy(source, destination);
|
|
340
|
+
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: resolveKey(prefix, source) }));
|
|
341
|
+
},
|
|
342
|
+
async remove(...paths) {
|
|
343
|
+
if (paths.length === 1) {
|
|
344
|
+
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: resolveKey(prefix, paths[0]) }));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
for (let i = 0; i < paths.length; i += 1e3) {
|
|
348
|
+
const batch = paths.slice(i, i + 1e3);
|
|
349
|
+
await client.send(
|
|
350
|
+
new DeleteObjectsCommand({
|
|
351
|
+
Bucket: bucket,
|
|
352
|
+
Delete: { Objects: batch.map((p) => ({ Key: resolveKey(prefix, p) })) }
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
async files(directory) {
|
|
358
|
+
const results = [];
|
|
359
|
+
let continuationToken;
|
|
360
|
+
const dirPrefix = resolveKey(prefix, directory).replace(/\/?$/, "/");
|
|
361
|
+
do {
|
|
362
|
+
const response = await client.send(
|
|
363
|
+
new ListObjectsV2Command({
|
|
364
|
+
Bucket: bucket,
|
|
365
|
+
Prefix: dirPrefix,
|
|
366
|
+
Delimiter: "/",
|
|
367
|
+
ContinuationToken: continuationToken
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
for (const item of response.Contents ?? []) {
|
|
371
|
+
if (item.Key) results.push(item.Key);
|
|
372
|
+
}
|
|
373
|
+
continuationToken = response.NextContinuationToken;
|
|
374
|
+
} while (continuationToken);
|
|
375
|
+
return results;
|
|
376
|
+
},
|
|
377
|
+
async allFiles(directory) {
|
|
378
|
+
const results = [];
|
|
379
|
+
let continuationToken;
|
|
380
|
+
const dirPrefix = resolveKey(prefix, directory).replace(/\/?$/, "/");
|
|
381
|
+
do {
|
|
382
|
+
const response = await client.send(
|
|
383
|
+
new ListObjectsV2Command({
|
|
384
|
+
Bucket: bucket,
|
|
385
|
+
Prefix: dirPrefix,
|
|
386
|
+
ContinuationToken: continuationToken
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
for (const item of response.Contents ?? []) {
|
|
390
|
+
if (item.Key) results.push(item.Key);
|
|
391
|
+
}
|
|
392
|
+
continuationToken = response.NextContinuationToken;
|
|
393
|
+
} while (continuationToken);
|
|
394
|
+
return results;
|
|
395
|
+
},
|
|
396
|
+
async directories(directory) {
|
|
397
|
+
const results = [];
|
|
398
|
+
let continuationToken;
|
|
399
|
+
const dirPrefix = resolveKey(prefix, directory).replace(/\/?$/, "/");
|
|
400
|
+
do {
|
|
401
|
+
const response = await client.send(
|
|
402
|
+
new ListObjectsV2Command({
|
|
403
|
+
Bucket: bucket,
|
|
404
|
+
Prefix: dirPrefix,
|
|
405
|
+
Delimiter: "/",
|
|
406
|
+
ContinuationToken: continuationToken
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
for (const item of response.CommonPrefixes ?? []) {
|
|
410
|
+
if (item.Prefix) results.push(item.Prefix);
|
|
411
|
+
}
|
|
412
|
+
continuationToken = response.NextContinuationToken;
|
|
413
|
+
} while (continuationToken);
|
|
414
|
+
return results;
|
|
415
|
+
},
|
|
416
|
+
async allDirectories(directory) {
|
|
417
|
+
const allKeys = await this.allFiles(directory);
|
|
418
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
419
|
+
for (const key of allKeys) {
|
|
420
|
+
const parts = key.split("/");
|
|
421
|
+
for (let i = 1; i < parts.length; i++) {
|
|
422
|
+
dirs.add(parts.slice(0, i).join("/") + "/");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return [...dirs].sort();
|
|
426
|
+
},
|
|
427
|
+
async makeDirectory() {
|
|
428
|
+
},
|
|
429
|
+
async removeDirectory(directory) {
|
|
430
|
+
const keys = await this.allFiles(directory);
|
|
431
|
+
if (keys.length === 0) return;
|
|
432
|
+
for (let i = 0; i < keys.length; i += 1e3) {
|
|
433
|
+
const batch = keys.slice(i, i + 1e3);
|
|
434
|
+
await client.send(
|
|
435
|
+
new DeleteObjectsCommand({
|
|
436
|
+
Bucket: bucket,
|
|
437
|
+
Delete: { Objects: batch.map((key) => ({ Key: key })) }
|
|
438
|
+
})
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function resolveKey(prefix, path) {
|
|
445
|
+
const normalized = prefix ? `${prefix.replace(/\/$/, "")}/${path.replace(/^\//, "")}` : path.replace(/^\//, "");
|
|
446
|
+
return normalized;
|
|
447
|
+
}
|
|
448
|
+
function expirationToSeconds(expiration) {
|
|
449
|
+
const expiresAt = new Date(expiration).getTime();
|
|
450
|
+
const now = Date.now();
|
|
451
|
+
return Math.max(1, Math.floor((expiresAt - now) / 1e3));
|
|
452
|
+
}
|
|
453
|
+
async function toBuffer(contents) {
|
|
454
|
+
if (Buffer.isBuffer(contents)) return contents;
|
|
455
|
+
if (typeof contents === "string") return Buffer.from(contents);
|
|
456
|
+
if (contents instanceof ReadableStream2) {
|
|
457
|
+
const chunks2 = [];
|
|
458
|
+
const reader = contents.getReader();
|
|
459
|
+
while (true) {
|
|
460
|
+
const { done, value } = await reader.read();
|
|
461
|
+
if (done) break;
|
|
462
|
+
chunks2.push(value);
|
|
463
|
+
}
|
|
464
|
+
return Buffer.concat(chunks2);
|
|
465
|
+
}
|
|
466
|
+
const chunks = [];
|
|
467
|
+
for await (const chunk of contents) {
|
|
468
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
469
|
+
}
|
|
470
|
+
return Buffer.concat(chunks);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/index.ts
|
|
474
|
+
function create(config) {
|
|
475
|
+
const driver = config.driver;
|
|
476
|
+
async function get(path2) {
|
|
477
|
+
return await driver.get(path2);
|
|
478
|
+
}
|
|
479
|
+
async function text(path2, encoding) {
|
|
480
|
+
const contents = await driver.get(path2);
|
|
481
|
+
return contents.toString(encoding);
|
|
482
|
+
}
|
|
483
|
+
async function json(path2, encoding) {
|
|
484
|
+
const contents = await driver.get(path2);
|
|
485
|
+
return JSON.parse(contents.toString(encoding));
|
|
486
|
+
}
|
|
487
|
+
async function exists(path2) {
|
|
488
|
+
return await driver.exists(path2);
|
|
489
|
+
}
|
|
490
|
+
async function missing(path2) {
|
|
491
|
+
return !await driver.exists(path2);
|
|
492
|
+
}
|
|
493
|
+
async function download(path2, filename, headers) {
|
|
494
|
+
const contents = await driver.get(path2);
|
|
495
|
+
const parsed = parsePath(path2);
|
|
496
|
+
const dispositionFilename = filename !== void 0 ? filename : `${parsed.name}${parsed.ext}`;
|
|
497
|
+
return new Response(contents, {
|
|
498
|
+
status: 200,
|
|
499
|
+
headers: combineHeaders({ "Content-Disposition": `inline; filename="${dispositionFilename}"` }, headers)
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
function url(path2) {
|
|
503
|
+
return driver.url(path2);
|
|
504
|
+
}
|
|
505
|
+
async function temporaryUrl(path2, expires, params) {
|
|
506
|
+
return await driver.temporaryUrl(path2, expires, params);
|
|
507
|
+
}
|
|
508
|
+
async function temporaryUploadUrl(path2, expires) {
|
|
509
|
+
return await driver.temporaryUploadUrl(path2, expires);
|
|
510
|
+
}
|
|
511
|
+
async function size(path2) {
|
|
512
|
+
return await driver.size(path2);
|
|
513
|
+
}
|
|
514
|
+
async function lastModified(path2) {
|
|
515
|
+
return await driver.lastModified(path2);
|
|
516
|
+
}
|
|
517
|
+
async function mimeType(path2) {
|
|
518
|
+
return await driver.mimeType(path2);
|
|
519
|
+
}
|
|
520
|
+
function path(path2) {
|
|
521
|
+
return driver.path(path2);
|
|
522
|
+
}
|
|
523
|
+
async function put(path2, contents) {
|
|
524
|
+
await driver.put(path2, contents);
|
|
525
|
+
}
|
|
526
|
+
async function prepend2(path2, contents) {
|
|
527
|
+
await driver.prepend(path2, contents);
|
|
528
|
+
}
|
|
529
|
+
async function append(path2, contents) {
|
|
530
|
+
await driver.append(path2, contents);
|
|
531
|
+
}
|
|
532
|
+
async function copy(source, destination) {
|
|
533
|
+
await driver.copy(source, destination);
|
|
534
|
+
}
|
|
535
|
+
async function move(source, destination) {
|
|
536
|
+
await driver.move(source, destination);
|
|
537
|
+
}
|
|
538
|
+
async function putFileAs(dir, file, filename) {
|
|
539
|
+
const filePath = joinPath(dir, filename);
|
|
540
|
+
await driver.put(filePath, file.stream());
|
|
541
|
+
return filePath;
|
|
542
|
+
}
|
|
543
|
+
async function putFile(dir, file) {
|
|
544
|
+
const parsed = parsePath(file.name);
|
|
545
|
+
const ext = mimeTypes.extension(file.type);
|
|
546
|
+
const name = `${sanitizeFilename(parsed.name)}-${randomUUID2()}${ext ? `.${ext}` : ""}`;
|
|
547
|
+
return await putFileAs(dir, file, name);
|
|
548
|
+
}
|
|
549
|
+
async function remove(...paths) {
|
|
550
|
+
await driver.remove(...paths);
|
|
551
|
+
}
|
|
552
|
+
async function files(directory) {
|
|
553
|
+
return await driver.files(directory);
|
|
554
|
+
}
|
|
555
|
+
async function allFiles(directory) {
|
|
556
|
+
return await driver.allFiles(directory);
|
|
557
|
+
}
|
|
558
|
+
async function directories(directory) {
|
|
559
|
+
return await driver.directories(directory);
|
|
560
|
+
}
|
|
561
|
+
async function allDirectories(directory) {
|
|
562
|
+
return await driver.allDirectories(directory);
|
|
563
|
+
}
|
|
564
|
+
async function makeDirectory(directory) {
|
|
565
|
+
await driver.makeDirectory(directory);
|
|
566
|
+
}
|
|
567
|
+
async function removeDirectory(directory) {
|
|
568
|
+
await driver.removeDirectory(directory);
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
get,
|
|
572
|
+
text,
|
|
573
|
+
json,
|
|
574
|
+
exists,
|
|
575
|
+
missing,
|
|
576
|
+
download,
|
|
577
|
+
url,
|
|
578
|
+
temporaryUrl,
|
|
579
|
+
temporaryUploadUrl,
|
|
580
|
+
size,
|
|
581
|
+
lastModified,
|
|
582
|
+
mimeType,
|
|
583
|
+
path,
|
|
584
|
+
put,
|
|
585
|
+
prepend: prepend2,
|
|
586
|
+
append,
|
|
587
|
+
copy,
|
|
588
|
+
move,
|
|
589
|
+
putFileAs,
|
|
590
|
+
putFile,
|
|
591
|
+
remove,
|
|
592
|
+
files,
|
|
593
|
+
allFiles,
|
|
594
|
+
directories,
|
|
595
|
+
allDirectories,
|
|
596
|
+
makeDirectory,
|
|
597
|
+
removeDirectory
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
var registry = ServiceRegistry.createRegistry(
|
|
601
|
+
ServiceRegistry.proxyFunctionsForObject
|
|
602
|
+
);
|
|
603
|
+
var Provider = {
|
|
604
|
+
create,
|
|
605
|
+
register: registry.register
|
|
606
|
+
};
|
|
607
|
+
function provider(key) {
|
|
608
|
+
const service = registry.resolve(key);
|
|
609
|
+
return {
|
|
610
|
+
get: service.get,
|
|
611
|
+
text: service.text,
|
|
612
|
+
json: service.json,
|
|
613
|
+
exists: service.exists,
|
|
614
|
+
missing: service.missing,
|
|
615
|
+
download: service.download,
|
|
616
|
+
url: service.url,
|
|
617
|
+
temporaryUrl: service.temporaryUrl,
|
|
618
|
+
temporaryUploadUrl: service.temporaryUploadUrl,
|
|
619
|
+
size: service.size,
|
|
620
|
+
lastModified: service.lastModified,
|
|
621
|
+
mimeType: service.mimeType,
|
|
622
|
+
path: service.path,
|
|
623
|
+
put: service.put,
|
|
624
|
+
prepend: service.prepend,
|
|
625
|
+
append: service.append,
|
|
626
|
+
copy: service.copy,
|
|
627
|
+
move: service.move,
|
|
628
|
+
putFileAs: service.putFileAs,
|
|
629
|
+
putFile: service.putFile,
|
|
630
|
+
remove: service.remove,
|
|
631
|
+
files: service.files,
|
|
632
|
+
allFiles: service.allFiles,
|
|
633
|
+
directories: service.directories,
|
|
634
|
+
allDirectories: service.allDirectories,
|
|
635
|
+
makeDirectory: service.makeDirectory,
|
|
636
|
+
removeDirectory: service.removeDirectory
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
var FileStorage = {
|
|
640
|
+
Provider,
|
|
641
|
+
...provider("default"),
|
|
642
|
+
provider,
|
|
643
|
+
drivers: {
|
|
644
|
+
local: createLocalDriver,
|
|
645
|
+
s3: createS3Driver
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
function combineHeaders(base, extra) {
|
|
649
|
+
const merged = new Headers(base);
|
|
650
|
+
if (extra) {
|
|
651
|
+
new Headers(extra).forEach((value, key) => {
|
|
652
|
+
merged.set(key, value);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return merged;
|
|
656
|
+
}
|
|
657
|
+
export {
|
|
658
|
+
FileStorage
|
|
659
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maestro-js/file-storage",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"default": "./dist/index.js"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"version": "1.0.0-alpha.0",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "restricted"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@aws-sdk/client-s3": "^3.750.0",
|
|
19
|
+
"@aws-sdk/s3-request-presigner": "^3.750.0",
|
|
20
|
+
"iso-fns2": "npm:iso-fns@2.0.0-alpha.26",
|
|
21
|
+
"mime-types": "^2.1.35",
|
|
22
|
+
"@maestro-js/service-registry": "1.0.0-alpha.0",
|
|
23
|
+
"@maestro-js/crypt": "1.0.0-alpha.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.19.11",
|
|
27
|
+
"@types/mime-types": "^2.1.4"
|
|
28
|
+
},
|
|
29
|
+
"license": "UNLICENSED",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.18.0"
|
|
32
|
+
},
|
|
33
|
+
"repository": "https://github.com/Marcato-Partners/maestro-js",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup --config ../../tsup.config.ts",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"format": "prettier --write src/",
|
|
38
|
+
"lint": "prettier --check src/"
|
|
39
|
+
}
|
|
40
|
+
}
|