@expofp/utils 3.11.12 → 3.12.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 +3 -0
- package/dist/index.js +3 -0
- package/dist/lib/import-js-as-json.d.ts +52 -0
- package/dist/lib/import-js-as-json.js +124 -0
- package/dist/lib/import-json.js +7 -0
- package/dist/lib/safe-decode.d.ts +8 -0
- package/dist/lib/safe-decode.js +14 -0
- package/dist/lib/safe-local-storage.d.ts +11 -0
- package/dist/lib/safe-local-storage.js +18 -0
- package/package.json +1 -1
- package/README.md +0 -11
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,15 @@ export * from './lib/clone.js';
|
|
|
3
3
|
export * from './lib/deep-freeze.js';
|
|
4
4
|
export * from './lib/fetch-with-retry.js';
|
|
5
5
|
export * from './lib/import-fs-promises.js';
|
|
6
|
+
export * from './lib/import-js-as-json.js';
|
|
6
7
|
export * from './lib/import-json.js';
|
|
7
8
|
export * from './lib/import-node-module.js';
|
|
8
9
|
export * from './lib/import-runtime-json.js';
|
|
9
10
|
export * from './lib/load-script.js';
|
|
10
11
|
export * from './lib/make-local-path.js';
|
|
11
12
|
export * from './lib/read-file-from-url.js';
|
|
13
|
+
export * from './lib/safe-decode.js';
|
|
14
|
+
export * from './lib/safe-local-storage.js';
|
|
12
15
|
export * from './lib/safe-slugify.js';
|
|
13
16
|
export * from './lib/short-hash.js';
|
|
14
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,14 @@ export * from './lib/clone.js';
|
|
|
3
3
|
export * from './lib/deep-freeze.js';
|
|
4
4
|
export * from './lib/fetch-with-retry.js';
|
|
5
5
|
export * from './lib/import-fs-promises.js';
|
|
6
|
+
export * from './lib/import-js-as-json.js';
|
|
6
7
|
export * from './lib/import-json.js';
|
|
7
8
|
export * from './lib/import-node-module.js';
|
|
8
9
|
export * from './lib/import-runtime-json.js';
|
|
9
10
|
export * from './lib/load-script.js';
|
|
10
11
|
export * from './lib/make-local-path.js';
|
|
11
12
|
export * from './lib/read-file-from-url.js';
|
|
13
|
+
export * from './lib/safe-decode.js';
|
|
14
|
+
export * from './lib/safe-local-storage.js';
|
|
12
15
|
export * from './lib/safe-slugify.js';
|
|
13
16
|
export * from './lib/short-hash.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface EvalJsToJsonOptions {
|
|
2
|
+
/**
|
|
3
|
+
* If set, return only these top-level names (missing ones are skipped) instead
|
|
4
|
+
* of every assigned global. Useful when a script assigns noise you don't want.
|
|
5
|
+
*/
|
|
6
|
+
pick?: readonly string[];
|
|
7
|
+
/**
|
|
8
|
+
* Extra globals seeded into the sandbox before running — e.g. a `console` or a
|
|
9
|
+
* stub `document` the script expects. Seeded keys are inputs and are omitted
|
|
10
|
+
* from the result (unless the script overwrites one and you `pick` it).
|
|
11
|
+
*/
|
|
12
|
+
globals?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export interface ImportJsAsJsonOptions extends EvalJsToJsonOptions {
|
|
15
|
+
signal?: AbortSignal | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Evaluate a standalone (non-module) JavaScript snippet and return everything it
|
|
19
|
+
* assigned to the global scope as a plain object. **Works in both the browser and
|
|
20
|
+
* Node.js** — no `eval`-free CSP required beyond `unsafe-eval` (see caveats).
|
|
21
|
+
*
|
|
22
|
+
* The snippet runs inside `with (sandbox) { … }`, where `sandbox` is a Proxy that
|
|
23
|
+
* traps every free identifier. So, with no need to know the names in advance:
|
|
24
|
+
* - top-level `var` declarations (`var __data = {…}`) land in the sandbox
|
|
25
|
+
* - bare and browser-style assignments (`__x = …`, `window['__paths'] = …`,
|
|
26
|
+
* `self.x = …`), including dynamically-named ones (`window['__p' + i] = …`)
|
|
27
|
+
* - built-ins the script reads (`JSON`, `Math`, `document`, `fetch`, …) fall
|
|
28
|
+
* through to the real global object
|
|
29
|
+
*
|
|
30
|
+
* Not captured: top-level `let`/`const` and `function` *declarations* — they bind
|
|
31
|
+
* to the wrapper's own scope, not the sandbox. Values are whatever the script
|
|
32
|
+
* assigned, so the result is JSON-shaped only if the script assigned JSON-shaped
|
|
33
|
+
* values.
|
|
34
|
+
*
|
|
35
|
+
* Caveats — this is a data extractor, not a sandbox:
|
|
36
|
+
* - Requires `Proxy` and dynamic code eval; a strict CSP without `unsafe-eval`
|
|
37
|
+
* (or an ancient pre-`Proxy` WebView) will make it throw.
|
|
38
|
+
* - The script gets full access to the host's real globals and runs synchronously
|
|
39
|
+
* with no timeout or isolation. **Only evaluate code you trust** — validating the
|
|
40
|
+
* source's origin/trust is the caller's job.
|
|
41
|
+
*/
|
|
42
|
+
export declare function evalJsToJson<T = Record<string, unknown>>(code: string, options?: EvalJsToJsonOptions): T;
|
|
43
|
+
/**
|
|
44
|
+
* Load a standalone JavaScript file and return its global assignments as a plain
|
|
45
|
+
* object. Fetches `http(s):` URLs (browser or Node) and reads `file:` URLs (Node
|
|
46
|
+
* only). Thin loader around {@link evalJsToJson} — see it for capture rules and
|
|
47
|
+
* the trust caveat.
|
|
48
|
+
*
|
|
49
|
+
* @param url Absolute `http:`/`https:` URL (any environment) or `file:` URL (Node).
|
|
50
|
+
*/
|
|
51
|
+
export declare function importJsAsJson<T = Record<string, unknown>>(url: string, options?: ImportJsAsJsonOptions): Promise<T>;
|
|
52
|
+
//# sourceMappingURL=import-js-as-json.d.ts.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { fetchWithRetry } from './fetch-with-retry.js';
|
|
4
|
+
const log = debug('efp:utils:importJsAsJson');
|
|
5
|
+
/**
|
|
6
|
+
* Import a Node.js builtin without the browser bundler trying to resolve it: the
|
|
7
|
+
* specifier is a runtime variable marked `@vite-ignore`, so Vite/Rollup leave it
|
|
8
|
+
* as a plain dynamic import. Only reached for `file:` URLs, which are Node-only.
|
|
9
|
+
*/
|
|
10
|
+
async function importNodeBuiltin(specifier) {
|
|
11
|
+
return (await import(/* @vite-ignore */ specifier));
|
|
12
|
+
}
|
|
13
|
+
/** The real global object, looked up defensively (older WebViews lack `globalThis`). */
|
|
14
|
+
const globalRef = typeof globalThis !== 'undefined'
|
|
15
|
+
? globalThis
|
|
16
|
+
: typeof self !== 'undefined'
|
|
17
|
+
? self
|
|
18
|
+
: typeof window !== 'undefined'
|
|
19
|
+
? window
|
|
20
|
+
: {};
|
|
21
|
+
/**
|
|
22
|
+
* Identifiers pointed back at the sandbox so browser-style scripts (`window.x = …`,
|
|
23
|
+
* `self.y = …`) run unmodified. Treated as inputs — never part of the result.
|
|
24
|
+
*/
|
|
25
|
+
const SELF_REFERENCE_GLOBALS = ['window', 'self', 'globalThis'];
|
|
26
|
+
/**
|
|
27
|
+
* Evaluate a standalone (non-module) JavaScript snippet and return everything it
|
|
28
|
+
* assigned to the global scope as a plain object. **Works in both the browser and
|
|
29
|
+
* Node.js** — no `eval`-free CSP required beyond `unsafe-eval` (see caveats).
|
|
30
|
+
*
|
|
31
|
+
* The snippet runs inside `with (sandbox) { … }`, where `sandbox` is a Proxy that
|
|
32
|
+
* traps every free identifier. So, with no need to know the names in advance:
|
|
33
|
+
* - top-level `var` declarations (`var __data = {…}`) land in the sandbox
|
|
34
|
+
* - bare and browser-style assignments (`__x = …`, `window['__paths'] = …`,
|
|
35
|
+
* `self.x = …`), including dynamically-named ones (`window['__p' + i] = …`)
|
|
36
|
+
* - built-ins the script reads (`JSON`, `Math`, `document`, `fetch`, …) fall
|
|
37
|
+
* through to the real global object
|
|
38
|
+
*
|
|
39
|
+
* Not captured: top-level `let`/`const` and `function` *declarations* — they bind
|
|
40
|
+
* to the wrapper's own scope, not the sandbox. Values are whatever the script
|
|
41
|
+
* assigned, so the result is JSON-shaped only if the script assigned JSON-shaped
|
|
42
|
+
* values.
|
|
43
|
+
*
|
|
44
|
+
* Caveats — this is a data extractor, not a sandbox:
|
|
45
|
+
* - Requires `Proxy` and dynamic code eval; a strict CSP without `unsafe-eval`
|
|
46
|
+
* (or an ancient pre-`Proxy` WebView) will make it throw.
|
|
47
|
+
* - The script gets full access to the host's real globals and runs synchronously
|
|
48
|
+
* with no timeout or isolation. **Only evaluate code you trust** — validating the
|
|
49
|
+
* source's origin/trust is the caller's job.
|
|
50
|
+
*/
|
|
51
|
+
export function evalJsToJson(code, options) {
|
|
52
|
+
if (typeof Proxy === 'undefined') {
|
|
53
|
+
throw new Error('evalJsToJson requires Proxy, which is unavailable in this environment');
|
|
54
|
+
}
|
|
55
|
+
const seeded = options?.globals ?? {};
|
|
56
|
+
const sandbox = { ...seeded };
|
|
57
|
+
const handler = {
|
|
58
|
+
// Trap every identifier lookup inside the `with` block into the sandbox…
|
|
59
|
+
has: () => true,
|
|
60
|
+
get: (target, key) => {
|
|
61
|
+
// …except `Symbol.unscopables`, which `with` consults to decide bindings.
|
|
62
|
+
if (key === Symbol.unscopables)
|
|
63
|
+
return undefined;
|
|
64
|
+
// Point window/self/globalThis at the sandbox so `window.x = …` lands here.
|
|
65
|
+
if (typeof key === 'string' && SELF_REFERENCE_GLOBALS.includes(key))
|
|
66
|
+
return scope;
|
|
67
|
+
// Script-assigned globals win; otherwise fall through to the real globals
|
|
68
|
+
// (JSON, Math, document, …) so the snippet can actually run.
|
|
69
|
+
if (key in target)
|
|
70
|
+
return target[key];
|
|
71
|
+
return globalRef[key];
|
|
72
|
+
},
|
|
73
|
+
set: (target, key, value) => {
|
|
74
|
+
target[key] = value;
|
|
75
|
+
return true;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
const scope = new Proxy(sandbox, handler);
|
|
79
|
+
// A Function-constructed body is always sloppy-mode (even inside this strict ESM
|
|
80
|
+
// module), so `with` is legal here. The trailing newline stops a `//` line comment
|
|
81
|
+
// at the end of `code` from swallowing the closing brace.
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval -- evaluating the legacy `.js` payload is the whole purpose of this module
|
|
83
|
+
const run = new Function('__scope__', `with(__scope__){${code}\n}`);
|
|
84
|
+
run(scope);
|
|
85
|
+
const excluded = new Set([...SELF_REFERENCE_GLOBALS, ...Object.keys(seeded)]);
|
|
86
|
+
return collectGlobals(sandbox, excluded, options?.pick);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Load a standalone JavaScript file and return its global assignments as a plain
|
|
90
|
+
* object. Fetches `http(s):` URLs (browser or Node) and reads `file:` URLs (Node
|
|
91
|
+
* only). Thin loader around {@link evalJsToJson} — see it for capture rules and
|
|
92
|
+
* the trust caveat.
|
|
93
|
+
*
|
|
94
|
+
* @param url Absolute `http:`/`https:` URL (any environment) or `file:` URL (Node).
|
|
95
|
+
*/
|
|
96
|
+
export async function importJsAsJson(url, options) {
|
|
97
|
+
log('Loading JS from', url);
|
|
98
|
+
const code = await loadJsSource(url, options?.signal ?? null);
|
|
99
|
+
return evalJsToJson(code, options);
|
|
100
|
+
}
|
|
101
|
+
async function loadJsSource(url, signal) {
|
|
102
|
+
const parsed = new URL(url);
|
|
103
|
+
if (parsed.protocol === 'file:') {
|
|
104
|
+
log('Read from file system', url);
|
|
105
|
+
const { readFile } = await importNodeBuiltin('node:fs/promises');
|
|
106
|
+
return readFile(parsed, { encoding: 'utf-8', signal: signal ?? undefined });
|
|
107
|
+
}
|
|
108
|
+
log('Fetch', url);
|
|
109
|
+
const response = await fetchWithRetry(url, { signal });
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(`Failed to fetch JS from ${url} (HTTP ${response.status})`);
|
|
112
|
+
}
|
|
113
|
+
return response.text();
|
|
114
|
+
}
|
|
115
|
+
function collectGlobals(sandbox, excluded, pick) {
|
|
116
|
+
const result = {};
|
|
117
|
+
const keys = pick ?? Object.keys(sandbox).filter((key) => !excluded.has(key));
|
|
118
|
+
for (const key of keys) {
|
|
119
|
+
if (Object.prototype.hasOwnProperty.call(sandbox, key)) {
|
|
120
|
+
result[key] = sandbox[key];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
package/dist/lib/import-json.js
CHANGED
|
@@ -72,6 +72,13 @@ async function loadJson(url, fetchCache, signal) {
|
|
|
72
72
|
return (await response.json());
|
|
73
73
|
})();
|
|
74
74
|
fetchCache.set(key, dataPromise);
|
|
75
|
+
// A rejection must not poison the URL for the cache's life (one transient
|
|
76
|
+
// fetch drop would replay as a permanent failure) — evict it so the next
|
|
77
|
+
// importJson retries.
|
|
78
|
+
dataPromise.catch(() => {
|
|
79
|
+
if (fetchCache.get(key) === dataPromise)
|
|
80
|
+
fetchCache.delete(key);
|
|
81
|
+
});
|
|
75
82
|
return dataPromise;
|
|
76
83
|
}
|
|
77
84
|
async function readJsonFromFs(url) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `decodeURIComponent`, or the input unchanged where decoding throws. Query
|
|
3
|
+
* strings arrive from outside the SDK (shared links, embedding pages), so a
|
|
4
|
+
* malformed percent-escape like `?100%booth` is normal input — code on the
|
|
5
|
+
* boot/dispatch path must degrade to the raw string, never die on `URIError`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function safeDecode(value: string): string;
|
|
8
|
+
//# sourceMappingURL=safe-decode.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `decodeURIComponent`, or the input unchanged where decoding throws. Query
|
|
3
|
+
* strings arrive from outside the SDK (shared links, embedding pages), so a
|
|
4
|
+
* malformed percent-escape like `?100%booth` is normal input — code on the
|
|
5
|
+
* boot/dispatch path must degrade to the raw string, never die on `URIError`.
|
|
6
|
+
*/
|
|
7
|
+
export function safeDecode(value) {
|
|
8
|
+
try {
|
|
9
|
+
return decodeURIComponent(value);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `window.localStorage`, or `null` where touching it throws. A cross-origin
|
|
3
|
+
* iframe with third-party cookies blocked throws a `SecurityError` on the
|
|
4
|
+
* property access itself (Chrome/Firefox), so bare `localStorage` reads are
|
|
5
|
+
* unsafe anywhere on the boot path — feature-detect through this instead.
|
|
6
|
+
* Node (no `window`) also lands on `null`. Reads on the returned store work in
|
|
7
|
+
* more environments than writes (quota-0 private modes throw on `setItem`
|
|
8
|
+
* only), so callers that write still need their own probe or try/catch.
|
|
9
|
+
*/
|
|
10
|
+
export declare function safeLocalStorage(): Storage | null;
|
|
11
|
+
//# sourceMappingURL=safe-local-storage.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* `window.localStorage`, or `null` where touching it throws. A cross-origin
|
|
4
|
+
* iframe with third-party cookies blocked throws a `SecurityError` on the
|
|
5
|
+
* property access itself (Chrome/Firefox), so bare `localStorage` reads are
|
|
6
|
+
* unsafe anywhere on the boot path — feature-detect through this instead.
|
|
7
|
+
* Node (no `window`) also lands on `null`. Reads on the returned store work in
|
|
8
|
+
* more environments than writes (quota-0 private modes throw on `setItem`
|
|
9
|
+
* only), so callers that write still need their own probe or try/catch.
|
|
10
|
+
*/
|
|
11
|
+
export function safeLocalStorage() {
|
|
12
|
+
try {
|
|
13
|
+
return window.localStorage;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
CHANGED
package/README.md
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# @expofp/utils
|
|
2
|
-
|
|
3
|
-
Generic, domain-free utilities for the ExpoFP SDK. Bundled with the packages that depend on it — no
|
|
4
|
-
separate install.
|
|
5
|
-
|
|
6
|
-
**Scope rule:** strictly common-use helpers only — no domain logic. Nothing here may know about floor
|
|
7
|
-
plans, booths, exhibitors, or events, or import from a domain package. The test: _could it ship in a
|
|
8
|
-
general-purpose npm utility library unchanged?_ If not, it belongs next to the domain that owns it.
|
|
9
|
-
|
|
10
|
-
- Build: `nx build utils`
|
|
11
|
-
- Test: `nx test utils`
|