@expofp/utils 3.0.0-alpha.10

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.
@@ -0,0 +1,14 @@
1
+ export * from './lib/buildZipArchive.js';
2
+ export * from './lib/clone.js';
3
+ export * from './lib/deepFreeze.js';
4
+ export * from './lib/importFsPromises.js';
5
+ export * from './lib/importJsonModule.js';
6
+ export * from './lib/importNodeModule.js';
7
+ export * from './lib/importRuntimeJsonModule.js';
8
+ export * from './lib/loadScript.js';
9
+ export * from './lib/makeLocalPath.js';
10
+ export * from './lib/niceFetch.js';
11
+ export * from './lib/readFileFromUrl.js';
12
+ export * from './lib/sha256Base62Truncated.js';
13
+ export * from './lib/slugifyFilesystemUnique.js';
14
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export * from './lib/buildZipArchive.js';
2
+ export * from './lib/clone.js';
3
+ export * from './lib/deepFreeze.js';
4
+ export * from './lib/importFsPromises.js';
5
+ export * from './lib/importJsonModule.js';
6
+ export * from './lib/importNodeModule.js';
7
+ export * from './lib/importRuntimeJsonModule.js';
8
+ export * from './lib/loadScript.js';
9
+ export * from './lib/makeLocalPath.js';
10
+ export * from './lib/niceFetch.js';
11
+ export * from './lib/readFileFromUrl.js';
12
+ export * from './lib/sha256Base62Truncated.js';
13
+ export * from './lib/slugifyFilesystemUnique.js';
@@ -0,0 +1,5 @@
1
+ export declare function buildZipArchive(files: AsyncIterable<{
2
+ path: string;
3
+ data: Uint8Array;
4
+ }>): Promise<Blob>;
5
+ //# sourceMappingURL=buildZipArchive.d.ts.map
@@ -0,0 +1,40 @@
1
+ import debug from 'debug';
2
+ import JSZip from 'jszip';
3
+ const log = debug('efp:utils:buildZipArchive');
4
+ const CONCURRENCY = 10;
5
+ async function processConcurrently(iterator, concurrency, processor) {
6
+ const activePromises = new Set();
7
+ for await (const item of iterator) {
8
+ const promise = Promise.resolve(processor(item)).then(() => {
9
+ activePromises.delete(promise);
10
+ });
11
+ activePromises.add(promise);
12
+ if (activePromises.size >= concurrency) {
13
+ await Promise.race(activePromises);
14
+ }
15
+ }
16
+ await Promise.all(activePromises);
17
+ }
18
+ export async function buildZipArchive(files) {
19
+ const zip = new JSZip();
20
+ // const bundle = await makeOfflineBundle(manifest);
21
+ let hasFiles = false;
22
+ await processConcurrently(files, CONCURRENCY, (file) => {
23
+ log('Adding file to zip:', file.path, 'size:', file.data.byteLength);
24
+ zip.file(file.path, file.data);
25
+ hasFiles = true;
26
+ });
27
+ // for (const extraFile of extraFiles ?? []) {
28
+ // log('Adding extra file to zip:', extraFile.path, 'size:', extraFile.data.byteLength);
29
+ // zip.file(extraFile.path, extraFile.data);
30
+ // hasFiles = true;
31
+ // }
32
+ if (!hasFiles) {
33
+ throw new Error('No files were added to the ZIP archive');
34
+ }
35
+ // Generate archive
36
+ const blob = await zip.generateAsync({ type: 'blob' });
37
+ // console.info('Manifest for HTML:', offlineData.manifest);
38
+ log('Generated offline ZIP bundle, size:', blob.size);
39
+ return blob;
40
+ }
@@ -0,0 +1,3 @@
1
+ export declare function deepClone<T>(obj: T): T;
2
+ export declare function shallowClone<T>(obj: T): T;
3
+ //# sourceMappingURL=clone.d.ts.map
@@ -0,0 +1,14 @@
1
+ export function deepClone(obj) {
2
+ if ('structuredClone' in globalThis) {
3
+ return globalThis.structuredClone(obj);
4
+ }
5
+ return JSON.parse(JSON.stringify(obj));
6
+ }
7
+ export function shallowClone(obj) {
8
+ if (obj === null || typeof obj !== 'object')
9
+ return obj;
10
+ if (Array.isArray(obj)) {
11
+ return obj.slice();
12
+ }
13
+ return { ...obj };
14
+ }
@@ -0,0 +1,2 @@
1
+ export declare function deepFreeze<T>(obj: T): T;
2
+ //# sourceMappingURL=deepFreeze.d.ts.map
@@ -0,0 +1,11 @@
1
+ export function deepFreeze(obj) {
2
+ if (obj === null || typeof obj !== 'object')
3
+ return obj;
4
+ Object.getOwnPropertyNames(obj).forEach((prop) => {
5
+ const value = obj[prop];
6
+ if (value && typeof value === 'object') {
7
+ deepFreeze(value);
8
+ }
9
+ });
10
+ return Object.freeze(obj);
11
+ }
@@ -0,0 +1,2 @@
1
+ export declare function importFsPromises(): Promise<typeof import("node:fs/promises")>;
2
+ //# sourceMappingURL=importFsPromises.d.ts.map
@@ -0,0 +1,4 @@
1
+ import { importNodeModule } from './importNodeModule.js';
2
+ export async function importFsPromises() {
3
+ return importNodeModule('node:fs/promises');
4
+ }
@@ -0,0 +1,11 @@
1
+ export interface ImportJsonModuleOptions {
2
+ forceFetch?: boolean;
3
+ fetchCache?: Map<string, Promise<unknown>>;
4
+ /**
5
+ * Callback invoked when a URL is fetched or imported or read from the file system.
6
+ */
7
+ importCallback?: (url: string) => void;
8
+ signal?: AbortSignal | null;
9
+ }
10
+ export declare function importJsonModule<T = unknown>(url: string, options?: ImportJsonModuleOptions): Promise<T>;
11
+ //# sourceMappingURL=importJsonModule.d.ts.map
@@ -0,0 +1,78 @@
1
+ import debug from 'debug';
2
+ import { deepFreeze } from './deepFreeze.js';
3
+ import { importFsPromises } from './importFsPromises.js';
4
+ import { niceFetch } from './niceFetch.js';
5
+ const log = debug('efp:utils:importJsonModule');
6
+ let importJsonNotAvailable;
7
+ const jsonFrozen = new WeakSet();
8
+ // ET: this is a workaround for Vite that analyzes dynamic imports and removes 'with' option
9
+ // to be removed when Vite supports it properly
10
+ // Cons: can have CSP issues in some environments
11
+ const importJsonNative = new Function('url', 'return import(url, { with: { type: "json" } });');
12
+ export async function importJsonModule(url, options) {
13
+ const opts = {
14
+ forceFetch: options?.forceFetch || false,
15
+ fetchCache: options?.fetchCache || new Map(),
16
+ importCallback: options?.importCallback,
17
+ signal: options?.signal || null,
18
+ };
19
+ // console.warn('importJson:', resolveContext.forceFetch);
20
+ if (importJsonNotAvailable === undefined && !opts.forceFetch) {
21
+ try {
22
+ await importJsonNative(url);
23
+ importJsonNotAvailable = false;
24
+ }
25
+ catch {
26
+ log('Dynamic import not available');
27
+ importJsonNotAvailable = true;
28
+ }
29
+ }
30
+ let result = undefined;
31
+ if (!importJsonNotAvailable && !opts.forceFetch) {
32
+ log('Dynamic import', url);
33
+ const module = await importJsonNative(url);
34
+ result = module.default;
35
+ }
36
+ else if (url.startsWith('file:') && !opts.forceFetch) {
37
+ log('Read from file system', url);
38
+ result = await readJsonFromFs(new URL(url));
39
+ }
40
+ else {
41
+ log('Fetch', url);
42
+ result = await loadJson(url, opts.fetchCache, opts.signal || null);
43
+ }
44
+ // log('Callback for imported JSON URL:', url, !!opts.importCallback);
45
+ opts.importCallback?.(url);
46
+ // Freeze the imported JSON to make it immutable
47
+ // This is rather a guard against accidental mutations
48
+ if (typeof result === 'object' && result !== null && !jsonFrozen.has(result)) {
49
+ deepFreeze(result);
50
+ jsonFrozen.add(result);
51
+ }
52
+ return result;
53
+ }
54
+ // const fetchCache = new Map<string, Promise<any>>();
55
+ async function loadJson(url, fetchCache, signal) {
56
+ const key = '__loadJson__' + url;
57
+ if (fetchCache.has(key)) {
58
+ return fetchCache.get(key);
59
+ }
60
+ const dataPromise = (async function loadJsonInner() {
61
+ // in browser just use fetch, for node - fetch-retry
62
+ const response = await niceFetch(url, { signal });
63
+ if (!response.ok) {
64
+ throw new Error(`Failed to fetch JSON from ${url}`);
65
+ }
66
+ return await response.json();
67
+ })();
68
+ fetchCache.set(key, dataPromise);
69
+ return dataPromise;
70
+ }
71
+ async function readJsonFromFs(url) {
72
+ if (url.protocol !== 'file:') {
73
+ throw new Error(`readJsonFromFs only supports file: URLs, got ${url.href}`);
74
+ }
75
+ const fs = await importFsPromises();
76
+ const data = await fs.readFile(url, { encoding: 'utf-8' });
77
+ return JSON.parse(data);
78
+ }
@@ -0,0 +1,2 @@
1
+ export declare function importNodeModule<T>(moduleName: string): Promise<T>;
2
+ //# sourceMappingURL=importNodeModule.d.ts.map
@@ -0,0 +1,7 @@
1
+ export async function importNodeModule(moduleName) {
2
+ if (typeof process === 'undefined' || !process.versions?.node) {
3
+ throw new Error('importNodeModule can only be used in Node.js environments');
4
+ }
5
+ // prevents Vite/Rollup from analyzing the import specifier
6
+ return new Function(`return import('${moduleName}')`)();
7
+ }
@@ -0,0 +1,2 @@
1
+ export declare function importRuntimeJsonModule<T = unknown>(fileName: string): Promise<T>;
2
+ //# sourceMappingURL=importRuntimeJsonModule.d.ts.map
@@ -0,0 +1,10 @@
1
+ import debug from 'debug';
2
+ import { importJsonModule } from './importJsonModule.js';
3
+ const log = debug('efp:utils:importRuntimeJsonModule');
4
+ export async function importRuntimeJsonModule(fileName) {
5
+ // ignore because vite may rewrite '.' to data:...
6
+ const baseUrl = new URL(/* @vite-ignore */ '.', import.meta.url).href;
7
+ const url = new URL(fileName, baseUrl);
8
+ log('Loading runtime JSON from', url.href);
9
+ return await importJsonModule(url.href);
10
+ }
@@ -0,0 +1,3 @@
1
+ /** @deprecated we should never load/eval scripts */
2
+ export declare function loadScript(scriptUrl: string, signal?: AbortSignal): Promise<void>;
3
+ //# sourceMappingURL=loadScript.d.ts.map
@@ -0,0 +1,108 @@
1
+ /// <reference lib="dom" />
2
+ import debug from 'debug';
3
+ import { niceFetch } from './niceFetch.js';
4
+ const log = debug('efp:utils:loadScript');
5
+ function isBrowser() {
6
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
7
+ }
8
+ function isAbortError(err) {
9
+ return err instanceof Error && err.name === 'AbortError';
10
+ }
11
+ /** @deprecated we should never load/eval scripts */
12
+ export async function loadScript(scriptUrl, signal) {
13
+ log('Loading', scriptUrl);
14
+ if (isBrowser()) {
15
+ await loadInBrowser(scriptUrl, signal);
16
+ }
17
+ else {
18
+ await loadInNode(scriptUrl, signal);
19
+ }
20
+ }
21
+ /* -------------------- Browser implementation -------------------- */
22
+ function loadInBrowser(scriptUrl, signal) {
23
+ return new Promise((resolve, reject) => {
24
+ const script = document.createElement('script');
25
+ script.src = scriptUrl;
26
+ script.async = true;
27
+ const cleanup = () => {
28
+ script.onload = null;
29
+ script.onerror = null;
30
+ script.remove();
31
+ };
32
+ const onAbort = () => {
33
+ cleanup();
34
+ reject(new Error('Script load aborted'));
35
+ };
36
+ if (signal?.aborted) {
37
+ cleanup();
38
+ reject(new Error('Script load aborted'));
39
+ return;
40
+ }
41
+ signal?.addEventListener('abort', onAbort, { once: true });
42
+ script.onload = () => {
43
+ signal?.removeEventListener('abort', onAbort);
44
+ cleanup();
45
+ resolve();
46
+ };
47
+ script.onerror = () => {
48
+ signal?.removeEventListener('abort', onAbort);
49
+ cleanup();
50
+ reject(new Error(`Failed to load script: ${scriptUrl}`));
51
+ };
52
+ document.head.appendChild(script);
53
+ });
54
+ }
55
+ /* -------------------- Node implementation -------------------- */
56
+ async function loadInNode(scriptUrl, signal) {
57
+ if (signal?.aborted) {
58
+ throw new Error('Script load aborted');
59
+ }
60
+ let res;
61
+ try {
62
+ res = await niceFetch(scriptUrl, { signal });
63
+ }
64
+ catch (err) {
65
+ // Node / WHATWG fetch uses AbortError on abort
66
+ if (isAbortError(err)) {
67
+ throw new Error('Script load aborted');
68
+ }
69
+ throw err;
70
+ }
71
+ if (!res.ok) {
72
+ throw new Error(`Failed to load script in Node (HTTP ${res.status}): ${scriptUrl}`);
73
+ }
74
+ // If it was aborted between headers and body:
75
+ if (signal?.aborted) {
76
+ throw new Error('Script load aborted');
77
+ }
78
+ let code;
79
+ try {
80
+ code = await res.text();
81
+ }
82
+ catch (err) {
83
+ if (isAbortError(err)) {
84
+ throw new Error('Script load aborted');
85
+ }
86
+ throw err;
87
+ }
88
+ if (signal?.aborted) {
89
+ throw new Error('Script load aborted');
90
+ }
91
+ const g = globalThis;
92
+ const prevWindow = g.window;
93
+ g.window = g;
94
+ try {
95
+ // Indirect eval -> global scope in Node
96
+ // Assumes the loaded script attaches itself to globalThis/window/global.
97
+ (0, eval)(code);
98
+ }
99
+ finally {
100
+ // Restore previous window (if any)
101
+ if (prevWindow === undefined) {
102
+ delete g.window;
103
+ }
104
+ else {
105
+ g.window = prevWindow;
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Makes a local file path from a URL, preserving extension and automatically adding hash for files.
3
+ *
4
+ * @param url Full URL object (e.g., new URL('http://demo.expofp.com/data/path/to/file.ext?version=12345'))
5
+ * @param filenameSuffix Optional suffix to add before file extension (e.g., '_small', '_medium', '_large'). Not allowed for directories.
6
+ *
7
+ * @returns
8
+ * For files: http-demo-expofp-com/data/path/to/fullhashoffullurl/filename_small.ext (hash is automatically added based on full URL including query params)
9
+ * For directories: http-demo-expofp-com/data/path/to/dir/ (no hash added, trailing slash preserved)
10
+ *
11
+ * @remarks
12
+ * - All path segments (protocol, domain, directories) are slugified to avoid special characters
13
+ * - Filename is slugified and truncated to max 50 characters (excluding extension)
14
+ * - Hash is automatically added for files (based on full URL including query params)
15
+ * - Hash is never added for directories (paths ending with /)
16
+ * - Query parameters affect the hash for files, making different versions have unique local paths
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * // File with query params
21
+ * const url1 = new URL('http://demo.expofp.com/image.jpg?v=1');
22
+ * await makeLocalPath(url1); // http-demo-expofp-com/ABC123XYZ/image.jpg
23
+ *
24
+ * const url2 = new URL('http://demo.expofp.com/image.jpg?v=2');
25
+ * await makeLocalPath(url2); // http-demo-expofp-com/DEF456UVW/image.jpg (different hash)
26
+ *
27
+ * // File with suffix
28
+ * const url3 = new URL('http://demo.expofp.com/image.jpg');
29
+ * await makeLocalPath(url3, '_small'); // http-demo-expofp-com/GHI789RST/image_small.jpg
30
+ *
31
+ * // Directory
32
+ * const url4 = new URL('http://demo.expofp.com/data/');
33
+ * await makeLocalPath(url4); // http-demo-expofp-com/data/
34
+ * ```
35
+ */
36
+ export declare function makeLocalPath(url: URL, filenameSuffix?: string): Promise<string>;
37
+ //# sourceMappingURL=makeLocalPath.d.ts.map
@@ -0,0 +1,100 @@
1
+ import debug from 'debug';
2
+ import { sha256Base62Truncated } from './sha256Base62Truncated.js';
3
+ import { slugifyFilesystemUnique } from './slugifyFilesystemUnique.js';
4
+ const log = debug('efp:utils:makeLocalPath');
5
+ /**
6
+ * Makes a local file path from a URL, preserving extension and automatically adding hash for files.
7
+ *
8
+ * @param url Full URL object (e.g., new URL('http://demo.expofp.com/data/path/to/file.ext?version=12345'))
9
+ * @param filenameSuffix Optional suffix to add before file extension (e.g., '_small', '_medium', '_large'). Not allowed for directories.
10
+ *
11
+ * @returns
12
+ * For files: http-demo-expofp-com/data/path/to/fullhashoffullurl/filename_small.ext (hash is automatically added based on full URL including query params)
13
+ * For directories: http-demo-expofp-com/data/path/to/dir/ (no hash added, trailing slash preserved)
14
+ *
15
+ * @remarks
16
+ * - All path segments (protocol, domain, directories) are slugified to avoid special characters
17
+ * - Filename is slugified and truncated to max 50 characters (excluding extension)
18
+ * - Hash is automatically added for files (based on full URL including query params)
19
+ * - Hash is never added for directories (paths ending with /)
20
+ * - Query parameters affect the hash for files, making different versions have unique local paths
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // File with query params
25
+ * const url1 = new URL('http://demo.expofp.com/image.jpg?v=1');
26
+ * await makeLocalPath(url1); // http-demo-expofp-com/ABC123XYZ/image.jpg
27
+ *
28
+ * const url2 = new URL('http://demo.expofp.com/image.jpg?v=2');
29
+ * await makeLocalPath(url2); // http-demo-expofp-com/DEF456UVW/image.jpg (different hash)
30
+ *
31
+ * // File with suffix
32
+ * const url3 = new URL('http://demo.expofp.com/image.jpg');
33
+ * await makeLocalPath(url3, '_small'); // http-demo-expofp-com/GHI789RST/image_small.jpg
34
+ *
35
+ * // Directory
36
+ * const url4 = new URL('http://demo.expofp.com/data/');
37
+ * await makeLocalPath(url4); // http-demo-expofp-com/data/
38
+ * ```
39
+ */
40
+ export async function makeLocalPath(url, filenameSuffix) {
41
+ // // Normalize baseUrl to end with /
42
+ // const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
43
+ // // Combine to get full URL
44
+ // const fullUrl = new URL(relativeUrl || '', normalizedBase).href;
45
+ // Parse the URL
46
+ // const url = new URL(fullUrl);
47
+ // Check if this is a directory (ends with / or is empty/root)
48
+ const isDirectory = url.pathname.endsWith('/') || url.pathname === '';
49
+ const addHash = !isDirectory;
50
+ // Validate parameters for directories
51
+ if (isDirectory && filenameSuffix) {
52
+ throw new Error('filenameSuffix is not allowed for directory paths');
53
+ }
54
+ // Create hash if requested (using full URL)
55
+ const hash = addHash ? await sha256Base62Truncated(url.href) : null;
56
+ // Slugify protocol and domain (e.g., "http-demo-expofp-com")
57
+ const protocol = slugifyFilesystemUnique(url.protocol.replace(':', ''));
58
+ const domain = slugifyFilesystemUnique(url.hostname);
59
+ const port = url.port ? `-${url.port}` : '';
60
+ const protocolDomain = `${protocol}-${domain}${port}`;
61
+ // Parse the path
62
+ const pathParts = url.pathname.split('/').filter(Boolean);
63
+ if (isDirectory) {
64
+ // For directories, all parts are directory parts
65
+ const slugifiedDirs = pathParts.map((part) => slugifyFilesystemUnique(part));
66
+ // Build the full local path with trailing /
67
+ const pathSegments = hash
68
+ ? [protocolDomain, ...slugifiedDirs, hash]
69
+ : [protocolDomain, ...slugifiedDirs];
70
+ return pathSegments.join('/') + '/';
71
+ }
72
+ // For files: Extract filename (last part) and directory parts
73
+ const filename = pathParts.pop() || '';
74
+ const directoryParts = pathParts;
75
+ // Slugify directory parts
76
+ const slugifiedDirs = directoryParts.map((part) => slugifyFilesystemUnique(part));
77
+ // Extract extension and base filename
78
+ const lastDotIndex = filename.lastIndexOf('.');
79
+ const extension = lastDotIndex !== -1 ? filename.slice(lastDotIndex) : '';
80
+ const baseFilename = lastDotIndex !== -1 ? filename.slice(0, lastDotIndex) : filename;
81
+ // Slugify the base filename
82
+ let slugifiedFilename = slugifyFilesystemUnique(baseFilename);
83
+ // Ensure filename is max 50 characters (not including extension)
84
+ const maxFilenameLength = 50;
85
+ if (slugifiedFilename.length > maxFilenameLength) {
86
+ log(`Filename truncated from ${slugifiedFilename.length} to ${maxFilenameLength} characters: ${slugifiedFilename}`);
87
+ slugifiedFilename = slugifiedFilename.slice(0, maxFilenameLength);
88
+ }
89
+ // Add suffix if provided (before extension)
90
+ if (filenameSuffix) {
91
+ slugifiedFilename = `${slugifiedFilename}${filenameSuffix}`;
92
+ }
93
+ // Combine filename with extension
94
+ const finalFilename = `${slugifiedFilename}${extension}`;
95
+ // Build the full local path
96
+ const pathSegments = hash
97
+ ? [protocolDomain, ...slugifiedDirs, hash, finalFilename]
98
+ : [protocolDomain, ...slugifiedDirs, finalFilename];
99
+ return pathSegments.join('/');
100
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Fetches a resource from the network, with retry support in Node.js environments.
3
+ * In browser environments, it uses the native `fetch` API.
4
+ * In Node.js environments, it uses the `fetch-retry` library to add retry capabilities.
5
+ * Node.js are more prone to transient network errors, so retries can help improve reliability.
6
+ */
7
+ export declare const niceFetch: (...args: Parameters<typeof window.fetch>) => ReturnType<typeof window.fetch>;
8
+ //# sourceMappingURL=niceFetch.d.ts.map
@@ -0,0 +1,21 @@
1
+ /// <reference lib="dom" />
2
+ let fn;
3
+ /**
4
+ * Fetches a resource from the network, with retry support in Node.js environments.
5
+ * In browser environments, it uses the native `fetch` API.
6
+ * In Node.js environments, it uses the `fetch-retry` library to add retry capabilities.
7
+ * Node.js are more prone to transient network errors, so retries can help improve reliability.
8
+ */
9
+ export const niceFetch = async function fetch(...args) {
10
+ if (!fn) {
11
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
12
+ fn = window.fetch.bind(window);
13
+ }
14
+ else {
15
+ const fr = await import('fetch-retry');
16
+ const fetchWithRetry = fr.default(globalThis.fetch);
17
+ fn = fetchWithRetry;
18
+ }
19
+ }
20
+ return fn(...args);
21
+ };
@@ -0,0 +1,11 @@
1
+ type ReadFileFromUrlResult = {
2
+ ok: true;
3
+ data: Uint8Array;
4
+ } | {
5
+ ok: false;
6
+ };
7
+ export declare function readFileFromUrl(url: URL, options?: {
8
+ signal?: AbortSignal;
9
+ }): Promise<ReadFileFromUrlResult>;
10
+ export {};
11
+ //# sourceMappingURL=readFileFromUrl.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { importFsPromises } from './importFsPromises.js';
2
+ import { niceFetch } from './niceFetch.js';
3
+ export async function readFileFromUrl(url, options) {
4
+ if (url.protocol === 'file:') {
5
+ const { readFile } = await importFsPromises();
6
+ const data = await readFile(url, { signal: options?.signal });
7
+ return { ok: true, data };
8
+ }
9
+ else {
10
+ const response = await niceFetch(url, { signal: options?.signal });
11
+ if (!response.ok) {
12
+ return { ok: false };
13
+ }
14
+ const arrayBuffer = await response.arrayBuffer();
15
+ return { ok: true, data: new Uint8Array(arrayBuffer) };
16
+ }
17
+ }
@@ -0,0 +1,2 @@
1
+ export declare function sha256Base62Truncated(input: string, bytes?: number): Promise<string>;
2
+ //# sourceMappingURL=sha256Base62Truncated.d.ts.map
@@ -0,0 +1,23 @@
1
+ export async function sha256Base62Truncated(input, bytes = 16) {
2
+ const data = new TextEncoder().encode(input);
3
+ const digest = await crypto.subtle.digest('SHA-256', data);
4
+ const full = new Uint8Array(digest);
5
+ return toBase62(full.subarray(0, bytes));
6
+ }
7
+ const BASE62_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
8
+ function toBase62(bytes) {
9
+ // TODO: check if this ArrayBuffer guard is still needed — parameter is typed Uint8Array
10
+ const u8 = bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes;
11
+ // Convert bytes -> BigInt (no Buffer required)
12
+ let num = 0n;
13
+ for (const b of u8)
14
+ num = (num << 8n) + BigInt(b);
15
+ if (num === 0n)
16
+ return '0';
17
+ let result = '';
18
+ while (num > 0n) {
19
+ result = BASE62_CHARS[Number(num % 62n)] + result;
20
+ num /= 62n;
21
+ }
22
+ return result;
23
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Generates a filesystem-safe slug from input string, ensuring uniqueness.
3
+ * @param input
4
+ * @returns
5
+ */
6
+ export declare function slugifyFilesystemUnique(input: string): string;
7
+ //# sourceMappingURL=slugifyFilesystemUnique.d.ts.map
@@ -0,0 +1,80 @@
1
+ // Module-level caches
2
+ const inputToSlug = new Map();
3
+ const slugToInput = new Map();
4
+ const WINDOWS_RESERVED = new Set([
5
+ 'con',
6
+ 'prn',
7
+ 'aux',
8
+ 'nul',
9
+ 'com1',
10
+ 'com2',
11
+ 'com3',
12
+ 'com4',
13
+ 'com5',
14
+ 'com6',
15
+ 'com7',
16
+ 'com8',
17
+ 'com9',
18
+ 'lpt1',
19
+ 'lpt2',
20
+ 'lpt3',
21
+ 'lpt4',
22
+ 'lpt5',
23
+ 'lpt6',
24
+ 'lpt7',
25
+ 'lpt8',
26
+ 'lpt9',
27
+ ]);
28
+ function makeBaseSlug(input) {
29
+ let slug = input
30
+ .normalize('NFKD')
31
+ // All non letters/numbers → "-"
32
+ .replace(/[^\p{Letter}\p{Number}]+/gu, '-')
33
+ // Collapse multiple "-"
34
+ .replace(/-+/g, '-')
35
+ // Trim "-" from start/end
36
+ .replace(/^-|-$/g, '')
37
+ .toLowerCase();
38
+ // Strip forbidden Windows characters just in case
39
+ slug = slug.replace(/[<>:"/\\|?*]/g, '');
40
+ // Windows forbids trailing space/period
41
+ slug = slug.replace(/[. ]+$/g, '');
42
+ // Fallback if everything was stripped
43
+ if (!slug)
44
+ slug = 'file';
45
+ // Avoid bare reserved device names
46
+ if (WINDOWS_RESERVED.has(slug)) {
47
+ slug += '-file';
48
+ }
49
+ return slug;
50
+ }
51
+ /**
52
+ * Generates a filesystem-safe slug from input string, ensuring uniqueness.
53
+ * @param input
54
+ * @returns
55
+ */
56
+ export function slugifyFilesystemUnique(input) {
57
+ // If we've seen this exact input before, return the same slug
58
+ const existing = inputToSlug.get(input);
59
+ if (existing)
60
+ return existing;
61
+ const base = makeBaseSlug(input);
62
+ let candidate = base;
63
+ let counter = 2;
64
+ while (true) {
65
+ const existingInput = slugToInput.get(candidate);
66
+ if (!existingInput) {
67
+ // Free slug → claim it for this input
68
+ slugToInput.set(candidate, input);
69
+ inputToSlug.set(input, candidate);
70
+ return candidate;
71
+ }
72
+ if (existingInput === input) {
73
+ // Same input somehow (super defensive)
74
+ inputToSlug.set(input, candidate);
75
+ return candidate;
76
+ }
77
+ // Collision: same slug already used by different input → add suffix
78
+ candidate = `${base}-${counter++}`;
79
+ }
80
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@expofp/utils",
3
+ "version": "3.0.0-alpha.10",
4
+ "type": "module",
5
+ "description": "ExpoFP SDK internal: shared utilities",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ "./package.json": "./package.json",
15
+ ".": {
16
+ "@expofp/source": "./src/index.ts",
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "!**/*.tsbuildinfo",
25
+ "!**/*.map"
26
+ ],
27
+ "dependencies": {
28
+ "debug": "^4.4.3",
29
+ "fetch-retry": "^6.0.0",
30
+ "jszip": "^3.10.1",
31
+ "tslib": "^2.3.0"
32
+ }
33
+ }