@conorroberts/utils 0.0.42 → 0.0.44

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/images.d.mts CHANGED
@@ -26,6 +26,27 @@ interface CreateImageUrlResponse {
26
26
  errors: unknown[];
27
27
  messages: unknown[];
28
28
  }
29
+ interface CloudflareImagesV1Response {
30
+ result: {
31
+ images: CloudflareImage[];
32
+ };
33
+ success: boolean;
34
+ errors: CloudflareApiError[];
35
+ messages: string[];
36
+ }
37
+ interface CloudflareImage {
38
+ id: string;
39
+ filename: string;
40
+ uploaded: string;
41
+ requireSignedURLs: boolean;
42
+ variants: string[];
43
+ meta?: Record<string, any>;
44
+ creator?: string | null;
45
+ }
46
+ interface CloudflareApiError {
47
+ code: number;
48
+ message: string;
49
+ }
29
50
  declare class ImageUtils<ImageIds extends Record<string, any>> {
30
51
  private blacklist;
31
52
  private accountId;
@@ -55,8 +76,9 @@ declare class ImageUtils<ImageIds extends Record<string, any>> {
55
76
  id: string;
56
77
  }[]>;
57
78
  serverUpload(data: Blob, args: {
79
+ id?: string;
58
80
  apiKey: string;
59
- }): Promise<any>;
81
+ }): Promise<CloudflareImagesV1Response>;
60
82
  upload(url: string, body: FormData): Promise<string>;
61
83
  delete(id: string, args: {
62
84
  apiKey: string;
package/dist/images.mjs CHANGED
@@ -82,7 +82,7 @@ var ImageUtils = class {
82
82
  formData.append("file", data, nanoid());
83
83
  const headers = new Headers();
84
84
  headers.set("Authorization", `Bearer ${args.apiKey}`);
85
- return (await fetch(`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/images/v1`, {
85
+ return await (await fetch(`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/images/v1`, {
86
86
  method: "POST",
87
87
  headers,
88
88
  body: formData
@@ -1 +1 @@
1
- {"version":3,"file":"images.mjs","names":[],"sources":["../src/images.ts"],"sourcesContent":["import { nanoid } from \"nanoid\";\nimport dayjs from \"dayjs\";\nimport { ofetch } from \"ofetch\";\n\nexport interface OptimizedImageOptions {\n anim?: boolean;\n background?: string;\n blur?: number;\n brightness?: number;\n compression?: \"fast\"; // faster compression = larger file size\n contrast?: number;\n dpr?: number;\n fit?: \"scale-down\" | \"contain\" | \"cover\" | \"crop\" | \"pad\";\n format?: \"webp\" | \"avif\" | \"json\";\n gamma?: number;\n width?: number;\n height?: number;\n metadata?: \"keep\" | \"copyright\" | \"none\";\n quality?: number;\n rotate?: number;\n sharpen?: number;\n}\n\nexport interface CreateImageUrlResponse {\n result: {\n id: string;\n uploadURL: string;\n };\n success: boolean;\n errors: unknown[];\n messages: unknown[];\n}\n\ninterface UploadImageResponse {\n result: {\n id: string;\n filename: string;\n uploaded: string;\n requireSignedURLs: boolean;\n variants: string[];\n };\n success: boolean;\n errors: unknown[];\n messages: unknown[];\n}\n\nexport class ImageUtils<ImageIds extends Record<string, any>> {\n private blacklist: string[] = [\"img.clerk.com\"];\n private accountId: string;\n private accountHash: string;\n private _imageIds: ImageIds | undefined;\n\n constructor(args: {\n accountId: string;\n accountHash: string;\n blacklist?: string[];\n imageIds?: ImageIds;\n }) {\n this.accountId = args.accountId;\n this.accountHash = args.accountHash;\n\n this._imageIds = args.imageIds;\n\n if (args.blacklist) {\n this.blacklist.push(...args.blacklist);\n }\n }\n\n get imageIds() {\n if (!this._imageIds) {\n throw new Error(\"imageIds was not supplied in constructor\");\n }\n\n return this._imageIds;\n }\n\n public url(id: string) {\n return `https://imagedelivery.net/${this.accountHash}/${id}/public`;\n }\n\n private isBlacklisted(url: string) {\n return this.blacklist.some((u) => url.includes(u));\n }\n\n private isProtected(id: string) {\n if (!this._imageIds) {\n return false;\n }\n\n return Object.values(this._imageIds).some((e) => e === id);\n }\n\n /**\n * Will only operate on images that have been uploaded via cloudflare images\n */\n public optimizeUrl(url: string, options: OptimizedImageOptions) {\n if (this.isBlacklisted(url)) {\n return url;\n }\n\n // Final format should look similar to: https://imagedelivery.net/<ACCOUNT_HASH>/<IMAGE_ID>/w=400,sharpen=3\n return url.replace(\"public\", this.createImageOptionsString(options));\n }\n\n public optimizeId(id: string, options: OptimizedImageOptions) {\n return this.optimizeUrl(this.url(id), options);\n }\n\n public createOptionsSearchParams(options: OptimizedImageOptions) {\n const params = new URLSearchParams();\n\n const pairs = Object.entries(options);\n\n for (const [key, val] of pairs) {\n if (val === undefined) {\n continue;\n }\n\n params.set(key, val.toString());\n }\n\n return params;\n }\n\n public createImageOptionsString(options: OptimizedImageOptions) {\n const params = this.createOptionsSearchParams(options);\n\n return Array.from(params.entries())\n .map(([key, val]) => `${key}=${val}`)\n .join(\",\");\n }\n\n public async createUploadUrls(count: number, args: { apiKey: string }) {\n if (count === 0) {\n return [];\n }\n\n const headers = new Headers();\n headers.set(\"Authorization\", `Bearer ${args.apiKey}`);\n\n const urls = await Promise.all(\n Array.from({ length: count }).map(async () => {\n try {\n const form = new FormData();\n const id = nanoid();\n form.append(\"id\", id);\n form.append(\"expiry\", dayjs().add(5, \"minute\").toISOString());\n\n const img = await ofetch<CreateImageUrlResponse>(\n `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/images/v2/direct_upload`,\n { method: \"POST\", headers, body: form },\n );\n\n if (!img.success) {\n throw new Error(\"Error uploading image\");\n }\n\n return { url: img.result.uploadURL, id };\n } catch (e) {\n console.error(\"Error uploading image\");\n throw e;\n }\n }),\n );\n\n return urls;\n }\n\n public async serverUpload(data: Blob, args: { apiKey: string }) {\n const formData = new FormData();\n formData.append(\"file\", data, nanoid());\n\n const headers = new Headers();\n headers.set(\"Authorization\", `Bearer ${args.apiKey}`);\n\n const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/images/v1`, {\n method: \"POST\",\n headers,\n body: formData,\n });\n\n return response.json();\n }\n\n public async upload(url: string, body: FormData) {\n const fetchResponse = await ofetch<UploadImageResponse>(url, {\n method: \"POST\",\n body,\n });\n\n if (!fetchResponse.success) {\n throw new Error(\"Failed to upload image\");\n }\n\n const downloadUrl = fetchResponse.result.variants[0];\n\n if (!downloadUrl) {\n throw new Error(\"Could not find download URL\");\n }\n\n return downloadUrl;\n }\n\n public async delete(id: string, args: { apiKey: string }) {\n if (this.isProtected(id)) {\n return { success: true };\n }\n\n try {\n const headers = new Headers();\n headers.set(\"Authorization\", `Bearer ${args.apiKey}`);\n\n await ofetch(`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/images/v1/${id}`, {\n method: \"POST\",\n headers,\n });\n return { success: true };\n } catch {\n return { success: false };\n }\n }\n\n public async batchUpload(files: { file: File; url: { id: string; value: string } }[]) {\n return await Promise.all(\n files.map(async (e) => {\n const formData = new FormData();\n formData.append(\"file\", e.file);\n\n const downloadUrl = await this.upload(e.url.value, formData);\n\n return {\n url: downloadUrl,\n id: e.url.id,\n };\n }),\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"images.mjs","names":[],"sources":["../src/images.ts"],"sourcesContent":["import { nanoid } from \"nanoid\";\nimport dayjs from \"dayjs\";\nimport { ofetch } from \"ofetch\";\n\nexport interface OptimizedImageOptions {\n anim?: boolean;\n background?: string;\n blur?: number;\n brightness?: number;\n compression?: \"fast\"; // faster compression = larger file size\n contrast?: number;\n dpr?: number;\n fit?: \"scale-down\" | \"contain\" | \"cover\" | \"crop\" | \"pad\";\n format?: \"webp\" | \"avif\" | \"json\";\n gamma?: number;\n width?: number;\n height?: number;\n metadata?: \"keep\" | \"copyright\" | \"none\";\n quality?: number;\n rotate?: number;\n sharpen?: number;\n}\n\nexport interface CreateImageUrlResponse {\n result: {\n id: string;\n uploadURL: string;\n };\n success: boolean;\n errors: unknown[];\n messages: unknown[];\n}\n\ninterface UploadImageResponse {\n result: {\n id: string;\n filename: string;\n uploaded: string;\n requireSignedURLs: boolean;\n variants: string[];\n };\n success: boolean;\n errors: unknown[];\n messages: unknown[];\n}\n\ninterface CloudflareImagesV1Response {\n result: {\n images: CloudflareImage[];\n };\n success: boolean;\n errors: CloudflareApiError[];\n messages: string[];\n}\n\ninterface CloudflareImage {\n id: string; // Unique image identifier\n filename: string; // Original filename\n uploaded: string; // ISO 8601 date-time string\n requireSignedURLs: boolean;\n variants: string[]; // Array of URLs for the image variants\n meta?: Record<string, any>; // User modifiable key-value store (max 1024 bytes)\n creator?: string | null; // Internal user ID (optional)\n}\n\ninterface CloudflareApiError {\n code: number;\n message: string;\n}\n\nexport class ImageUtils<ImageIds extends Record<string, any>> {\n private blacklist: string[] = [\"img.clerk.com\"];\n private accountId: string;\n private accountHash: string;\n private _imageIds: ImageIds | undefined;\n\n constructor(args: {\n accountId: string;\n accountHash: string;\n blacklist?: string[];\n imageIds?: ImageIds;\n }) {\n this.accountId = args.accountId;\n this.accountHash = args.accountHash;\n\n this._imageIds = args.imageIds;\n\n if (args.blacklist) {\n this.blacklist.push(...args.blacklist);\n }\n }\n\n get imageIds() {\n if (!this._imageIds) {\n throw new Error(\"imageIds was not supplied in constructor\");\n }\n\n return this._imageIds;\n }\n\n public url(id: string) {\n return `https://imagedelivery.net/${this.accountHash}/${id}/public`;\n }\n\n private isBlacklisted(url: string) {\n return this.blacklist.some((u) => url.includes(u));\n }\n\n private isProtected(id: string) {\n if (!this._imageIds) {\n return false;\n }\n\n return Object.values(this._imageIds).some((e) => e === id);\n }\n\n /**\n * Will only operate on images that have been uploaded via cloudflare images\n */\n public optimizeUrl(url: string, options: OptimizedImageOptions) {\n if (this.isBlacklisted(url)) {\n return url;\n }\n\n // Final format should look similar to: https://imagedelivery.net/<ACCOUNT_HASH>/<IMAGE_ID>/w=400,sharpen=3\n return url.replace(\"public\", this.createImageOptionsString(options));\n }\n\n public optimizeId(id: string, options: OptimizedImageOptions) {\n return this.optimizeUrl(this.url(id), options);\n }\n\n public createOptionsSearchParams(options: OptimizedImageOptions) {\n const params = new URLSearchParams();\n\n const pairs = Object.entries(options);\n\n for (const [key, val] of pairs) {\n if (val === undefined) {\n continue;\n }\n\n params.set(key, val.toString());\n }\n\n return params;\n }\n\n public createImageOptionsString(options: OptimizedImageOptions) {\n const params = this.createOptionsSearchParams(options);\n\n return Array.from(params.entries())\n .map(([key, val]) => `${key}=${val}`)\n .join(\",\");\n }\n\n public async createUploadUrls(count: number, args: { apiKey: string }) {\n if (count === 0) {\n return [];\n }\n\n const headers = new Headers();\n headers.set(\"Authorization\", `Bearer ${args.apiKey}`);\n\n const urls = await Promise.all(\n Array.from({ length: count }).map(async () => {\n try {\n const form = new FormData();\n const id = nanoid();\n form.append(\"id\", id);\n form.append(\"expiry\", dayjs().add(5, \"minute\").toISOString());\n\n const img = await ofetch<CreateImageUrlResponse>(\n `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/images/v2/direct_upload`,\n { method: \"POST\", headers, body: form },\n );\n\n if (!img.success) {\n throw new Error(\"Error uploading image\");\n }\n\n return { url: img.result.uploadURL, id };\n } catch (e) {\n console.error(\"Error uploading image\");\n throw e;\n }\n }),\n );\n\n return urls;\n }\n\n public async serverUpload(data: Blob, args: { id?: string; apiKey: string }) {\n const formData = new FormData();\n formData.append(\"file\", data, nanoid());\n\n const headers = new Headers();\n headers.set(\"Authorization\", `Bearer ${args.apiKey}`);\n\n const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/images/v1`, {\n method: \"POST\",\n headers,\n body: formData,\n });\n\n const json: CloudflareImagesV1Response = await response.json();\n\n return json;\n }\n\n public async upload(url: string, body: FormData) {\n const fetchResponse = await ofetch<UploadImageResponse>(url, {\n method: \"POST\",\n body,\n });\n\n if (!fetchResponse.success) {\n throw new Error(\"Failed to upload image\");\n }\n\n const downloadUrl = fetchResponse.result.variants[0];\n\n if (!downloadUrl) {\n throw new Error(\"Could not find download URL\");\n }\n\n return downloadUrl;\n }\n\n public async delete(id: string, args: { apiKey: string }) {\n if (this.isProtected(id)) {\n return { success: true };\n }\n\n try {\n const headers = new Headers();\n headers.set(\"Authorization\", `Bearer ${args.apiKey}`);\n\n await ofetch(`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/images/v1/${id}`, {\n method: \"POST\",\n headers,\n });\n return { success: true };\n } catch {\n return { success: false };\n }\n }\n\n public async batchUpload(files: { file: File; url: { id: string; value: string } }[]) {\n return await Promise.all(\n files.map(async (e) => {\n const formData = new FormData();\n formData.append(\"file\", e.file);\n\n const downloadUrl = await this.upload(e.url.value, formData);\n\n return {\n url: downloadUrl,\n id: e.url.id,\n };\n }),\n );\n }\n}\n"],"mappings":"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"}
@@ -1,20 +1,20 @@
1
- import * as oxlint15 from "oxlint";
1
+ import * as oxlint24 from "oxlint";
2
2
 
3
3
  //#region src/oxlint-plugins/jsx-component-pascal-case.d.ts
4
- declare const jsxComponentPascalCaseRule: oxlint15.Rule;
4
+ declare const jsxComponentPascalCaseRule: oxlint24.Rule;
5
5
  declare namespace _default {
6
6
  namespace meta {
7
7
  let name: string;
8
8
  }
9
9
  let rules: {
10
- "jsx-component-pascal-case": oxlint15.Rule;
10
+ "jsx-component-pascal-case": oxlint24.Rule;
11
11
  };
12
12
  }
13
- type RuleContext = oxlint15.Context;
14
- type ESTNode = oxlint15.ESTree.Node;
15
- type ESTExpression = oxlint15.ESTree.Expression;
16
- type ReturnStatementNode = oxlint15.ESTree.ReturnStatement;
17
- type FunctionLikeNode = oxlint15.ESTree.Function | oxlint15.ESTree.ArrowFunctionExpression;
13
+ type RuleContext = oxlint24.Context;
14
+ type ESTNode = oxlint24.ESTree.Node;
15
+ type ESTExpression = oxlint24.ESTree.Expression;
16
+ type ReturnStatementNode = oxlint24.ESTree.ReturnStatement;
17
+ type FunctionLikeNode = oxlint24.ESTree.Function | oxlint24.ESTree.ArrowFunctionExpression;
18
18
  type FunctionContext = {
19
19
  node: FunctionLikeNode;
20
20
  name: string;
@@ -1,20 +1,20 @@
1
- import * as oxlint35 from "oxlint";
1
+ import * as oxlint32 from "oxlint";
2
2
 
3
3
  //#region src/oxlint-plugins/no-component-date-instantiation.d.ts
4
- declare const noComponentDateInstantiationRule: oxlint35.Rule;
4
+ declare const noComponentDateInstantiationRule: oxlint32.Rule;
5
5
  declare namespace _default {
6
6
  namespace meta {
7
7
  let name: string;
8
8
  }
9
9
  let rules: {
10
- "no-component-date-instantiation": oxlint35.Rule;
10
+ "no-component-date-instantiation": oxlint32.Rule;
11
11
  };
12
12
  }
13
- type RuleContext = oxlint35.Context;
14
- type ESTNode = oxlint35.ESTree.Node;
15
- type NewExpressionNode = oxlint35.ESTree.NewExpression;
16
- type ReturnStatementNode = oxlint35.ESTree.ReturnStatement;
17
- type FunctionLikeNode = oxlint35.ESTree.Function | oxlint35.ESTree.ArrowFunctionExpression;
13
+ type RuleContext = oxlint32.Context;
14
+ type ESTNode = oxlint32.ESTree.Node;
15
+ type NewExpressionNode = oxlint32.ESTree.NewExpression;
16
+ type ReturnStatementNode = oxlint32.ESTree.ReturnStatement;
17
+ type FunctionLikeNode = oxlint32.ESTree.Function | oxlint32.ESTree.ArrowFunctionExpression;
18
18
  type FunctionContext = {
19
19
  node: FunctionLikeNode;
20
20
  parent: FunctionContext | null;
@@ -1,16 +1,16 @@
1
- import * as oxlint29 from "oxlint";
1
+ import * as oxlint10 from "oxlint";
2
2
 
3
3
  //#region src/oxlint-plugins/no-emoji.d.ts
4
- declare const noEmojiRule: oxlint29.Rule;
4
+ declare const noEmojiRule: oxlint10.Rule;
5
5
  declare namespace _default {
6
6
  namespace meta {
7
7
  let name: string;
8
8
  }
9
9
  let rules: {
10
- "no-emoji": oxlint29.Rule;
10
+ "no-emoji": oxlint10.Rule;
11
11
  };
12
12
  }
13
- type ESTNode = oxlint29.ESTree.Node;
13
+ type ESTNode = oxlint10.ESTree.Node;
14
14
  //#endregion
15
15
  export { ESTNode, _default as default, noEmojiRule };
16
16
  //# sourceMappingURL=no-emoji.d.mts.map
@@ -1,16 +1,16 @@
1
- import * as oxlint12 from "oxlint";
1
+ import * as oxlint18 from "oxlint";
2
2
 
3
3
  //#region src/oxlint-plugins/no-finally.d.ts
4
- declare const noFinallyRule: oxlint12.Rule;
4
+ declare const noFinallyRule: oxlint18.Rule;
5
5
  declare namespace _default {
6
6
  namespace meta {
7
7
  let name: string;
8
8
  }
9
9
  let rules: {
10
- "no-finally": oxlint12.Rule;
10
+ "no-finally": oxlint18.Rule;
11
11
  };
12
12
  }
13
- type ESTNode = oxlint12.ESTree.Node;
13
+ type ESTNode = oxlint18.ESTree.Node;
14
14
  //#endregion
15
15
  export { ESTNode, _default as default, noFinallyRule };
16
16
  //# sourceMappingURL=no-finally.d.mts.map
@@ -1,16 +1,16 @@
1
- import * as oxlint26 from "oxlint";
1
+ import * as oxlint21 from "oxlint";
2
2
 
3
3
  //#region src/oxlint-plugins/no-react-namespace.d.ts
4
- declare const noReactNamespaceRule: oxlint26.Rule;
4
+ declare const noReactNamespaceRule: oxlint21.Rule;
5
5
  declare namespace _default {
6
6
  namespace meta {
7
7
  let name: string;
8
8
  }
9
9
  let rules: {
10
- "no-react-namespace": oxlint26.Rule;
10
+ "no-react-namespace": oxlint21.Rule;
11
11
  };
12
12
  }
13
- type ESTNode = oxlint26.ESTree.Node;
13
+ type ESTNode = oxlint21.ESTree.Node;
14
14
  //#endregion
15
15
  export { ESTNode, _default as default, noReactNamespaceRule };
16
16
  //# sourceMappingURL=no-react-namespace.d.mts.map
@@ -1,4 +1,4 @@
1
- import * as oxlint10 from "oxlint";
1
+ import * as oxlint16 from "oxlint";
2
2
 
3
3
  //#region src/oxlint-plugins/no-switch-plugin.d.ts
4
4
  declare namespace _default {
@@ -6,10 +6,10 @@ declare namespace _default {
6
6
  let name: string;
7
7
  }
8
8
  let rules: {
9
- "no-switch": oxlint10.Rule;
9
+ "no-switch": oxlint16.Rule;
10
10
  };
11
11
  }
12
- type ESTNode = oxlint10.ESTree.Node;
12
+ type ESTNode = oxlint16.ESTree.Node;
13
13
  //#endregion
14
14
  export { ESTNode, _default as default };
15
15
  //# sourceMappingURL=no-switch-plugin.d.mts.map
@@ -1,16 +1,16 @@
1
- import * as oxlint32 from "oxlint";
1
+ import * as oxlint13 from "oxlint";
2
2
 
3
3
  //#region src/oxlint-plugins/no-top-level-let.d.ts
4
- declare const noTopLevelLetRule: oxlint32.Rule;
4
+ declare const noTopLevelLetRule: oxlint13.Rule;
5
5
  declare namespace _default {
6
6
  namespace meta {
7
7
  let name: string;
8
8
  }
9
9
  let rules: {
10
- "no-top-level-let": oxlint32.Rule;
10
+ "no-top-level-let": oxlint13.Rule;
11
11
  };
12
12
  }
13
- type ESTNode = oxlint32.ESTree.Node;
13
+ type ESTNode = oxlint13.ESTree.Node;
14
14
  //#endregion
15
15
  export { ESTNode, _default as default, noTopLevelLetRule };
16
16
  //# sourceMappingURL=no-top-level-let.d.mts.map
@@ -1,16 +1,16 @@
1
- import * as oxlint23 from "oxlint";
1
+ import * as oxlint40 from "oxlint";
2
2
 
3
3
  //#region src/oxlint-plugins/no-type-cast.d.ts
4
- declare const noTypeCastRule: oxlint23.Rule;
4
+ declare const noTypeCastRule: oxlint40.Rule;
5
5
  declare namespace _default {
6
6
  namespace meta {
7
7
  let name: string;
8
8
  }
9
9
  let rules: {
10
- "no-type-cast": oxlint23.Rule;
10
+ "no-type-cast": oxlint40.Rule;
11
11
  };
12
12
  }
13
- type ESTNode = oxlint23.ESTree.Node;
13
+ type ESTNode = oxlint40.ESTree.Node;
14
14
  //#endregion
15
15
  export { ESTNode, _default as default, noTypeCastRule };
16
16
  //# sourceMappingURL=no-type-cast.d.mts.map
@@ -0,0 +1,57 @@
1
+ //#region src/react/useStableCallback.d.ts
2
+ type AnyFunction = (...args: any[]) => any;
3
+ /**
4
+ * Creates a stable callback that always calls the latest version of the function.
5
+ * Useful for callbacks that need to be used in dependency arrays but should always
6
+ * execute the most recent version of the callback.
7
+ */
8
+ declare const useStableCallback: <T extends AnyFunction>(callback: T) => T;
9
+ //#endregion
10
+ //#region src/react/useOnce.d.ts
11
+ /**
12
+ * Runs a callback only once when a condition becomes truthy.
13
+ * The callback is stabilized internally to always reference the latest version.
14
+ *
15
+ * @param condition - When truthy (evaluated via Boolean()), the callback will be executed (only once).
16
+ * @param callback - The function to run once when the condition is met.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * const isReady = true;
21
+ * useOnce(isReady, () => {
22
+ * console.log("Ready!");
23
+ * });
24
+ * ```
25
+ */
26
+ declare const useOnce: (condition: unknown, callback: () => void) => void;
27
+ //#endregion
28
+ //#region src/react/useLocalOnce.d.ts
29
+ /**
30
+ * Runs a callback only once when a condition becomes truthy, executing synchronously
31
+ * during render. The callback is stabilized internally to always reference the latest version.
32
+ *
33
+ * Unlike `useOnce`, this runs at the top level of the hook (not in useEffect),
34
+ * making it suitable for state updates that need to happen synchronously during render.
35
+ *
36
+ * @param condition - When truthy (evaluated via Boolean()), the callback will be executed (only once).
37
+ * @param callback - The function to run once when the condition is met.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * const user: User | null = getUser();
42
+ * useLocalOnce(user, () => {
43
+ * setState(user.name);
44
+ * });
45
+ * ```
46
+ */
47
+ declare const useLocalOnce: (condition: unknown, callback: () => void) => void;
48
+ //#endregion
49
+ //#region src/react/useOnUnmount.d.ts
50
+ /**
51
+ * Calls the given callback when the component unmounts.
52
+ * Uses useStableCallback internally to ensure the latest version is called.
53
+ */
54
+ declare const useOnUnmount: (callback: () => void) => void;
55
+ //#endregion
56
+ export { useLocalOnce, useOnUnmount, useOnce, useStableCallback };
57
+ //# sourceMappingURL=react.d.mts.map
package/dist/react.mjs ADDED
@@ -0,0 +1,91 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+
3
+ //#region src/react/useStableCallback.ts
4
+ /**
5
+ * Creates a stable callback that always calls the latest version of the function.
6
+ * Useful for callbacks that need to be used in dependency arrays but should always
7
+ * execute the most recent version of the callback.
8
+ */
9
+ const useStableCallback = (callback) => {
10
+ const callbackRef = useRef(callback);
11
+ callbackRef.current = callback;
12
+ return useCallback((...args) => {
13
+ return callbackRef.current(...args);
14
+ }, []);
15
+ };
16
+
17
+ //#endregion
18
+ //#region src/react/useOnce.ts
19
+ /**
20
+ * Runs a callback only once when a condition becomes truthy.
21
+ * The callback is stabilized internally to always reference the latest version.
22
+ *
23
+ * @param condition - When truthy (evaluated via Boolean()), the callback will be executed (only once).
24
+ * @param callback - The function to run once when the condition is met.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * const isReady = true;
29
+ * useOnce(isReady, () => {
30
+ * console.log("Ready!");
31
+ * });
32
+ * ```
33
+ */
34
+ const useOnce = (condition, callback) => {
35
+ const hasRunRef = useRef(false);
36
+ const stableCallback = useStableCallback(callback);
37
+ useEffect(() => {
38
+ if (Boolean(condition) && !hasRunRef.current) {
39
+ hasRunRef.current = true;
40
+ stableCallback();
41
+ }
42
+ }, [condition, stableCallback]);
43
+ };
44
+
45
+ //#endregion
46
+ //#region src/react/useLocalOnce.ts
47
+ /**
48
+ * Runs a callback only once when a condition becomes truthy, executing synchronously
49
+ * during render. The callback is stabilized internally to always reference the latest version.
50
+ *
51
+ * Unlike `useOnce`, this runs at the top level of the hook (not in useEffect),
52
+ * making it suitable for state updates that need to happen synchronously during render.
53
+ *
54
+ * @param condition - When truthy (evaluated via Boolean()), the callback will be executed (only once).
55
+ * @param callback - The function to run once when the condition is met.
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * const user: User | null = getUser();
60
+ * useLocalOnce(user, () => {
61
+ * setState(user.name);
62
+ * });
63
+ * ```
64
+ */
65
+ const useLocalOnce = (condition, callback) => {
66
+ const hasRunRef = useRef(false);
67
+ const stableCallback = useStableCallback(callback);
68
+ if (Boolean(condition) && !hasRunRef.current) {
69
+ hasRunRef.current = true;
70
+ stableCallback();
71
+ }
72
+ };
73
+
74
+ //#endregion
75
+ //#region src/react/useOnUnmount.ts
76
+ /**
77
+ * Calls the given callback when the component unmounts.
78
+ * Uses useStableCallback internally to ensure the latest version is called.
79
+ */
80
+ const useOnUnmount = (callback) => {
81
+ const stableCallback = useStableCallback(callback);
82
+ useEffect(() => {
83
+ return () => {
84
+ stableCallback();
85
+ };
86
+ }, [stableCallback]);
87
+ };
88
+
89
+ //#endregion
90
+ export { useLocalOnce, useOnUnmount, useOnce, useStableCallback };
91
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.mjs","names":[],"sources":["../src/react/useStableCallback.ts","../src/react/useOnce.ts","../src/react/useLocalOnce.ts","../src/react/useOnUnmount.ts"],"sourcesContent":["// oxlint-disable no-explicit-any\r\n\r\nimport { useCallback, useRef } from \"react\";\r\n\r\ntype AnyFunction = (...args: any[]) => any;\r\ntype AnyArgs = any[];\r\n\r\n/**\r\n * Creates a stable callback that always calls the latest version of the function.\r\n * Useful for callbacks that need to be used in dependency arrays but should always\r\n * execute the most recent version of the callback.\r\n */\r\nexport const useStableCallback = <T extends AnyFunction>(callback: T): T => {\r\n const callbackRef = useRef(callback);\r\n\r\n // Update the ref on every render\r\n callbackRef.current = callback;\r\n\r\n return useCallback((...args: AnyArgs) => {\r\n return callbackRef.current(...args);\r\n }, []) as T;\r\n};\r\n","import { useEffect, useRef } from \"react\";\r\nimport { useStableCallback } from \"./useStableCallback\";\r\n\r\n/**\r\n * Runs a callback only once when a condition becomes truthy.\r\n * The callback is stabilized internally to always reference the latest version.\r\n *\r\n * @param condition - When truthy (evaluated via Boolean()), the callback will be executed (only once).\r\n * @param callback - The function to run once when the condition is met.\r\n *\r\n * @example\r\n * ```tsx\r\n * const isReady = true;\r\n * useOnce(isReady, () => {\r\n * console.log(\"Ready!\");\r\n * });\r\n * ```\r\n */\r\nexport const useOnce = (condition: unknown, callback: () => void): void => {\r\n const hasRunRef = useRef(false);\r\n const stableCallback = useStableCallback(callback);\r\n\r\n useEffect(() => {\r\n if (Boolean(condition) && !hasRunRef.current) {\r\n hasRunRef.current = true;\r\n stableCallback();\r\n }\r\n }, [condition, stableCallback]);\r\n};\r\n","import { useRef } from \"react\";\r\nimport { useStableCallback } from \"./useStableCallback\";\r\n\r\n/**\r\n * Runs a callback only once when a condition becomes truthy, executing synchronously\r\n * during render. The callback is stabilized internally to always reference the latest version.\r\n *\r\n * Unlike `useOnce`, this runs at the top level of the hook (not in useEffect),\r\n * making it suitable for state updates that need to happen synchronously during render.\r\n *\r\n * @param condition - When truthy (evaluated via Boolean()), the callback will be executed (only once).\r\n * @param callback - The function to run once when the condition is met.\r\n *\r\n * @example\r\n * ```tsx\r\n * const user: User | null = getUser();\r\n * useLocalOnce(user, () => {\r\n * setState(user.name);\r\n * });\r\n * ```\r\n */\r\nexport const useLocalOnce = (condition: unknown, callback: () => void): void => {\r\n const hasRunRef = useRef(false);\r\n const stableCallback = useStableCallback(callback);\r\n\r\n if (Boolean(condition) && !hasRunRef.current) {\r\n hasRunRef.current = true;\r\n stableCallback();\r\n }\r\n};\r\n","import { useEffect } from \"react\";\r\nimport { useStableCallback } from \"./useStableCallback\";\r\n\r\n/**\r\n * Calls the given callback when the component unmounts.\r\n * Uses useStableCallback internally to ensure the latest version is called.\r\n */\r\nexport const useOnUnmount = (callback: () => void): void => {\r\n const stableCallback = useStableCallback(callback);\r\n\r\n useEffect(() => {\r\n return () => {\r\n stableCallback();\r\n };\r\n }, [stableCallback]);\r\n};\r\n"],"mappings":";;;;;;;;AAYA,MAAa,qBAA4C,aAAmB;CAC1E,MAAM,cAAc,OAAO,SAAS;AAGpC,aAAY,UAAU;AAEtB,QAAO,aAAa,GAAG,SAAkB;AACvC,SAAO,YAAY,QAAQ,GAAG,KAAK;IAClC,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;ACFR,MAAa,WAAW,WAAoB,aAA+B;CACzE,MAAM,YAAY,OAAO,MAAM;CAC/B,MAAM,iBAAiB,kBAAkB,SAAS;AAElD,iBAAgB;AACd,MAAI,QAAQ,UAAU,IAAI,CAAC,UAAU,SAAS;AAC5C,aAAU,UAAU;AACpB,mBAAgB;;IAEjB,CAAC,WAAW,eAAe,CAAC;;;;;;;;;;;;;;;;;;;;;;;ACNjC,MAAa,gBAAgB,WAAoB,aAA+B;CAC9E,MAAM,YAAY,OAAO,MAAM;CAC/B,MAAM,iBAAiB,kBAAkB,SAAS;AAElD,KAAI,QAAQ,UAAU,IAAI,CAAC,UAAU,SAAS;AAC5C,YAAU,UAAU;AACpB,kBAAgB;;;;;;;;;;ACpBpB,MAAa,gBAAgB,aAA+B;CAC1D,MAAM,iBAAiB,kBAAkB,SAAS;AAElD,iBAAgB;AACd,eAAa;AACX,mBAAgB;;IAEjB,CAAC,eAAe,CAAC"}
package/package.json CHANGED
@@ -15,6 +15,10 @@
15
15
  "types": "./dist/images.d.mts",
16
16
  "default": "./dist/images.mjs"
17
17
  },
18
+ "./react": {
19
+ "types": "./dist/react.d.mts",
20
+ "default": "./dist/react.mjs"
21
+ },
18
22
  "./oxlint/config": {
19
23
  "default": "./dist/oxlint/config.mjson"
20
24
  },
@@ -63,17 +67,22 @@
63
67
  "remeda": "2.32.0",
64
68
  "valibot": "1.1.0"
65
69
  },
66
- "keywords": [],
67
- "author": "",
70
+ "peerDependencies": {
71
+ "react": ">=18.0.0",
72
+ "react-dom": ">=18.0.0"
73
+ },
68
74
  "license": "ISC",
69
75
  "devDependencies": {
70
76
  "@biomejs/biome": "1.8.3",
71
- "@types/node": "24.10.0",
77
+ "@testing-library/react": "16.3.0",
78
+ "@types/node": "25.0.1",
79
+ "@types/react": "19.2.7",
80
+ "react": "19.2.3",
72
81
  "tsdown": "0.17.2",
73
82
  "typescript": "5.9.3",
74
83
  "vitest": "4.0.15"
75
84
  },
76
- "version": "0.0.42",
85
+ "version": "0.0.44",
77
86
  "scripts": {
78
87
  "dev": "tsdown --watch",
79
88
  "build": "tsc --noEmit && tsdown",