@ziioapp/os-features 0.1.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 +22 -0
- package/README.md +20 -0
- package/dist/files/object-url.d.ts +17 -0
- package/dist/files/object-url.js +21 -0
- package/dist/files/text-file.d.ts +27 -0
- package/dist/files/text-file.js +58 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/opfs/index.d.ts +4 -0
- package/dist/opfs/index.js +4 -0
- package/dist/opfs/io.d.ts +42 -0
- package/dist/opfs/io.js +156 -0
- package/dist/opfs/layout.d.ts +50 -0
- package/dist/opfs/layout.js +98 -0
- package/dist/opfs/path.d.ts +8 -0
- package/dist/opfs/path.js +52 -0
- package/dist/opfs/volume.d.ts +19 -0
- package/dist/opfs/volume.js +42 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ziioai
|
|
4
|
+
Copyright (c) 2023 shadcn
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @ziioapp/os-features
|
|
2
|
+
|
|
3
|
+
浏览器文件工具与 ZiioApp OPFS 目录协议。当前包含文本编码识别、对象 URL 生命周期辅助、OPFS 路径与读写操作,以及 ZiioApp 应用目录布局。
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { decodeTextFile } from "@ziioapp/os-features";
|
|
7
|
+
import {
|
|
8
|
+
ensureZiioOpfsLayout,
|
|
9
|
+
getZiioOpfsAppPaths,
|
|
10
|
+
readOpfsTextFile,
|
|
11
|
+
} from "@ziioapp/os-features/opfs";
|
|
12
|
+
|
|
13
|
+
await ensureZiioOpfsLayout({ appIds: ["my-app"] });
|
|
14
|
+
const paths = getZiioOpfsAppPaths("my-app");
|
|
15
|
+
const text = await readOpfsTextFile(`${paths.appData}/notes.txt`);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
OPFS 操作需要支持 `navigator.storage.getDirectory()` 的浏览器环境;不提供 Node.js 文件系统或 Electron 主进程实现。OPFS 数据按浏览器 origin/存储分区隔离,安装同一包不会使不同应用自动共享文件。
|
|
19
|
+
|
|
20
|
+
`/.ziio`、`/users`、`/apps` 等路径及 `ziioOpfsLayoutVersion` 是持久化协议的一部分,后续修改须考虑已有数据迁移。开发时可使用根入口和 `/files/*`、`/opfs`、`/opfs/*` 子路径;发布产物提供对应的 JavaScript 与类型声明。
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ObjectUrlFileEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
idx: number;
|
|
4
|
+
name: string;
|
|
5
|
+
size: number;
|
|
6
|
+
lastModified: number;
|
|
7
|
+
type: string;
|
|
8
|
+
file: File;
|
|
9
|
+
url: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CreateObjectUrlFileEntriesOptions {
|
|
12
|
+
startIndex?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function createObjectUrlFileEntries(files: File[], options?: CreateObjectUrlFileEntriesOptions): ObjectUrlFileEntry[];
|
|
15
|
+
export declare function revokeObjectUrls(items: Iterable<{
|
|
16
|
+
url: string;
|
|
17
|
+
} | string>): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function createObjectUrlFileEntries(files, options = {}) {
|
|
2
|
+
const startIndex = options.startIndex ?? 0;
|
|
3
|
+
return files.map((file, index) => {
|
|
4
|
+
const idx = startIndex + index;
|
|
5
|
+
return {
|
|
6
|
+
id: `${file.name}:${file.lastModified}:${idx}`,
|
|
7
|
+
idx,
|
|
8
|
+
name: file.name,
|
|
9
|
+
size: file.size,
|
|
10
|
+
lastModified: file.lastModified,
|
|
11
|
+
type: file.type,
|
|
12
|
+
file,
|
|
13
|
+
url: URL.createObjectURL(file),
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function revokeObjectUrls(items) {
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
URL.revokeObjectURL(typeof item === "string" ? item : item.url);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { detect } from "jschardet";
|
|
2
|
+
export interface TextEncodingOptions {
|
|
3
|
+
sampleSize?: number;
|
|
4
|
+
defaultEncoding?: string;
|
|
5
|
+
utf8Confidence?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface TextEncodingDetection {
|
|
8
|
+
encoding: string;
|
|
9
|
+
confidence: number;
|
|
10
|
+
rawEncoding?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DecodedTextFile {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
size: number;
|
|
16
|
+
lastModified: number;
|
|
17
|
+
bytes: Uint8Array;
|
|
18
|
+
text: string;
|
|
19
|
+
encoding: string;
|
|
20
|
+
confidence: number;
|
|
21
|
+
detectedEncoding: string;
|
|
22
|
+
rawEncoding?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function readFileBytes(file: File): Promise<Uint8Array>;
|
|
25
|
+
export declare function detectTextEncoding(bytes: Uint8Array, options?: TextEncodingOptions): TextEncodingDetection;
|
|
26
|
+
export declare function decodeText(bytes: Uint8Array, encoding: string): string;
|
|
27
|
+
export declare function decodeTextFile(file: File, options?: TextEncodingOptions): Promise<DecodedTextFile>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { detect as detectCharset } from "jschardet";
|
|
2
|
+
export { detect } from "jschardet";
|
|
3
|
+
const defaultSampleSize = 256 * 1024;
|
|
4
|
+
export async function readFileBytes(file) {
|
|
5
|
+
return new Uint8Array(await file.arrayBuffer());
|
|
6
|
+
}
|
|
7
|
+
export function detectTextEncoding(bytes, options = {}) {
|
|
8
|
+
const sampleSize = options.sampleSize ?? defaultSampleSize;
|
|
9
|
+
const sample = binaryString(bytes.slice(0, Math.min(bytes.length, sampleSize)));
|
|
10
|
+
const detected = detectCharset(sample);
|
|
11
|
+
const encoding = normalizeEncoding(detected.encoding, detected.confidence, options);
|
|
12
|
+
return {
|
|
13
|
+
encoding,
|
|
14
|
+
confidence: detected.confidence ?? 0,
|
|
15
|
+
rawEncoding: detected.encoding,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function decodeText(bytes, encoding) {
|
|
19
|
+
try {
|
|
20
|
+
return new TextDecoder(encoding).decode(bytes);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function decodeTextFile(file, options = {}) {
|
|
27
|
+
const bytes = await readFileBytes(file);
|
|
28
|
+
const detection = detectTextEncoding(bytes, options);
|
|
29
|
+
return {
|
|
30
|
+
id: `${file.name}:${file.size}:${file.lastModified}`,
|
|
31
|
+
name: file.name,
|
|
32
|
+
size: file.size,
|
|
33
|
+
lastModified: file.lastModified,
|
|
34
|
+
bytes,
|
|
35
|
+
text: decodeText(bytes, detection.encoding),
|
|
36
|
+
encoding: detection.encoding,
|
|
37
|
+
confidence: detection.confidence,
|
|
38
|
+
detectedEncoding: detection.encoding,
|
|
39
|
+
rawEncoding: detection.rawEncoding,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function normalizeEncoding(encoding, confidence = 0, options = {}) {
|
|
43
|
+
const value = (encoding ?? "").toLowerCase();
|
|
44
|
+
const utf8Confidence = options.utf8Confidence ?? 0.6;
|
|
45
|
+
if (value.includes("utf") && confidence >= utf8Confidence)
|
|
46
|
+
return "utf-8";
|
|
47
|
+
if (value.includes("gb18030"))
|
|
48
|
+
return "gb18030";
|
|
49
|
+
if (value.includes("gb") || value.includes("big5"))
|
|
50
|
+
return value;
|
|
51
|
+
return options.defaultEncoding ?? "gbk";
|
|
52
|
+
}
|
|
53
|
+
function binaryString(bytes) {
|
|
54
|
+
let output = "";
|
|
55
|
+
for (const byte of bytes)
|
|
56
|
+
output += String.fromCharCode(byte);
|
|
57
|
+
return output;
|
|
58
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { CreateObjectUrlFileEntriesOptions, ObjectUrlFileEntry, } from "./files/object-url.js";
|
|
2
|
+
export { createObjectUrlFileEntries, revokeObjectUrls, } from "./files/object-url.js";
|
|
3
|
+
export type { DecodedTextFile, TextEncodingDetection, TextEncodingOptions, } from "./files/text-file.js";
|
|
4
|
+
export { decodeText, decodeTextFile, detect, detectTextEncoding, readFileBytes, } from "./files/text-file.js";
|
|
5
|
+
export type { EnsureZiioOpfsLayoutOptions, EnsureZiioOpfsLayoutResult, OpfsWriteOptions, OpfsWriteResult, OpfsWriteStatus, ZiioOpfsAppPaths, ZiioOpfsFsRegistry, ZiioOpfsSharedKind, ZiioOpfsUserPaths, } from "./opfs/index.js";
|
|
6
|
+
export { assertZiioOpfsName, defaultZiioOpfsUserId, ensureOpfsDirectories, ensureOpfsDirectory, ensureZiioOpfsLayout, getOpfsDirectoryHandle, getOpfsRootDirectory, getZiioOpfsAppDirs, getZiioOpfsAppPaths, getZiioOpfsBaseDirs, getZiioOpfsUserPaths, joinOpfsPath, joinRelativeOpfsPath, normalizeOpfsPath, normalizeRelativeOpfsPath, opfsBasename, opfsDirectoryExists, opfsDirname, opfsFileExists, readOpfsFile, readOpfsJsonFile, readOpfsTextFile, splitOpfsPath, stripOpfsPrefix, writeOpfsFile, writeOpfsJsonFile, writeOpfsTextFile, ziioOpfsLayoutVersion, ziioOpfsSharedKinds, ziioOpfsSystemPaths, } from "./opfs/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createObjectUrlFileEntries, revokeObjectUrls, } from "./files/object-url.js";
|
|
2
|
+
export { decodeText, decodeTextFile, detect, detectTextEncoding, readFileBytes, } from "./files/text-file.js";
|
|
3
|
+
export { assertZiioOpfsName, defaultZiioOpfsUserId, ensureOpfsDirectories, ensureOpfsDirectory, ensureZiioOpfsLayout, getOpfsDirectoryHandle, getOpfsRootDirectory, getZiioOpfsAppDirs, getZiioOpfsAppPaths, getZiioOpfsBaseDirs, getZiioOpfsUserPaths, joinOpfsPath, joinRelativeOpfsPath, normalizeOpfsPath, normalizeRelativeOpfsPath, opfsBasename, opfsDirectoryExists, opfsDirname, opfsFileExists, readOpfsFile, readOpfsJsonFile, readOpfsTextFile, splitOpfsPath, stripOpfsPrefix, writeOpfsFile, writeOpfsJsonFile, writeOpfsTextFile, ziioOpfsLayoutVersion, ziioOpfsSharedKinds, ziioOpfsSystemPaths, } from "./opfs/index.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { appendOpfsFile, appendOpfsTextFile, ensureOpfsDirectories, ensureOpfsDirectory, getOpfsDirectoryHandle, getOpfsRootDirectory, listOpfsDirectory, type OpfsEntryInfo, type OpfsRemoveOptions, type OpfsWriteOptions, type OpfsWriteResult, type OpfsWriteStatus, opfsDirectoryExists, opfsFileExists, readOpfsFile, readOpfsJsonFile, readOpfsTextFile, readOpfsTextLines, removeOpfsEntry, writeOpfsFile, writeOpfsJsonFile, writeOpfsTextFile, } from "./io.js";
|
|
2
|
+
export { assertZiioOpfsName, defaultZiioOpfsUserId, getZiioOpfsAppDirs, getZiioOpfsAppPaths, getZiioOpfsBaseDirs, getZiioOpfsUserPaths, type ZiioOpfsAppPaths, type ZiioOpfsSharedKind, type ZiioOpfsUserPaths, ziioOpfsLayoutVersion, ziioOpfsSharedKinds, ziioOpfsSystemPaths, } from "./layout.js";
|
|
3
|
+
export { joinOpfsPath, joinRelativeOpfsPath, normalizeOpfsPath, normalizeRelativeOpfsPath, opfsBasename, opfsDirname, splitOpfsPath, stripOpfsPrefix, } from "./path.js";
|
|
4
|
+
export { type EnsureZiioOpfsLayoutOptions, type EnsureZiioOpfsLayoutResult, ensureZiioOpfsLayout, type ZiioOpfsFsRegistry, } from "./volume.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { appendOpfsFile, appendOpfsTextFile, ensureOpfsDirectories, ensureOpfsDirectory, getOpfsDirectoryHandle, getOpfsRootDirectory, listOpfsDirectory, opfsDirectoryExists, opfsFileExists, readOpfsFile, readOpfsJsonFile, readOpfsTextFile, readOpfsTextLines, removeOpfsEntry, writeOpfsFile, writeOpfsJsonFile, writeOpfsTextFile, } from "./io.js";
|
|
2
|
+
export { assertZiioOpfsName, defaultZiioOpfsUserId, getZiioOpfsAppDirs, getZiioOpfsAppPaths, getZiioOpfsBaseDirs, getZiioOpfsUserPaths, ziioOpfsLayoutVersion, ziioOpfsSharedKinds, ziioOpfsSystemPaths, } from "./layout.js";
|
|
3
|
+
export { joinOpfsPath, joinRelativeOpfsPath, normalizeOpfsPath, normalizeRelativeOpfsPath, opfsBasename, opfsDirname, splitOpfsPath, stripOpfsPrefix, } from "./path.js";
|
|
4
|
+
export { ensureZiioOpfsLayout, } from "./volume.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface OpfsWriteOptions {
|
|
2
|
+
createParents?: boolean;
|
|
3
|
+
overwrite?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface OpfsEntryInfo {
|
|
6
|
+
kind: "directory" | "file";
|
|
7
|
+
lastModified: number;
|
|
8
|
+
name: string;
|
|
9
|
+
path: string;
|
|
10
|
+
size: number;
|
|
11
|
+
type: string;
|
|
12
|
+
}
|
|
13
|
+
export interface OpfsRemoveOptions {
|
|
14
|
+
recursive?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export type OpfsWriteStatus = "created" | "skipped" | "updated";
|
|
17
|
+
export interface OpfsWriteResult {
|
|
18
|
+
path: string;
|
|
19
|
+
status: OpfsWriteStatus;
|
|
20
|
+
}
|
|
21
|
+
export declare function getOpfsRootDirectory(): Promise<FileSystemDirectoryHandle>;
|
|
22
|
+
export declare function getOpfsDirectoryHandle(path: string, options?: {
|
|
23
|
+
create?: boolean;
|
|
24
|
+
root?: FileSystemDirectoryHandle;
|
|
25
|
+
}): Promise<FileSystemDirectoryHandle>;
|
|
26
|
+
export declare function ensureOpfsDirectory(path: string): Promise<string>;
|
|
27
|
+
export declare function ensureOpfsDirectories(paths: Iterable<string>): Promise<string[]>;
|
|
28
|
+
export declare function opfsDirectoryExists(path: string): Promise<boolean>;
|
|
29
|
+
export declare function opfsFileExists(path: string): Promise<boolean>;
|
|
30
|
+
export declare function readOpfsFile(path: string): Promise<File>;
|
|
31
|
+
export declare function readOpfsTextFile(path: string): Promise<string>;
|
|
32
|
+
export declare function readOpfsTextLines(path: string, options?: {
|
|
33
|
+
maxLines?: number;
|
|
34
|
+
}): Promise<string[]>;
|
|
35
|
+
export declare function readOpfsJsonFile<T = unknown>(path: string): Promise<T>;
|
|
36
|
+
export declare function writeOpfsFile(path: string, content: BlobPart | BlobPart[], options?: OpfsWriteOptions): Promise<OpfsWriteResult>;
|
|
37
|
+
export declare function writeOpfsTextFile(path: string, text: string, options?: OpfsWriteOptions): Promise<OpfsWriteResult>;
|
|
38
|
+
export declare function appendOpfsFile(path: string, content: BlobPart | BlobPart[], options?: Pick<OpfsWriteOptions, "createParents">): Promise<OpfsWriteResult>;
|
|
39
|
+
export declare function appendOpfsTextFile(path: string, text: string, options?: Pick<OpfsWriteOptions, "createParents">): Promise<OpfsWriteResult>;
|
|
40
|
+
export declare function writeOpfsJsonFile(path: string, value: unknown, options?: OpfsWriteOptions): Promise<OpfsWriteResult>;
|
|
41
|
+
export declare function listOpfsDirectory(path: string): Promise<OpfsEntryInfo[]>;
|
|
42
|
+
export declare function removeOpfsEntry(path: string, options?: OpfsRemoveOptions): Promise<string>;
|
package/dist/opfs/io.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { joinOpfsPath, normalizeOpfsPath, opfsBasename, opfsDirname, splitOpfsPath, } from "./path.js";
|
|
2
|
+
export async function getOpfsRootDirectory() {
|
|
3
|
+
if (!globalThis.navigator?.storage?.getDirectory) {
|
|
4
|
+
throw new Error("OPFS is not available in this environment.");
|
|
5
|
+
}
|
|
6
|
+
return navigator.storage.getDirectory();
|
|
7
|
+
}
|
|
8
|
+
export async function getOpfsDirectoryHandle(path, options = {}) {
|
|
9
|
+
const segments = splitOpfsPath(path);
|
|
10
|
+
let current = options.root ?? (await getOpfsRootDirectory());
|
|
11
|
+
for (const segment of segments) {
|
|
12
|
+
current = await current.getDirectoryHandle(segment, {
|
|
13
|
+
create: options.create,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return current;
|
|
17
|
+
}
|
|
18
|
+
export async function ensureOpfsDirectory(path) {
|
|
19
|
+
const normalizedPath = normalizeOpfsPath(path);
|
|
20
|
+
await getOpfsDirectoryHandle(normalizedPath, { create: true });
|
|
21
|
+
return normalizedPath;
|
|
22
|
+
}
|
|
23
|
+
export async function ensureOpfsDirectories(paths) {
|
|
24
|
+
const ensured = [];
|
|
25
|
+
for (const path of paths) {
|
|
26
|
+
ensured.push(await ensureOpfsDirectory(path));
|
|
27
|
+
}
|
|
28
|
+
return ensured;
|
|
29
|
+
}
|
|
30
|
+
export async function opfsDirectoryExists(path) {
|
|
31
|
+
try {
|
|
32
|
+
await getOpfsDirectoryHandle(path);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function opfsFileExists(path) {
|
|
40
|
+
try {
|
|
41
|
+
const parent = await getOpfsDirectoryHandle(opfsDirname(path));
|
|
42
|
+
await parent.getFileHandle(opfsBasename(path));
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function readOpfsFile(path) {
|
|
50
|
+
const parent = await getOpfsDirectoryHandle(opfsDirname(path));
|
|
51
|
+
const handle = await parent.getFileHandle(opfsBasename(path));
|
|
52
|
+
return handle.getFile();
|
|
53
|
+
}
|
|
54
|
+
export async function readOpfsTextFile(path) {
|
|
55
|
+
return (await readOpfsFile(path)).text();
|
|
56
|
+
}
|
|
57
|
+
export async function readOpfsTextLines(path, options = {}) {
|
|
58
|
+
const lines = (await readOpfsTextFile(path)).split(/\r?\n/);
|
|
59
|
+
return options.maxLines === undefined
|
|
60
|
+
? lines
|
|
61
|
+
: lines.slice(0, options.maxLines);
|
|
62
|
+
}
|
|
63
|
+
export async function readOpfsJsonFile(path) {
|
|
64
|
+
return JSON.parse(await readOpfsTextFile(path));
|
|
65
|
+
}
|
|
66
|
+
export async function writeOpfsFile(path, content, options = {}) {
|
|
67
|
+
const normalizedPath = normalizeOpfsPath(path);
|
|
68
|
+
const overwrite = options.overwrite ?? true;
|
|
69
|
+
const exists = await opfsFileExists(normalizedPath);
|
|
70
|
+
if (exists && !overwrite) {
|
|
71
|
+
return { path: normalizedPath, status: "skipped" };
|
|
72
|
+
}
|
|
73
|
+
const parentPath = opfsDirname(normalizedPath);
|
|
74
|
+
const parent = options.createParents === false
|
|
75
|
+
? await getOpfsDirectoryHandle(parentPath)
|
|
76
|
+
: await getOpfsDirectoryHandle(parentPath, { create: true });
|
|
77
|
+
const handle = await parent.getFileHandle(opfsBasename(normalizedPath), {
|
|
78
|
+
create: true,
|
|
79
|
+
});
|
|
80
|
+
const writable = await handle.createWritable();
|
|
81
|
+
await writable.write(Array.isArray(content) ? new Blob(content) : content);
|
|
82
|
+
await writable.close();
|
|
83
|
+
return { path: normalizedPath, status: exists ? "updated" : "created" };
|
|
84
|
+
}
|
|
85
|
+
export async function writeOpfsTextFile(path, text, options = {}) {
|
|
86
|
+
return writeOpfsFile(path, text, options);
|
|
87
|
+
}
|
|
88
|
+
export async function appendOpfsFile(path, content, options = {}) {
|
|
89
|
+
const normalizedPath = normalizeOpfsPath(path);
|
|
90
|
+
const exists = await opfsFileExists(normalizedPath);
|
|
91
|
+
const parentPath = opfsDirname(normalizedPath);
|
|
92
|
+
const parent = options.createParents === false
|
|
93
|
+
? await getOpfsDirectoryHandle(parentPath)
|
|
94
|
+
: await getOpfsDirectoryHandle(parentPath, { create: true });
|
|
95
|
+
const handle = await parent.getFileHandle(opfsBasename(normalizedPath), {
|
|
96
|
+
create: true,
|
|
97
|
+
});
|
|
98
|
+
const writable = await handle.createWritable({ keepExistingData: true });
|
|
99
|
+
if (exists) {
|
|
100
|
+
const file = await handle.getFile();
|
|
101
|
+
await writable.seek(file.size);
|
|
102
|
+
}
|
|
103
|
+
await writable.write(Array.isArray(content) ? new Blob(content) : content);
|
|
104
|
+
await writable.close();
|
|
105
|
+
return { path: normalizedPath, status: exists ? "updated" : "created" };
|
|
106
|
+
}
|
|
107
|
+
export async function appendOpfsTextFile(path, text, options = {}) {
|
|
108
|
+
return appendOpfsFile(path, text, options);
|
|
109
|
+
}
|
|
110
|
+
export async function writeOpfsJsonFile(path, value, options = {}) {
|
|
111
|
+
return writeOpfsTextFile(path, `${JSON.stringify(value, null, 2)}\n`, options);
|
|
112
|
+
}
|
|
113
|
+
export async function listOpfsDirectory(path) {
|
|
114
|
+
const normalizedPath = normalizeOpfsPath(path);
|
|
115
|
+
const directory = await getOpfsDirectoryHandle(normalizedPath);
|
|
116
|
+
const entries = [];
|
|
117
|
+
for await (const [name, handle] of directory.entries()) {
|
|
118
|
+
const childPath = joinOpfsPath(normalizedPath, name);
|
|
119
|
+
if (handle.kind === "directory") {
|
|
120
|
+
entries.push({
|
|
121
|
+
kind: "directory",
|
|
122
|
+
lastModified: 0,
|
|
123
|
+
name,
|
|
124
|
+
path: childPath,
|
|
125
|
+
size: 0,
|
|
126
|
+
type: "",
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const fileHandle = handle;
|
|
131
|
+
const file = await fileHandle.getFile();
|
|
132
|
+
entries.push({
|
|
133
|
+
kind: "file",
|
|
134
|
+
lastModified: file.lastModified,
|
|
135
|
+
name,
|
|
136
|
+
path: childPath,
|
|
137
|
+
size: file.size,
|
|
138
|
+
type: file.type,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return entries.sort((a, b) => {
|
|
142
|
+
if (a.kind !== b.kind)
|
|
143
|
+
return a.kind === "directory" ? -1 : 1;
|
|
144
|
+
return a.name.localeCompare(b.name);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
export async function removeOpfsEntry(path, options = {}) {
|
|
148
|
+
const normalizedPath = normalizeOpfsPath(path);
|
|
149
|
+
const name = opfsBasename(normalizedPath);
|
|
150
|
+
if (!name) {
|
|
151
|
+
throw new Error("Cannot remove OPFS root directory.");
|
|
152
|
+
}
|
|
153
|
+
const parent = await getOpfsDirectoryHandle(opfsDirname(normalizedPath));
|
|
154
|
+
await parent.removeEntry(name, { recursive: options.recursive ?? false });
|
|
155
|
+
return normalizedPath;
|
|
156
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const ziioOpfsLayoutVersion = 1;
|
|
2
|
+
export declare const defaultZiioOpfsUserId = "default";
|
|
3
|
+
export declare const ziioOpfsSharedKinds: readonly ["skills", "functions", "notes", "mcp", "libs"];
|
|
4
|
+
export type ZiioOpfsSharedKind = (typeof ziioOpfsSharedKinds)[number];
|
|
5
|
+
export interface ZiioOpfsUserPaths {
|
|
6
|
+
userId: string;
|
|
7
|
+
userRoot: string;
|
|
8
|
+
home: string;
|
|
9
|
+
documents: string;
|
|
10
|
+
downloads: string;
|
|
11
|
+
workspace: string;
|
|
12
|
+
appDataRoot: string;
|
|
13
|
+
cacheRoot: string;
|
|
14
|
+
tmpRoot: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ZiioOpfsAppPaths extends ZiioOpfsUserPaths {
|
|
17
|
+
appId: string;
|
|
18
|
+
appInstall: string;
|
|
19
|
+
appData: string;
|
|
20
|
+
appCache: string;
|
|
21
|
+
appTmp: string;
|
|
22
|
+
sharedRoot: string;
|
|
23
|
+
sharedSkills: string;
|
|
24
|
+
sharedFunctions: string;
|
|
25
|
+
sharedNotes: string;
|
|
26
|
+
sharedMcp: string;
|
|
27
|
+
sharedLibs: string;
|
|
28
|
+
}
|
|
29
|
+
export declare const ziioOpfsSystemPaths: {
|
|
30
|
+
readonly systemRoot: "/.ziio";
|
|
31
|
+
readonly fsRegistry: "/.ziio/fs.json";
|
|
32
|
+
readonly appsRegistry: "/.ziio/apps.json";
|
|
33
|
+
readonly usersRegistry: "/.ziio/users.json";
|
|
34
|
+
readonly migrations: "/.ziio/migrations";
|
|
35
|
+
readonly trash: "/.ziio/trash";
|
|
36
|
+
readonly tmp: "/.ziio/tmp";
|
|
37
|
+
};
|
|
38
|
+
export declare function assertZiioOpfsName(name: string, label?: string): string;
|
|
39
|
+
export declare function getZiioOpfsUserPaths(userId?: string): ZiioOpfsUserPaths;
|
|
40
|
+
export declare function getZiioOpfsAppPaths(appId: string, options?: {
|
|
41
|
+
userId?: string;
|
|
42
|
+
}): ZiioOpfsAppPaths;
|
|
43
|
+
export declare function getZiioOpfsBaseDirs(options?: {
|
|
44
|
+
userId?: string;
|
|
45
|
+
sharedKinds?: readonly ZiioOpfsSharedKind[];
|
|
46
|
+
}): string[];
|
|
47
|
+
export declare function getZiioOpfsAppDirs(appId: string, options?: {
|
|
48
|
+
userId?: string;
|
|
49
|
+
sharedKinds?: readonly ZiioOpfsSharedKind[];
|
|
50
|
+
}): string[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { joinOpfsPath } from "./path.js";
|
|
2
|
+
const invalidZiioOpfsNameRe = /[\\/\0]/;
|
|
3
|
+
export const ziioOpfsLayoutVersion = 1;
|
|
4
|
+
export const defaultZiioOpfsUserId = "default";
|
|
5
|
+
export const ziioOpfsSharedKinds = [
|
|
6
|
+
"skills",
|
|
7
|
+
"functions",
|
|
8
|
+
"notes",
|
|
9
|
+
"mcp",
|
|
10
|
+
"libs",
|
|
11
|
+
];
|
|
12
|
+
export const ziioOpfsSystemPaths = {
|
|
13
|
+
systemRoot: "/.ziio",
|
|
14
|
+
fsRegistry: "/.ziio/fs.json",
|
|
15
|
+
appsRegistry: "/.ziio/apps.json",
|
|
16
|
+
usersRegistry: "/.ziio/users.json",
|
|
17
|
+
migrations: "/.ziio/migrations",
|
|
18
|
+
trash: "/.ziio/trash",
|
|
19
|
+
tmp: "/.ziio/tmp",
|
|
20
|
+
};
|
|
21
|
+
export function assertZiioOpfsName(name, label = "name") {
|
|
22
|
+
const trimmed = name.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
throw new Error(`OPFS ${label} must not be empty.`);
|
|
25
|
+
}
|
|
26
|
+
if (invalidZiioOpfsNameRe.test(trimmed) ||
|
|
27
|
+
trimmed === "." ||
|
|
28
|
+
trimmed === "..") {
|
|
29
|
+
throw new Error(`Invalid OPFS ${label}: ${name}`);
|
|
30
|
+
}
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
export function getZiioOpfsUserPaths(userId = defaultZiioOpfsUserId) {
|
|
34
|
+
const safeUserId = assertZiioOpfsName(userId, "user id");
|
|
35
|
+
const userRoot = joinOpfsPath("/users", safeUserId);
|
|
36
|
+
return {
|
|
37
|
+
userId: safeUserId,
|
|
38
|
+
userRoot,
|
|
39
|
+
home: joinOpfsPath(userRoot, "home"),
|
|
40
|
+
documents: joinOpfsPath(userRoot, "home", "documents"),
|
|
41
|
+
downloads: joinOpfsPath(userRoot, "home", "downloads"),
|
|
42
|
+
workspace: joinOpfsPath(userRoot, "home", "workspace"),
|
|
43
|
+
appDataRoot: joinOpfsPath(userRoot, "app-data"),
|
|
44
|
+
cacheRoot: joinOpfsPath(userRoot, "cache"),
|
|
45
|
+
tmpRoot: joinOpfsPath(userRoot, "tmp"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function getZiioOpfsAppPaths(appId, options = {}) {
|
|
49
|
+
const safeAppId = assertZiioOpfsName(appId, "app id");
|
|
50
|
+
const userPaths = getZiioOpfsUserPaths(options.userId);
|
|
51
|
+
const sharedRoot = "/shared";
|
|
52
|
+
return {
|
|
53
|
+
...userPaths,
|
|
54
|
+
appId: safeAppId,
|
|
55
|
+
appInstall: joinOpfsPath("/apps", safeAppId),
|
|
56
|
+
appData: joinOpfsPath(userPaths.appDataRoot, safeAppId),
|
|
57
|
+
appCache: joinOpfsPath(userPaths.cacheRoot, safeAppId),
|
|
58
|
+
appTmp: joinOpfsPath(userPaths.tmpRoot, safeAppId),
|
|
59
|
+
sharedRoot,
|
|
60
|
+
sharedSkills: joinOpfsPath(sharedRoot, "skills"),
|
|
61
|
+
sharedFunctions: joinOpfsPath(sharedRoot, "functions"),
|
|
62
|
+
sharedNotes: joinOpfsPath(sharedRoot, "notes"),
|
|
63
|
+
sharedMcp: joinOpfsPath(sharedRoot, "mcp"),
|
|
64
|
+
sharedLibs: joinOpfsPath(sharedRoot, "libs"),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function getZiioOpfsBaseDirs(options = {}) {
|
|
68
|
+
const userPaths = getZiioOpfsUserPaths(options.userId);
|
|
69
|
+
const sharedKinds = options.sharedKinds ?? ziioOpfsSharedKinds;
|
|
70
|
+
return [
|
|
71
|
+
ziioOpfsSystemPaths.systemRoot,
|
|
72
|
+
ziioOpfsSystemPaths.migrations,
|
|
73
|
+
ziioOpfsSystemPaths.trash,
|
|
74
|
+
ziioOpfsSystemPaths.tmp,
|
|
75
|
+
"/users",
|
|
76
|
+
userPaths.userRoot,
|
|
77
|
+
userPaths.home,
|
|
78
|
+
userPaths.documents,
|
|
79
|
+
userPaths.downloads,
|
|
80
|
+
userPaths.workspace,
|
|
81
|
+
userPaths.appDataRoot,
|
|
82
|
+
userPaths.cacheRoot,
|
|
83
|
+
userPaths.tmpRoot,
|
|
84
|
+
"/shared",
|
|
85
|
+
...sharedKinds.map((kind) => joinOpfsPath("/shared", kind)),
|
|
86
|
+
"/apps",
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
export function getZiioOpfsAppDirs(appId, options = {}) {
|
|
90
|
+
const appPaths = getZiioOpfsAppPaths(appId, options);
|
|
91
|
+
return [
|
|
92
|
+
...getZiioOpfsBaseDirs(options),
|
|
93
|
+
appPaths.appInstall,
|
|
94
|
+
appPaths.appData,
|
|
95
|
+
appPaths.appCache,
|
|
96
|
+
appPaths.appTmp,
|
|
97
|
+
];
|
|
98
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function splitOpfsPath(path: string): string[];
|
|
2
|
+
export declare function normalizeOpfsPath(path: string): string;
|
|
3
|
+
export declare function normalizeRelativeOpfsPath(path: string): string;
|
|
4
|
+
export declare function joinOpfsPath(...parts: Array<number | string>): string;
|
|
5
|
+
export declare function joinRelativeOpfsPath(...parts: Array<number | string>): string;
|
|
6
|
+
export declare function opfsBasename(path: string): string;
|
|
7
|
+
export declare function opfsDirname(path: string): string;
|
|
8
|
+
export declare function stripOpfsPrefix(path: string, prefix: string): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const nullByteRe = /\0/;
|
|
2
|
+
export function splitOpfsPath(path) {
|
|
3
|
+
if (nullByteRe.test(path)) {
|
|
4
|
+
throw new Error("OPFS path must not contain null bytes.");
|
|
5
|
+
}
|
|
6
|
+
return path
|
|
7
|
+
.replace(/\\/g, "/")
|
|
8
|
+
.split("/")
|
|
9
|
+
.filter((part) => part.length > 0 && part !== ".")
|
|
10
|
+
.map((part) => {
|
|
11
|
+
if (part === "..") {
|
|
12
|
+
throw new Error("OPFS path must not contain '..' segments.");
|
|
13
|
+
}
|
|
14
|
+
return part;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function normalizeOpfsPath(path) {
|
|
18
|
+
const segments = splitOpfsPath(path);
|
|
19
|
+
return segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
20
|
+
}
|
|
21
|
+
export function normalizeRelativeOpfsPath(path) {
|
|
22
|
+
return splitOpfsPath(path).join("/");
|
|
23
|
+
}
|
|
24
|
+
export function joinOpfsPath(...parts) {
|
|
25
|
+
return normalizeOpfsPath(parts.filter((part) => String(part)).join("/"));
|
|
26
|
+
}
|
|
27
|
+
export function joinRelativeOpfsPath(...parts) {
|
|
28
|
+
return normalizeRelativeOpfsPath(parts.filter((part) => String(part)).join("/"));
|
|
29
|
+
}
|
|
30
|
+
export function opfsBasename(path) {
|
|
31
|
+
return splitOpfsPath(path).at(-1) ?? "";
|
|
32
|
+
}
|
|
33
|
+
export function opfsDirname(path) {
|
|
34
|
+
const segments = splitOpfsPath(path);
|
|
35
|
+
segments.pop();
|
|
36
|
+
return segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
37
|
+
}
|
|
38
|
+
export function stripOpfsPrefix(path, prefix) {
|
|
39
|
+
const normalizedPath = normalizeOpfsPath(path);
|
|
40
|
+
const normalizedPrefix = normalizeOpfsPath(prefix);
|
|
41
|
+
if (normalizedPrefix === "/") {
|
|
42
|
+
return normalizeRelativeOpfsPath(normalizedPath);
|
|
43
|
+
}
|
|
44
|
+
if (normalizedPath === normalizedPrefix) {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
const prefixWithSlash = `${normalizedPrefix}/`;
|
|
48
|
+
if (!normalizedPath.startsWith(prefixWithSlash)) {
|
|
49
|
+
throw new Error(`OPFS path "${normalizedPath}" is outside prefix "${normalizedPrefix}".`);
|
|
50
|
+
}
|
|
51
|
+
return normalizeRelativeOpfsPath(normalizedPath.slice(prefixWithSlash.length));
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ZiioOpfsSharedKind } from "./layout.js";
|
|
2
|
+
export interface ZiioOpfsFsRegistry {
|
|
3
|
+
kind: "ziio.opfs";
|
|
4
|
+
version: number;
|
|
5
|
+
users: string[];
|
|
6
|
+
apps: string[];
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface EnsureZiioOpfsLayoutOptions {
|
|
10
|
+
appIds?: readonly string[];
|
|
11
|
+
userId?: string;
|
|
12
|
+
sharedKinds?: readonly ZiioOpfsSharedKind[];
|
|
13
|
+
now?: Date;
|
|
14
|
+
}
|
|
15
|
+
export interface EnsureZiioOpfsLayoutResult {
|
|
16
|
+
directories: string[];
|
|
17
|
+
registry: ZiioOpfsFsRegistry;
|
|
18
|
+
}
|
|
19
|
+
export declare function ensureZiioOpfsLayout(options?: EnsureZiioOpfsLayoutOptions): Promise<EnsureZiioOpfsLayoutResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ensureOpfsDirectories, readOpfsJsonFile, writeOpfsJsonFile, } from "./io.js";
|
|
2
|
+
import { getZiioOpfsAppDirs, getZiioOpfsBaseDirs, ziioOpfsLayoutVersion, ziioOpfsSystemPaths, } from "./layout.js";
|
|
3
|
+
export async function ensureZiioOpfsLayout(options = {}) {
|
|
4
|
+
const appIds = [...(options.appIds ?? [])];
|
|
5
|
+
const dirs = appIds.length > 0
|
|
6
|
+
? appIds.flatMap((appId) => getZiioOpfsAppDirs(appId, options))
|
|
7
|
+
: getZiioOpfsBaseDirs(options);
|
|
8
|
+
const directories = await ensureOpfsDirectories([...new Set(dirs)]);
|
|
9
|
+
const registry = await upsertZiioOpfsFsRegistry(options);
|
|
10
|
+
return { directories, registry };
|
|
11
|
+
}
|
|
12
|
+
async function upsertZiioOpfsFsRegistry(options) {
|
|
13
|
+
const now = (options.now ?? new Date()).toISOString();
|
|
14
|
+
let registry = {
|
|
15
|
+
kind: "ziio.opfs",
|
|
16
|
+
version: ziioOpfsLayoutVersion,
|
|
17
|
+
users: [],
|
|
18
|
+
apps: [],
|
|
19
|
+
updatedAt: now,
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
registry = await readOpfsJsonFile(ziioOpfsSystemPaths.fsRegistry);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Missing or unreadable registry is recreated from the requested layout.
|
|
26
|
+
}
|
|
27
|
+
const users = new Set(registry.users);
|
|
28
|
+
users.add(options.userId ?? "default");
|
|
29
|
+
const apps = new Set(registry.apps);
|
|
30
|
+
for (const appId of options.appIds ?? []) {
|
|
31
|
+
apps.add(appId);
|
|
32
|
+
}
|
|
33
|
+
registry = {
|
|
34
|
+
kind: "ziio.opfs",
|
|
35
|
+
version: ziioOpfsLayoutVersion,
|
|
36
|
+
users: [...users].sort(),
|
|
37
|
+
apps: [...apps].sort(),
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
};
|
|
40
|
+
await writeOpfsJsonFile(ziioOpfsSystemPaths.fsRegistry, registry);
|
|
41
|
+
return registry;
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ziioapp/os-features",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser file utilities and ZiioApp OPFS layout helpers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./files/*": {
|
|
12
|
+
"types": "./dist/files/*.d.ts",
|
|
13
|
+
"import": "./dist/files/*.js"
|
|
14
|
+
},
|
|
15
|
+
"./opfs": {
|
|
16
|
+
"types": "./dist/opfs/index.d.ts",
|
|
17
|
+
"import": "./dist/opfs/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./opfs/*": {
|
|
20
|
+
"types": "./dist/opfs/*.d.ts",
|
|
21
|
+
"import": "./dist/opfs/*.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"jschardet": "^3.1.4"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "6.0.3"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"registry": "https://registry.npmjs.org/"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"author": "ziioai",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/ziioai/ziioos.git",
|
|
45
|
+
"directory": "packages/os-features"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/ziioai/ziioos#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/ziioai/ziioos/issues"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"opfs",
|
|
53
|
+
"browser",
|
|
54
|
+
"filesystem",
|
|
55
|
+
"ziioapp"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
59
|
+
"build": "node ../../scripts/build-package.mjs"
|
|
60
|
+
}
|
|
61
|
+
}
|