@mad-c/file-system-helpers 1.0.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/path.js ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Split a path string into segments and normalize it.
3
+ *
4
+ * The normalization does the following:
5
+ * - Removes empty path segments.
6
+ * - Decode from URI parts to handle paths coming from URLs.
7
+ *
8
+ * @param path The path to split into segments
9
+ */
10
+ export function pathToSegments(path) {
11
+ const segments = path.split('/').filter(Boolean).map((part) => part.trim());
12
+ return segments;
13
+ }
14
+ /**
15
+ * Returns the directory name, given a path string.
16
+ * It is similar to node's `dirname` fucntion.
17
+ *
18
+ * @param path The path to get the directory name
19
+ */
20
+ export function dirname(path) {
21
+ const segments = pathToSegments(path);
22
+ const pathBase = path.startsWith('/') ? '/' : '';
23
+ if (segments.length <= 1) {
24
+ return pathBase;
25
+ }
26
+ const dirSegments = segments.slice(0, -1);
27
+ return `${pathBase}${dirSegments.join('/')}`;
28
+ }
29
+ /**
30
+ * Returns the name for the last part of a given path string.
31
+ * It is similar to node's `basename` function.
32
+ *
33
+ * @param path The path to get the last part.
34
+ * @param suffix An optional extension to remove from the item, like an extension.
35
+ */
36
+ export function basename(path, suffix) {
37
+ const segments = pathToSegments(path);
38
+ const lastSegment = segments.at(-1) ?? '';
39
+ if (suffix && lastSegment.endsWith(suffix)) {
40
+ return lastSegment.slice(0, -suffix.length);
41
+ }
42
+ return lastSegment;
43
+ }
44
+ /**
45
+ * Returns the extension of the path, from the last occurrence of the `.` (dot) to the end of the string.
46
+ * Returns an empty string if there is no dot or if the only dot is in the start of the string.
47
+ * It is similar to node's `extname` function.
48
+ *
49
+ * @param path The path to get the extension
50
+ */
51
+ export function extname(path) {
52
+ const base = basename(path);
53
+ const index = base.lastIndexOf('.');
54
+ if (index <= 0) {
55
+ return '';
56
+ }
57
+ return base.slice(index);
58
+ }
59
+ /**
60
+ * Resolves a sequence of paths or path segments into an absolute path.
61
+ * It is similar to node's `resolve` function.
62
+ *
63
+ * **Note**: If no absolute path is provided, the function prepends a `/`. It assumes the starting point is _always_ the file system root.
64
+ *
65
+ * @param paths A sequence of paths or path segments
66
+ */
67
+ export function resolve(...paths) {
68
+ // INfO: start by concatenating all paths
69
+ let fullPath = '';
70
+ for (const path of paths) {
71
+ if (path.startsWith('/')) {
72
+ fullPath = path;
73
+ }
74
+ else {
75
+ fullPath = fullPath ? `${fullPath}/${path}` : path;
76
+ }
77
+ }
78
+ // INFO: prepend initial dash
79
+ if (!fullPath.startsWith('/')) {
80
+ fullPath = `/${fullPath}`;
81
+ }
82
+ // INFO: resolve `..`, `.`, and empty segments
83
+ const stack = [];
84
+ const segments = pathToSegments(fullPath);
85
+ for (const segment of segments) {
86
+ if (segment === '..') {
87
+ stack.pop();
88
+ }
89
+ else if (segment !== '.' && segment !== '') {
90
+ stack.push(segment);
91
+ }
92
+ }
93
+ return `/${stack.join('/')}`;
94
+ }
95
+ const RESTRICTED_NAMES = [
96
+ 'CON',
97
+ 'PRN',
98
+ 'AUX',
99
+ 'NUL',
100
+ 'COM1',
101
+ 'COM2',
102
+ 'COM3',
103
+ 'COM4',
104
+ 'COM5',
105
+ 'COM6',
106
+ 'COM7',
107
+ 'COM8',
108
+ 'COM9',
109
+ 'LPT1',
110
+ 'LPT2',
111
+ 'LPT3',
112
+ 'LPT4',
113
+ 'LPT5',
114
+ 'LPT6',
115
+ 'LPT7',
116
+ 'LPT8',
117
+ 'LPT9'
118
+ ];
119
+ const RESTRICTED_CHARACTERS = [
120
+ // INFO: C0 control characters
121
+ // oxlint-disable-next-line no-magic-numbers
122
+ ...new Array(32).fill('').map((_, i) => String.fromCharCode(i)),
123
+ // INFO: "del" character
124
+ '\x7f',
125
+ // INFO: C1 control characters
126
+ // oxlint-disable-next-line no-magic-numbers
127
+ ...new Array(32).fill('').map((_, i) => String.fromCharCode(128 + i)),
128
+ // INFO: characters not allowed on Windows/Linux/Mac
129
+ ...['*', '"', '/', '\\', '>', '<', ':', '|', '?', "'"],
130
+ // INFO: also add "%" to make it easier to decode lone "%" symbols
131
+ '%'
132
+ ];
133
+ function defaultEncodeReplacer(segment) {
134
+ const trimmedSegment = segment.trim();
135
+ if (trimmedSegment === '..') {
136
+ return '%2e%2e';
137
+ }
138
+ if (trimmedSegment === '.') {
139
+ return '%2e';
140
+ }
141
+ if (RESTRICTED_NAMES.includes(trimmedSegment)) {
142
+ // oxlint-disable-next-line typescript/no-misused-spread
143
+ return [...trimmedSegment].map((char) => `%${char.charCodeAt(0).toString(16)}`).join('');
144
+ }
145
+ // oxlint-disable-next-line typescript/no-misused-spread
146
+ const codePoints = [...trimmedSegment];
147
+ let normalizedSegment = '';
148
+ for (const codePoint of codePoints) {
149
+ if (RESTRICTED_CHARACTERS.includes(codePoint)) {
150
+ normalizedSegment += `%${codePoint.charCodeAt(0).toString(16)}`;
151
+ }
152
+ else {
153
+ normalizedSegment += codePoint;
154
+ }
155
+ }
156
+ return normalizedSegment;
157
+ }
158
+ /**
159
+ * Encodes a path string, taking care of restricted characters and names and converting them to percent encoded characters.
160
+ *
161
+ * @param path The path to encode.
162
+ * @param replacer A custom replacer function that will be invoked for each segment of the path.
163
+ */
164
+ export function encodePath(path, replacer) {
165
+ const segments = pathToSegments(path);
166
+ return segments.map((segment) => (replacer ?? defaultEncodeReplacer)(segment)).join('/');
167
+ }
168
+ function defaultDecodeReplacer(segment) {
169
+ return decodeURIComponent(segment.trim().replaceAll(/%(?![0-9a-fA-F]{2})/giu, '%25'));
170
+ }
171
+ /**
172
+ * Decodes a path string, converting back from perdent encoded characters.
173
+ *
174
+ * @param path The path to decode.
175
+ * @param replacer A custom replacer function that will be invoked for each segment of the path.
176
+ */
177
+ export function decodePath(path, replacer) {
178
+ const segments = pathToSegments(path);
179
+ return segments.map((segment) => (replacer ?? defaultDecodeReplacer)(segment)).join('/');
180
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Get the current permission state for the handle.
3
+ *
4
+ * @param handle The handle to check permissions.
5
+ * @param permissions The permission type to check.
6
+ */
7
+ export declare function getHandlePermisions(handle: FileSystemHandle, permissions?: FileSystemPermissionMode): Promise<PermissionState>;
8
+ /**
9
+ * Checks and then request for permissions for a given handle.
10
+ *
11
+ * @param handle The handle to request permissions.
12
+ * @param permissions The permission type to get.
13
+ */
14
+ export declare function requestHandlePermissions(handle: FileSystemHandle, permissions?: FileSystemPermissionMode): Promise<void>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Get the current permission state for the handle.
3
+ *
4
+ * @param handle The handle to check permissions.
5
+ * @param permissions The permission type to check.
6
+ */
7
+ export async function getHandlePermisions(handle, permissions = 'read') {
8
+ const permission = await handle.queryPermission({ mode: permissions });
9
+ return permission;
10
+ }
11
+ /**
12
+ * Checks and then request for permissions for a given handle.
13
+ *
14
+ * @param handle The handle to request permissions.
15
+ * @param permissions The permission type to get.
16
+ */
17
+ export async function requestHandlePermissions(handle, permissions = 'read') {
18
+ const permission = await getHandlePermisions(handle, permissions);
19
+ if (permission !== 'granted') {
20
+ const request = await handle.requestPermission({ mode: permissions });
21
+ if (request !== 'granted') {
22
+ throw new Error('Permission to access the entry was denied');
23
+ }
24
+ }
25
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mad-c/file-system-helpers",
3
+ "version": "1.0.0",
4
+ "description": "Helpwer for working with the File System API",
5
+ "keywords": [
6
+ "file system",
7
+ "FS",
8
+ "browser file system",
9
+ "origin private file system",
10
+ "OPFS"
11
+ ],
12
+ "author": "madcampos",
13
+ "license": "LGPL-2.1-or-later",
14
+ "type": "module",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "exports": {
19
+ ".": "./dist/opfs.js",
20
+ "./path": "./dist/path.js",
21
+ "./permissions": "./dist/permissions.js",
22
+ "./access": "./dist/fs-access.js"
23
+ },
24
+ "scripts": {
25
+ "bootstrap": "playwright install chromium",
26
+ "build": "tsc --project tsconfig.build.json",
27
+ "format": "dprint fmt",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint:js": "oxlint --fix",
30
+ "lint": "pnpm run typecheck && pnpm run lint:js",
31
+ "test": "vitest",
32
+ "changelog": "bumpy generate",
33
+ "prepublishOnly": "pnpm run build",
34
+ "bump-version": "bumpy version --commit",
35
+ "postbump-version": "bumpy publish"
36
+ },
37
+ "devDependencies": {
38
+ "@types/wicg-file-system-access": "^2023.10.7",
39
+ "@varlock/bumpy": "^1.18.0",
40
+ "@vitest/browser-playwright": "^4.1.9",
41
+ "@vitest/coverage-v8": "^4.1.9",
42
+ "dprint": "^0.54.0",
43
+ "oxlint": "^1.71.0",
44
+ "oxlint-tsgolint": "^0.23.0",
45
+ "typescript": "^6.0.3",
46
+ "vitest": "^4.1.9"
47
+ }
48
+ }