@kaciras/utilities
Advanced tools
Comparing version 0.5.1 to 0.6.0
@@ -267,2 +267,80 @@ const htmlEscapes = { | ||
} | ||
/** | ||
* A simple string template engine, only support replace placeholders. | ||
* | ||
* It 10x faster than String.replaceAll() by splits the string ahead of time. | ||
* | ||
* @example | ||
* const template = "<html>...</html>"; | ||
* const newComposite = compositor(template, { | ||
* metadata: "<!--ssr-metadata-->", | ||
* bodyAttrs: /(?<=<body.*?)(?=>)/s, | ||
* appHtml: /(?<=<body.*?>).*(?=<\/body>)/s, | ||
* }); | ||
* | ||
* const c = newComposite(); | ||
* c.put("appHtml", appHtml); | ||
* c.put("metadata", meta); | ||
* c.put("bodyAttrs", ` class="${bodyClass}"`); | ||
* return composite.toString(); | ||
* | ||
* @param template The template string | ||
* @param placeholders An object contains placeholders with its name as key. | ||
*/ function compositor(template, placeholders) { | ||
const nameToSlot = new Map(); | ||
const positions = []; | ||
for (const name of Object.keys(placeholders)){ | ||
const pattern = placeholders[name]; | ||
let startPos; | ||
let endPos; | ||
if (typeof pattern === "string") { | ||
startPos = template.indexOf(pattern); | ||
if (startPos === -1) { | ||
throw new Error("No match for: " + pattern); | ||
} | ||
endPos = startPos + pattern.length; | ||
} else { | ||
const match = pattern.exec(template); | ||
if (!match) { | ||
throw new Error("No match for: " + pattern); | ||
} | ||
startPos = match.index; | ||
endPos = startPos + match[0].length; | ||
} | ||
positions.push({ | ||
name, | ||
startPos, | ||
endPos | ||
}); | ||
} | ||
// Sort by start position so we can check for overlap. | ||
positions.sort((a, b)=>a.startPos - b.startPos); | ||
let lastEnd = 0; | ||
const parts = []; | ||
for(let i = 0; i < positions.length; i++){ | ||
const { name: name1 , startPos: startPos1 , endPos: endPos1 } = positions[i]; | ||
nameToSlot.set(name1, i * 2 + 1); | ||
if (startPos1 < lastEnd) { | ||
throw new Error("Placeholder overlapped."); | ||
} | ||
parts.push(template.slice(lastEnd, startPos1)); | ||
parts.push(template.slice(startPos1, lastEnd = endPos1)); | ||
} | ||
parts.push(template.slice(lastEnd)); | ||
return ()=>new Composite(nameToSlot, [ | ||
...parts | ||
]); | ||
} | ||
class Composite { | ||
constructor(nameToSlot, parts){ | ||
this.parts = parts; | ||
this.nameToSlot = nameToSlot; | ||
} | ||
toString() { | ||
return this.parts.join(""); | ||
} | ||
put(name, value) { | ||
this.parts[this.nameToSlot.get(name)] = value; | ||
} | ||
} | ||
@@ -291,6 +369,118 @@ /** | ||
} | ||
class FetchClientError extends Error { | ||
constructor(response){ | ||
super(`Fetch failed with status: ${response.status}`); | ||
this.response = response; | ||
this.code = response.status; | ||
this.name = "FetchClientError"; | ||
} | ||
} | ||
const defaultRequest = { | ||
credentials: "include" | ||
}; | ||
async function check(response) { | ||
if (response.ok) { | ||
return response; | ||
} | ||
throw new FetchClientError(response); | ||
} | ||
/** | ||
* Wrapper for Promise<Response>, provide status checking and useful method alias. | ||
* | ||
* @example | ||
* // await it will check the response status. | ||
* try { | ||
* const response = await client.get(...); | ||
* assert(response.ok); | ||
* } catch (e) { | ||
* console.error(e.message); | ||
* } | ||
* | ||
* // Use `raw` to get the original response promise. | ||
* const unchecked = await client.get(...).raw; | ||
* | ||
* // Get the response body in JSON format. | ||
* const data = await client.get(...).json<Type>(); | ||
* | ||
* // Get the Location header. | ||
* const location = await apiService.get(...).location; | ||
*/ class ResponseFacade { | ||
constructor(raw){ | ||
this.raw = raw; | ||
} | ||
json() { | ||
return this.then((r)=>r.json()); | ||
} | ||
get location() { | ||
return this.then((r)=>r.headers.get("location")); | ||
} | ||
get [Symbol.toStringTag]() { | ||
return "ResponseFacade"; | ||
} | ||
catch(onRejected) { | ||
return this.raw.then(check).catch(onRejected); | ||
} | ||
finally(onFinally) { | ||
return this.raw.then(check).finally(onFinally); | ||
} | ||
then(onFulfilled, onRejected) { | ||
return this.raw.then(check).then(onFulfilled, onRejected); | ||
} | ||
} | ||
/** | ||
* A very simple helper to make `fetch` simpler. | ||
*/ class FetchClient { | ||
constructor(baseURL = "", init = defaultRequest){ | ||
this.init = init; | ||
this.baseURL = baseURL; | ||
} | ||
fetch(url, method, data, params) { | ||
const { baseURL , init } = this; | ||
// https://github.com/whatwg/url/issues/427 | ||
if (params) { | ||
const searchParams = new URLSearchParams(); | ||
for (const k of Object.keys(params)){ | ||
if (params[k] !== undefined) searchParams.set(k, params[k]); | ||
} | ||
url = `${url}?${searchParams}`; | ||
} | ||
const headers = new Headers(init.headers); | ||
const custom = { | ||
method, | ||
headers | ||
}; | ||
// fetch will auto set content-type if body is some types. | ||
if (data instanceof FormData) { | ||
custom.body = data; | ||
} else if (data) { | ||
custom.body = JSON.stringify(data); | ||
headers.set("content-type", "application/json"); | ||
} | ||
const request = new Request(new URL(url, baseURL), init); | ||
return new ResponseFacade(fetch(request, custom)); | ||
} | ||
head(url, params) { | ||
return this.fetch(url, "HEAD", null, params); | ||
} | ||
get(url, params) { | ||
return this.fetch(url, "GET", null, params); | ||
} | ||
delete(url, params) { | ||
return this.fetch(url, "DELETE", null, params); | ||
} | ||
post(url, data, params) { | ||
return this.fetch(url, "POST", data, params); | ||
} | ||
put(url, data, params) { | ||
return this.fetch(url, "PUT", data, params); | ||
} | ||
patch(url, data, params) { | ||
return this.fetch(url, "PATCH", data, params); | ||
} | ||
} | ||
const NOOP = ()=>{}; | ||
const noop = ()=>{}; | ||
const identity = (v)=>v; | ||
/** | ||
* An AbortSignal object that never aborts. | ||
* An AbortSignal that never aborts. | ||
*/ const NeverAbort = { | ||
@@ -342,3 +532,3 @@ aborted: false, | ||
*/ function silencePromise(value) { | ||
if (typeof value?.then === "function") value.catch(NOOP); | ||
if (typeof value?.then === "function") value.catch(noop); | ||
} | ||
@@ -467,3 +657,3 @@ /** | ||
isError: true | ||
}, []); | ||
}); | ||
} | ||
@@ -668,2 +858,2 @@ } | ||
export { AbortError, LRUCache, MultiEventEmitter, MultiMap, NOOP, NeverAbort, rpc as RPC, SingleEventEmitter, TimeUnit, base64url, blobToBase64URL, escapeHTML, fetchFile, formatDuration, formatSize, parseSize, saveFile, selectFile, sha256, silencePromise, sleep, svgToUrl, uniqueId }; | ||
export { AbortError, Composite, FetchClient, FetchClientError, LRUCache, MultiEventEmitter, MultiMap, NeverAbort, rpc as RPC, ResponseFacade, SingleEventEmitter, TimeUnit, base64url, blobToBase64URL, compositor, escapeHTML, fetchFile, formatDuration, formatSize, identity, noop, parseSize, saveFile, selectFile, sha256, silencePromise, sleep, svgToUrl, uniqueId }; |
223
dist/node.js
@@ -157,2 +157,136 @@ import process from 'process'; | ||
/** | ||
* Fetch the resource into a File object. | ||
* | ||
* @param request This defines the resource that you wish to fetch. | ||
* @param init An object containing any custom settings that you want to apply to the request. | ||
*/ async function fetchFile(request, init) { | ||
const url = typeof request === "string" ? request : request.url; | ||
const response = await fetch(request, init); | ||
if (!response.ok) { | ||
throw new Error(`Failed to fetch (${response.status}) ${url}`); | ||
} | ||
const blob = await response.blob(); | ||
const timeHeader = response.headers.get("last-modified"); | ||
const lastModified = timeHeader ? new Date(timeHeader).getTime() : undefined; | ||
const name = new URL(url).pathname.split("/").at(-1) || "download"; | ||
return new File([ | ||
blob | ||
], name, { | ||
type: blob.type, | ||
lastModified | ||
}); | ||
} | ||
class FetchClientError extends Error { | ||
constructor(response){ | ||
super(`Fetch failed with status: ${response.status}`); | ||
this.response = response; | ||
this.code = response.status; | ||
this.name = "FetchClientError"; | ||
} | ||
} | ||
const defaultRequest = { | ||
credentials: "include" | ||
}; | ||
async function check(response) { | ||
if (response.ok) { | ||
return response; | ||
} | ||
throw new FetchClientError(response); | ||
} | ||
/** | ||
* Wrapper for Promise<Response>, provide status checking and useful method alias. | ||
* | ||
* @example | ||
* // await it will check the response status. | ||
* try { | ||
* const response = await client.get(...); | ||
* assert(response.ok); | ||
* } catch (e) { | ||
* console.error(e.message); | ||
* } | ||
* | ||
* // Use `raw` to get the original response promise. | ||
* const unchecked = await client.get(...).raw; | ||
* | ||
* // Get the response body in JSON format. | ||
* const data = await client.get(...).json<Type>(); | ||
* | ||
* // Get the Location header. | ||
* const location = await apiService.get(...).location; | ||
*/ class ResponseFacade { | ||
constructor(raw){ | ||
this.raw = raw; | ||
} | ||
json() { | ||
return this.then((r)=>r.json()); | ||
} | ||
get location() { | ||
return this.then((r)=>r.headers.get("location")); | ||
} | ||
get [Symbol.toStringTag]() { | ||
return "ResponseFacade"; | ||
} | ||
catch(onRejected) { | ||
return this.raw.then(check).catch(onRejected); | ||
} | ||
finally(onFinally) { | ||
return this.raw.then(check).finally(onFinally); | ||
} | ||
then(onFulfilled, onRejected) { | ||
return this.raw.then(check).then(onFulfilled, onRejected); | ||
} | ||
} | ||
/** | ||
* A very simple helper to make `fetch` simpler. | ||
*/ class FetchClient { | ||
constructor(baseURL = "", init = defaultRequest){ | ||
this.init = init; | ||
this.baseURL = baseURL; | ||
} | ||
fetch(url, method, data, params) { | ||
const { baseURL , init } = this; | ||
// https://github.com/whatwg/url/issues/427 | ||
if (params) { | ||
const searchParams = new URLSearchParams(); | ||
for (const k of Object.keys(params)){ | ||
if (params[k] !== undefined) searchParams.set(k, params[k]); | ||
} | ||
url = `${url}?${searchParams}`; | ||
} | ||
const headers = new Headers(init.headers); | ||
const custom = { | ||
method, | ||
headers | ||
}; | ||
// fetch will auto set content-type if body is some types. | ||
if (data instanceof FormData) { | ||
custom.body = data; | ||
} else if (data) { | ||
custom.body = JSON.stringify(data); | ||
headers.set("content-type", "application/json"); | ||
} | ||
const request = new Request(new URL(url, baseURL), init); | ||
return new ResponseFacade(fetch(request, custom)); | ||
} | ||
head(url, params) { | ||
return this.fetch(url, "HEAD", null, params); | ||
} | ||
get(url, params) { | ||
return this.fetch(url, "GET", null, params); | ||
} | ||
delete(url, params) { | ||
return this.fetch(url, "DELETE", null, params); | ||
} | ||
post(url, data, params) { | ||
return this.fetch(url, "POST", data, params); | ||
} | ||
put(url, data, params) { | ||
return this.fetch(url, "PUT", data, params); | ||
} | ||
patch(url, data, params) { | ||
return this.fetch(url, "PATCH", data, params); | ||
} | ||
} | ||
var TimeUnit; | ||
@@ -273,6 +407,85 @@ (function(TimeUnit) { | ||
} | ||
/** | ||
* A simple string template engine, only support replace placeholders. | ||
* | ||
* It 10x faster than String.replaceAll() by splits the string ahead of time. | ||
* | ||
* @example | ||
* const template = "<html>...</html>"; | ||
* const newComposite = compositor(template, { | ||
* metadata: "<!--ssr-metadata-->", | ||
* bodyAttrs: /(?<=<body.*?)(?=>)/s, | ||
* appHtml: /(?<=<body.*?>).*(?=<\/body>)/s, | ||
* }); | ||
* | ||
* const c = newComposite(); | ||
* c.put("appHtml", appHtml); | ||
* c.put("metadata", meta); | ||
* c.put("bodyAttrs", ` class="${bodyClass}"`); | ||
* return composite.toString(); | ||
* | ||
* @param template The template string | ||
* @param placeholders An object contains placeholders with its name as key. | ||
*/ function compositor(template, placeholders) { | ||
const nameToSlot = new Map(); | ||
const positions = []; | ||
for (const name of Object.keys(placeholders)){ | ||
const pattern = placeholders[name]; | ||
let startPos; | ||
let endPos; | ||
if (typeof pattern === "string") { | ||
startPos = template.indexOf(pattern); | ||
if (startPos === -1) { | ||
throw new Error("No match for: " + pattern); | ||
} | ||
endPos = startPos + pattern.length; | ||
} else { | ||
const match = pattern.exec(template); | ||
if (!match) { | ||
throw new Error("No match for: " + pattern); | ||
} | ||
startPos = match.index; | ||
endPos = startPos + match[0].length; | ||
} | ||
positions.push({ | ||
name, | ||
startPos, | ||
endPos | ||
}); | ||
} | ||
// Sort by start position so we can check for overlap. | ||
positions.sort((a, b)=>a.startPos - b.startPos); | ||
let lastEnd = 0; | ||
const parts = []; | ||
for(let i = 0; i < positions.length; i++){ | ||
const { name: name1 , startPos: startPos1 , endPos: endPos1 } = positions[i]; | ||
nameToSlot.set(name1, i * 2 + 1); | ||
if (startPos1 < lastEnd) { | ||
throw new Error("Placeholder overlapped."); | ||
} | ||
parts.push(template.slice(lastEnd, startPos1)); | ||
parts.push(template.slice(startPos1, lastEnd = endPos1)); | ||
} | ||
parts.push(template.slice(lastEnd)); | ||
return ()=>new Composite(nameToSlot, [ | ||
...parts | ||
]); | ||
} | ||
class Composite { | ||
constructor(nameToSlot, parts){ | ||
this.parts = parts; | ||
this.nameToSlot = nameToSlot; | ||
} | ||
toString() { | ||
return this.parts.join(""); | ||
} | ||
put(name, value) { | ||
this.parts[this.nameToSlot.get(name)] = value; | ||
} | ||
} | ||
const NOOP = ()=>{}; | ||
const noop = ()=>{}; | ||
const identity = (v)=>v; | ||
/** | ||
* An AbortSignal object that never aborts. | ||
* An AbortSignal that never aborts. | ||
*/ const NeverAbort = { | ||
@@ -325,3 +538,3 @@ aborted: false, | ||
*/ function silencePromise(value) { | ||
if (typeof value?.then === "function") value.catch(NOOP); | ||
if (typeof value?.then === "function") value.catch(noop); | ||
} | ||
@@ -410,3 +623,3 @@ /** | ||
isError: true | ||
}, []); | ||
}); | ||
} | ||
@@ -639,2 +852,2 @@ } | ||
export { AbortError, LRUCache, MultiEventEmitter, MultiMap, NOOP, NeverAbort, rpc as RPC, SingleEventEmitter, TimeUnit, base64url, blobToBase64URL, escapeHTML, formatDuration, formatSize, onExit, parseSize, sha256, silencePromise, sleep, svgToUrl, uniqueId }; | ||
export { AbortError, Composite, FetchClient, FetchClientError, LRUCache, MultiEventEmitter, MultiMap, NeverAbort, rpc as RPC, ResponseFacade, SingleEventEmitter, TimeUnit, base64url, blobToBase64URL, compositor, escapeHTML, fetchFile, formatDuration, formatSize, identity, noop, onExit, parseSize, sha256, silencePromise, sleep, svgToUrl, uniqueId }; |
{ | ||
"name": "@kaciras/utilities", | ||
"version": "0.5.1", | ||
"version": "0.6.0", | ||
"license": "MIT", | ||
@@ -26,12 +26,13 @@ "description": "A set of common JS functions for node and browser.", | ||
"@kaciras/eslint-config-typescript": "^2.5.0", | ||
"@rollup/plugin-replace": "^5.0.1", | ||
"@stryker-mutator/core": "^6.3.0", | ||
"@stryker-mutator/jest-runner": "^6.3.0", | ||
"@swc/core": "^1.3.21", | ||
"@swc/jest": "^0.2.23", | ||
"@types/node": "^18.11.11", | ||
"eslint": "^8.29.0", | ||
"@rollup/plugin-replace": "^5.0.2", | ||
"@stryker-mutator/core": "^6.3.1", | ||
"@stryker-mutator/jest-runner": "^6.3.1", | ||
"@swc/core": "^1.3.24", | ||
"@swc/jest": "^0.2.24", | ||
"@types/node": "^18.11.18", | ||
"eslint": "^8.30.0", | ||
"is-builtin-module": "^3.2.0", | ||
"jest": "^29.3.1", | ||
"rollup": "^3.6.0", | ||
"mockttp": "^3.6.2", | ||
"rollup": "^3.8.1", | ||
"typescript": "^4.9.4" | ||
@@ -38,0 +39,0 @@ }, |
62695
2021
16
4