@herowcode/utils 1.1.0 → 1.1.1

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.
Files changed (57) hide show
  1. package/README.md +137 -2
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.esm.js +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/string/format-hms-to-seconds.d.ts +2 -0
  8. package/dist/string/format-hms-to-seconds.d.ts.map +1 -0
  9. package/dist/string/format-hms-to-seconds.esm.js +25 -0
  10. package/dist/string/format-hms-to-seconds.js +25 -0
  11. package/dist/string/format-hms-to-seconds.js.map +1 -0
  12. package/dist/string/format-seconds-to-fragment.d.ts +3 -0
  13. package/dist/string/format-seconds-to-fragment.d.ts.map +1 -0
  14. package/dist/string/format-seconds-to-fragment.esm.js +15 -0
  15. package/dist/string/format-seconds-to-fragment.js +15 -0
  16. package/dist/string/format-seconds-to-fragment.js.map +1 -0
  17. package/dist/string/format-seconds-to-hms.d.ts +2 -0
  18. package/dist/string/format-seconds-to-hms.d.ts.map +1 -0
  19. package/dist/string/format-seconds-to-hms.esm.js +13 -0
  20. package/dist/string/format-seconds-to-hms.js +13 -0
  21. package/dist/string/format-seconds-to-hms.js.map +1 -0
  22. package/dist/string/format-string-to-time.d.ts +2 -0
  23. package/dist/string/format-string-to-time.d.ts.map +1 -0
  24. package/dist/string/format-string-to-time.esm.js +10 -0
  25. package/dist/string/format-string-to-time.js +10 -0
  26. package/dist/string/format-string-to-time.js.map +1 -0
  27. package/dist/string/index.d.ts +4 -0
  28. package/dist/string/index.d.ts.map +1 -1
  29. package/dist/string/index.esm.js +4 -0
  30. package/dist/string/index.js +4 -0
  31. package/dist/string/index.js.map +1 -1
  32. package/dist/youtube/extract-youtube-video-id.d.ts +2 -0
  33. package/dist/youtube/extract-youtube-video-id.d.ts.map +1 -0
  34. package/dist/youtube/extract-youtube-video-id.esm.js +26 -0
  35. package/dist/youtube/extract-youtube-video-id.js +26 -0
  36. package/dist/youtube/extract-youtube-video-id.js.map +1 -0
  37. package/dist/youtube/generate-youtube-url.d.ts +20 -0
  38. package/dist/youtube/generate-youtube-url.d.ts.map +1 -0
  39. package/dist/youtube/generate-youtube-url.esm.js +81 -0
  40. package/dist/youtube/generate-youtube-url.js +81 -0
  41. package/dist/youtube/generate-youtube-url.js.map +1 -0
  42. package/dist/youtube/index.d.ts +5 -0
  43. package/dist/youtube/index.d.ts.map +1 -0
  44. package/dist/youtube/index.esm.js +4 -0
  45. package/dist/youtube/index.js +4 -0
  46. package/dist/youtube/index.js.map +1 -0
  47. package/dist/youtube/use-get-video-duration.d.ts +7 -0
  48. package/dist/youtube/use-get-video-duration.d.ts.map +1 -0
  49. package/dist/youtube/use-get-video-duration.esm.js +150 -0
  50. package/dist/youtube/use-get-video-duration.js +150 -0
  51. package/dist/youtube/use-get-video-duration.js.map +1 -0
  52. package/dist/youtube/validate-youtube-link.d.ts +2 -0
  53. package/dist/youtube/validate-youtube-link.d.ts.map +1 -0
  54. package/dist/youtube/validate-youtube-link.esm.js +40 -0
  55. package/dist/youtube/validate-youtube-link.js +40 -0
  56. package/dist/youtube/validate-youtube-link.js.map +1 -0
  57. package/package.json +28 -13
@@ -0,0 +1,81 @@
1
+ import { formatHMSToSeconds, formatSecondsToFragment } from "../string";
2
+ import { extractYouTubeId } from "./extract-youtube-video-id";
3
+ export const generateYoutubeURL = (opts) => {
4
+ const { videoURL, start, end, embed = false, short = false, useFragment = false, autoplay, controls, rel, loop, mute, modestbranding, origin, playlist, params = {}, } = opts;
5
+ const videoId = extractYouTubeId(videoURL);
6
+ if (!videoId)
7
+ return null;
8
+ const startSec = formatHMSToSeconds(start);
9
+ const endSec = formatHMSToSeconds(end);
10
+ // base url
11
+ const base = embed
12
+ ? `https://www.youtube.com/embed/${videoId}`
13
+ : short
14
+ ? `https://youtu.be/${videoId}`
15
+ : "https://www.youtube.com/watch";
16
+ const search = new URLSearchParams();
17
+ // Add video ID for watch URLs
18
+ if (!embed && !short) {
19
+ search.set("v", videoId);
20
+ }
21
+ // Standard param names for watch/embed
22
+ if (!useFragment) {
23
+ if (startSec != null) {
24
+ // youtu.be historically uses "t" as a query param, but "start" is widely supported.
25
+ // Use "t" for short links, otherwise "start".
26
+ search.set(short ? "t" : "start", String(startSec));
27
+ }
28
+ if (endSec != null) {
29
+ search.set("end", String(endSec));
30
+ }
31
+ }
32
+ // embed / player related params
33
+ if (typeof autoplay !== "undefined")
34
+ search.set("autoplay", autoplay ? "1" : "0");
35
+ if (typeof controls !== "undefined")
36
+ search.set("controls", String(controls));
37
+ if (typeof rel !== "undefined")
38
+ search.set("rel", String(rel));
39
+ if (typeof modestbranding !== "undefined")
40
+ search.set("modestbranding", String(modestbranding));
41
+ if (typeof origin !== "undefined")
42
+ search.set("origin", origin);
43
+ if (typeof mute !== "undefined")
44
+ search.set("mute", mute ? "1" : "0");
45
+ // loop requires playlist param when embedding a single video
46
+ if (loop) {
47
+ search.set("loop", "1");
48
+ if (playlist) {
49
+ search.set("playlist", playlist);
50
+ }
51
+ else if (embed) {
52
+ // for embed+loop, YouTube expects &playlist=VIDEO_ID
53
+ search.set("playlist", videoId);
54
+ }
55
+ }
56
+ else if (playlist) {
57
+ search.set("playlist", playlist);
58
+ }
59
+ // merge custom params (allow overriding)
60
+ Object.entries(params).forEach(([k, v]) => {
61
+ if (v === false)
62
+ search.set(k, "0");
63
+ else if (v === true)
64
+ search.set(k, "1");
65
+ else
66
+ search.set(k, String(v));
67
+ });
68
+ const qs = search.toString() ? `?${search.toString()}` : "";
69
+ // fragment handling (e.g. #t=1m2s)
70
+ let fragment = "";
71
+ if (useFragment && startSec != null) {
72
+ fragment = `#t=${formatSecondsToFragment(startSec)}`;
73
+ }
74
+ // For short links, people often prefer the short host and start as query or fragment.
75
+ if (short) {
76
+ // prefer fragment if requested, otherwise use search params (t)
77
+ return `${base}${qs}${fragment}`;
78
+ }
79
+ // watch/embed links
80
+ return `${base}${qs}${fragment}`;
81
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-youtube-url.js","sourceRoot":"","sources":["../../src/youtube/generate-youtube-url.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export * from "./extract-youtube-video-id";
2
+ export * from "./generate-youtube-url";
3
+ export * from "./use-get-video-duration";
4
+ export * from "./validate-youtube-link";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/youtube/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA;AAC1C,cAAc,wBAAwB,CAAA;AACtC,cAAc,0BAA0B,CAAA;AACxC,cAAc,yBAAyB,CAAA"}
@@ -0,0 +1,4 @@
1
+ export * from "./extract-youtube-video-id";
2
+ export * from "./generate-youtube-url";
3
+ export * from "./use-get-video-duration";
4
+ export * from "./validate-youtube-link";
@@ -0,0 +1,4 @@
1
+ export * from "./extract-youtube-video-id";
2
+ export * from "./generate-youtube-url";
3
+ export * from "./use-get-video-duration";
4
+ export * from "./validate-youtube-link";
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/youtube/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,6DAA0C;AAC1C,yDAAsC;AACtC,2DAAwC;AACxC,0DAAuC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * useGetYoutubeVideoDuration
3
+ * Returns a function that accepts a YouTube URL (full or short) and returns a Promise
4
+ * resolving to the video duration formatted as "HH:MM:SS" or null on failure.
5
+ */
6
+ export declare function useGetYoutubeVideoDuration(): (videoUrl: string) => Promise<string | null>;
7
+ //# sourceMappingURL=use-get-video-duration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-get-video-duration.d.ts","sourceRoot":"","sources":["../../src/youtube/use-get-video-duration.ts"],"names":[],"mappings":"AAwCA;;;;GAIG;AACH,wBAAgB,0BAA0B,eAErB,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA0GnD"}
@@ -0,0 +1,150 @@
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: Window is any */
2
+ import { useCallback } from "react";
3
+ import { formatSecondsToHMS } from "../string";
4
+ import { extractYouTubeId } from "./extract-youtube-video-id";
5
+ import { validateYoutubeLink } from "./validate-youtube-link";
6
+ let YtApiLoading = null;
7
+ function loadYouTubeIFrameAPI() {
8
+ var _a;
9
+ if ((_a = window.YT) === null || _a === void 0 ? void 0 : _a.Player)
10
+ return Promise.resolve();
11
+ if (YtApiLoading)
12
+ return YtApiLoading;
13
+ YtApiLoading = new Promise((resolve) => {
14
+ const existing = document.querySelector('script[src="https://www.youtube.com/iframe_api"]');
15
+ if (existing) {
16
+ // Poll until the API is ready
17
+ const poll = setInterval(() => {
18
+ var _a;
19
+ if ((_a = window.YT) === null || _a === void 0 ? void 0 : _a.Player) {
20
+ clearInterval(poll);
21
+ resolve();
22
+ }
23
+ }, 50);
24
+ return;
25
+ }
26
+ const tag = document.createElement("script");
27
+ tag.src = "https://www.youtube.com/iframe_api";
28
+ tag.async = true;
29
+ document.body.appendChild(tag);
30
+ window.onYouTubeIframeAPIReady = () => {
31
+ resolve();
32
+ };
33
+ });
34
+ return YtApiLoading;
35
+ }
36
+ /**
37
+ * useGetYoutubeVideoDuration
38
+ * Returns a function that accepts a YouTube URL (full or short) and returns a Promise
39
+ * resolving to the video duration formatted as "HH:MM:SS" or null on failure.
40
+ */
41
+ export function useGetYoutubeVideoDuration() {
42
+ const getYoutubeVideoDuration = useCallback(async (videoUrl) => {
43
+ const videoId = extractYouTubeId(videoUrl);
44
+ if (!videoId)
45
+ return null;
46
+ const videoIsValid = await validateYoutubeLink(videoUrl);
47
+ if (!videoIsValid)
48
+ return null;
49
+ try {
50
+ await loadYouTubeIFrameAPI();
51
+ }
52
+ catch (_a) {
53
+ return null;
54
+ }
55
+ return await new Promise((resolve) => {
56
+ const iframeId = `yt-duration-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
57
+ const iframe = document.createElement("iframe");
58
+ // create a minimal offscreen iframe for the player
59
+ iframe.id = iframeId;
60
+ iframe.style.position = "fixed";
61
+ iframe.style.left = "-9999px";
62
+ iframe.style.width = "1px";
63
+ iframe.style.height = "1px";
64
+ iframe.style.opacity = "0";
65
+ iframe.style.pointerEvents = "none";
66
+ // embed URL with enablejsapi so we can construct YT.Player
67
+ const origin = window.location.origin;
68
+ iframe.src = `https://www.youtube.com/embed/${encodeURIComponent(videoId)}?enablejsapi=1&origin=${encodeURIComponent(origin)}`;
69
+ let resolved = false;
70
+ let player = null;
71
+ let cleanupTimeout = null;
72
+ function cleanupAndResolve(result) {
73
+ if (resolved)
74
+ return;
75
+ resolved = true;
76
+ try {
77
+ if (player && typeof player.destroy === "function")
78
+ player.destroy();
79
+ }
80
+ catch (_a) {
81
+ /* ignore */
82
+ }
83
+ try {
84
+ if (iframe.parentNode)
85
+ iframe.parentNode.removeChild(iframe);
86
+ }
87
+ catch (_b) {
88
+ /* ignore */
89
+ }
90
+ if (cleanupTimeout)
91
+ window.clearTimeout(cleanupTimeout);
92
+ resolve(result);
93
+ }
94
+ // timeout fallback
95
+ cleanupTimeout = window.setTimeout(() => {
96
+ cleanupAndResolve(null);
97
+ }, 10000); // 10s timeout
98
+ document.body.appendChild(iframe);
99
+ // Construct player
100
+ try {
101
+ player = new window.YT.Player(iframeId, {
102
+ events: {
103
+ onReady: (e) => {
104
+ try {
105
+ let duration = e.target.getDuration();
106
+ if (!duration || duration === 0) {
107
+ // Sometimes duration is 0 immediately; try a few short retries
108
+ let attempts = 0;
109
+ const tryInterval = setInterval(() => {
110
+ attempts += 1;
111
+ try {
112
+ duration = e.target.getDuration();
113
+ if (duration && duration > 0) {
114
+ clearInterval(tryInterval);
115
+ cleanupAndResolve(formatSecondsToHMS(duration));
116
+ }
117
+ else if (attempts >= 8) {
118
+ clearInterval(tryInterval);
119
+ cleanupAndResolve(null);
120
+ }
121
+ }
122
+ catch (_a) {
123
+ if (attempts >= 8) {
124
+ clearInterval(tryInterval);
125
+ cleanupAndResolve(null);
126
+ }
127
+ }
128
+ }, 300);
129
+ }
130
+ else {
131
+ cleanupAndResolve(formatSecondsToHMS(duration));
132
+ }
133
+ }
134
+ catch (_a) {
135
+ cleanupAndResolve(null);
136
+ }
137
+ },
138
+ onError: () => {
139
+ cleanupAndResolve(null);
140
+ },
141
+ },
142
+ });
143
+ }
144
+ catch (_a) {
145
+ cleanupAndResolve(null);
146
+ }
147
+ });
148
+ }, []);
149
+ return getYoutubeVideoDuration;
150
+ }
@@ -0,0 +1,150 @@
1
+ /** biome-ignore-all lint/suspicious/noExplicitAny: Window is any */
2
+ import { useCallback } from "react";
3
+ import { formatSecondsToHMS } from "../string";
4
+ import { extractYouTubeId } from "./extract-youtube-video-id";
5
+ import { validateYoutubeLink } from "./validate-youtube-link";
6
+ let YtApiLoading = null;
7
+ function loadYouTubeIFrameAPI() {
8
+ var _a;
9
+ if ((_a = window.YT) === null || _a === void 0 ? void 0 : _a.Player)
10
+ return Promise.resolve();
11
+ if (YtApiLoading)
12
+ return YtApiLoading;
13
+ YtApiLoading = new Promise((resolve) => {
14
+ const existing = document.querySelector('script[src="https://www.youtube.com/iframe_api"]');
15
+ if (existing) {
16
+ // Poll until the API is ready
17
+ const poll = setInterval(() => {
18
+ var _a;
19
+ if ((_a = window.YT) === null || _a === void 0 ? void 0 : _a.Player) {
20
+ clearInterval(poll);
21
+ resolve();
22
+ }
23
+ }, 50);
24
+ return;
25
+ }
26
+ const tag = document.createElement("script");
27
+ tag.src = "https://www.youtube.com/iframe_api";
28
+ tag.async = true;
29
+ document.body.appendChild(tag);
30
+ window.onYouTubeIframeAPIReady = () => {
31
+ resolve();
32
+ };
33
+ });
34
+ return YtApiLoading;
35
+ }
36
+ /**
37
+ * useGetYoutubeVideoDuration
38
+ * Returns a function that accepts a YouTube URL (full or short) and returns a Promise
39
+ * resolving to the video duration formatted as "HH:MM:SS" or null on failure.
40
+ */
41
+ export function useGetYoutubeVideoDuration() {
42
+ const getYoutubeVideoDuration = useCallback(async (videoUrl) => {
43
+ const videoId = extractYouTubeId(videoUrl);
44
+ if (!videoId)
45
+ return null;
46
+ const videoIsValid = await validateYoutubeLink(videoUrl);
47
+ if (!videoIsValid)
48
+ return null;
49
+ try {
50
+ await loadYouTubeIFrameAPI();
51
+ }
52
+ catch (_a) {
53
+ return null;
54
+ }
55
+ return await new Promise((resolve) => {
56
+ const iframeId = `yt-duration-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
57
+ const iframe = document.createElement("iframe");
58
+ // create a minimal offscreen iframe for the player
59
+ iframe.id = iframeId;
60
+ iframe.style.position = "fixed";
61
+ iframe.style.left = "-9999px";
62
+ iframe.style.width = "1px";
63
+ iframe.style.height = "1px";
64
+ iframe.style.opacity = "0";
65
+ iframe.style.pointerEvents = "none";
66
+ // embed URL with enablejsapi so we can construct YT.Player
67
+ const origin = window.location.origin;
68
+ iframe.src = `https://www.youtube.com/embed/${encodeURIComponent(videoId)}?enablejsapi=1&origin=${encodeURIComponent(origin)}`;
69
+ let resolved = false;
70
+ let player = null;
71
+ let cleanupTimeout = null;
72
+ function cleanupAndResolve(result) {
73
+ if (resolved)
74
+ return;
75
+ resolved = true;
76
+ try {
77
+ if (player && typeof player.destroy === "function")
78
+ player.destroy();
79
+ }
80
+ catch (_a) {
81
+ /* ignore */
82
+ }
83
+ try {
84
+ if (iframe.parentNode)
85
+ iframe.parentNode.removeChild(iframe);
86
+ }
87
+ catch (_b) {
88
+ /* ignore */
89
+ }
90
+ if (cleanupTimeout)
91
+ window.clearTimeout(cleanupTimeout);
92
+ resolve(result);
93
+ }
94
+ // timeout fallback
95
+ cleanupTimeout = window.setTimeout(() => {
96
+ cleanupAndResolve(null);
97
+ }, 10000); // 10s timeout
98
+ document.body.appendChild(iframe);
99
+ // Construct player
100
+ try {
101
+ player = new window.YT.Player(iframeId, {
102
+ events: {
103
+ onReady: (e) => {
104
+ try {
105
+ let duration = e.target.getDuration();
106
+ if (!duration || duration === 0) {
107
+ // Sometimes duration is 0 immediately; try a few short retries
108
+ let attempts = 0;
109
+ const tryInterval = setInterval(() => {
110
+ attempts += 1;
111
+ try {
112
+ duration = e.target.getDuration();
113
+ if (duration && duration > 0) {
114
+ clearInterval(tryInterval);
115
+ cleanupAndResolve(formatSecondsToHMS(duration));
116
+ }
117
+ else if (attempts >= 8) {
118
+ clearInterval(tryInterval);
119
+ cleanupAndResolve(null);
120
+ }
121
+ }
122
+ catch (_a) {
123
+ if (attempts >= 8) {
124
+ clearInterval(tryInterval);
125
+ cleanupAndResolve(null);
126
+ }
127
+ }
128
+ }, 300);
129
+ }
130
+ else {
131
+ cleanupAndResolve(formatSecondsToHMS(duration));
132
+ }
133
+ }
134
+ catch (_a) {
135
+ cleanupAndResolve(null);
136
+ }
137
+ },
138
+ onError: () => {
139
+ cleanupAndResolve(null);
140
+ },
141
+ },
142
+ });
143
+ }
144
+ catch (_a) {
145
+ cleanupAndResolve(null);
146
+ }
147
+ });
148
+ }, []);
149
+ return getYoutubeVideoDuration;
150
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-get-video-duration.js","sourceRoot":"","sources":["../../src/youtube/use-get-video-duration.ts"],"names":[],"mappings":";;AA6CA,gEA4GC;AAzJD,oEAAoE;AACpE,iCAAmC;AACnC,sCAA8C;AAC9C,yEAA6D;AAC7D,mEAA6D;AAE7D,IAAI,YAAY,GAAyB,IAAI,CAAA;AAE7C,SAAS,oBAAoB;;IAC3B,IAAI,MAAC,MAAc,CAAC,EAAE,0CAAE,MAAM;QAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;IACxD,IAAI,YAAY;QAAE,OAAO,YAAY,CAAA;IAErC,YAAY,GAAG,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QACrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CACrC,kDAAkD,CACnD,CAAA;QACD,IAAI,QAAQ,EAAE,CAAC;YACb,8BAA8B;YAC9B,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE;;gBAC5B,IAAI,MAAC,MAAc,CAAC,EAAE,0CAAE,MAAM,EAAE,CAAC;oBAC/B,aAAa,CAAC,IAAI,CAAC,CAAA;oBACnB,OAAO,EAAE,CAAA;gBACX,CAAC;YACH,CAAC,EAAE,EAAE,CAAC,CAAA;YACN,OAAM;QACR,CAAC;QAED,MAAM,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;QAC5C,GAAG,CAAC,GAAG,GAAG,oCAAoC,CAAA;QAC9C,GAAG,CAAC,KAAK,GAAG,IAAI,CAAA;QAChB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAE7B;QAAC,MAAc,CAAC,uBAAuB,GAAG,GAAG,EAAE;YAC9C,OAAO,EAAE,CAAA;QACX,CAAC,CAAA;IACH,CAAC,CAAC,CAAA;IAEF,OAAO,YAAY,CAAA;AACrB,CAAC;AAED;;;;GAIG;AACH,SAAgB,0BAA0B;IACxC,MAAM,uBAAuB,GAAG,IAAA,mBAAW,EACzC,KAAK,EAAE,QAAgB,EAA0B,EAAE;QACjD,MAAM,OAAO,GAAG,IAAA,2CAAgB,EAAC,QAAQ,CAAC,CAAA;QAC1C,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAA;QAEzB,MAAM,YAAY,GAAG,MAAM,IAAA,2CAAmB,EAAC,QAAQ,CAAC,CAAA;QACxD,IAAI,CAAC,YAAY;YAAE,OAAO,IAAI,CAAA;QAE9B,IAAI,CAAC;YACH,MAAM,oBAAoB,EAAE,CAAA;QAC9B,CAAC;QAAC,WAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;QAED,OAAO,MAAM,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,EAAE;YAClD,MAAM,QAAQ,GAAG,eAAe,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAA;YACtF,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAA;YAC/C,mDAAmD;YACnD,MAAM,CAAC,EAAE,GAAG,QAAQ,CAAA;YACpB,MAAM,CAAC,KAAK,CAAC,QAAQ,GAAG,OAAO,CAAA;YAC/B,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,SAAS,CAAA;YAC7B,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAA;YAC1B,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAA;YAC3B,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAA;YAC1B,MAAM,CAAC,KAAK,CAAC,aAAa,GAAG,MAAM,CAAA;YAEnC,2DAA2D;YAC3D,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAA;YACrC,MAAM,CAAC,GAAG,GAAG,iCAAiC,kBAAkB,CAAC,OAAO,CAAC,yBAAyB,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAA;YAE9H,IAAI,QAAQ,GAAG,KAAK,CAAA;YACpB,IAAI,MAAM,GAAQ,IAAI,CAAA;YACtB,IAAI,cAAc,GAAkB,IAAI,CAAA;YAExC,SAAS,iBAAiB,CAAC,MAAqB;gBAC9C,IAAI,QAAQ;oBAAE,OAAM;gBACpB,QAAQ,GAAG,IAAI,CAAA;gBACf,IAAI,CAAC;oBACH,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,UAAU;wBAAE,MAAM,CAAC,OAAO,EAAE,CAAA;gBACtE,CAAC;gBAAC,WAAM,CAAC;oBACP,YAAY;gBACd,CAAC;gBACD,IAAI,CAAC;oBACH,IAAI,MAAM,CAAC,UAAU;wBAAE,MAAM,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;gBAC9D,CAAC;gBAAC,WAAM,CAAC;oBACP,YAAY;gBACd,CAAC;gBACD,IAAI,cAAc;oBAAE,MAAM,CAAC,YAAY,CAAC,cAAc,CAAC,CAAA;gBACvD,OAAO,CAAC,MAAM,CAAC,CAAA;YACjB,CAAC;YAED,mBAAmB;YACnB,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;gBACtC,iBAAiB,CAAC,IAAI,CAAC,CAAA;YACzB,CAAC,EAAE,KAAK,CAAC,CAAA,CAAC,cAAc;YAExB,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;YAEjC,mBAAmB;YACnB,IAAI,CAAC;gBACH,MAAM,GAAG,IAAK,MAAc,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE;oBAC/C,MAAM,EAAE;wBACN,OAAO,EAAE,CAAC,CAAM,EAAE,EAAE;4BAClB,IAAI,CAAC;gCACH,IAAI,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAA;gCACrC,IAAI,CAAC,QAAQ,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;oCAChC,+DAA+D;oCAC/D,IAAI,QAAQ,GAAG,CAAC,CAAA;oCAChB,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;wCACnC,QAAQ,IAAI,CAAC,CAAA;wCACb,IAAI,CAAC;4CACH,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAA;4CACjC,IAAI,QAAQ,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;gDAC7B,aAAa,CAAC,WAAW,CAAC,CAAA;gDAC1B,iBAAiB,CAAC,IAAA,2BAAkB,EAAC,QAAQ,CAAC,CAAC,CAAA;4CACjD,CAAC;iDAAM,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;gDACzB,aAAa,CAAC,WAAW,CAAC,CAAA;gDAC1B,iBAAiB,CAAC,IAAI,CAAC,CAAA;4CACzB,CAAC;wCACH,CAAC;wCAAC,WAAM,CAAC;4CACP,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;gDAClB,aAAa,CAAC,WAAW,CAAC,CAAA;gDAC1B,iBAAiB,CAAC,IAAI,CAAC,CAAA;4CACzB,CAAC;wCACH,CAAC;oCACH,CAAC,EAAE,GAAG,CAAC,CAAA;gCACT,CAAC;qCAAM,CAAC;oCACN,iBAAiB,CAAC,IAAA,2BAAkB,EAAC,QAAQ,CAAC,CAAC,CAAA;gCACjD,CAAC;4BACH,CAAC;4BAAC,WAAM,CAAC;gCACP,iBAAiB,CAAC,IAAI,CAAC,CAAA;4BACzB,CAAC;wBACH,CAAC;wBACD,OAAO,EAAE,GAAG,EAAE;4BACZ,iBAAiB,CAAC,IAAI,CAAC,CAAA;wBACzB,CAAC;qBACF;iBACF,CAAC,CAAA;YACJ,CAAC;YAAC,WAAM,CAAC;gBACP,iBAAiB,CAAC,IAAI,CAAC,CAAA;YACzB,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC,EACD,EAAE,CACH,CAAA;IAED,OAAO,uBAAuB,CAAA;AAChC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function validateYoutubeLink(videoUrl: string): Promise<boolean>;
2
+ //# sourceMappingURL=validate-youtube-link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-youtube-link.d.ts","sourceRoot":"","sources":["../../src/youtube/validate-youtube-link.ts"],"names":[],"mappings":"AAAA,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA0C5E"}
@@ -0,0 +1,40 @@
1
+ export async function validateYoutubeLink(videoUrl) {
2
+ const { extractYouTubeId } = await import("./extract-youtube-video-id");
3
+ const videoId = extractYouTubeId(videoUrl);
4
+ if (!videoId)
5
+ return false;
6
+ // Try loading YouTube thumbnail images — avoids CORS problems because
7
+ // creating an Image and listening for load/error is not blocked by CORS.
8
+ const thumbs = [
9
+ `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
10
+ `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
11
+ `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`,
12
+ `https://img.youtube.com/vi/${videoId}/default.jpg`,
13
+ ];
14
+ const loadImage = (src) => new Promise((resolve) => {
15
+ const img = new Image();
16
+ img.onload = () => resolve(true);
17
+ img.onerror = () => resolve(false);
18
+ img.src = src;
19
+ });
20
+ for (const url of thumbs) {
21
+ try {
22
+ const ok = await loadImage(url);
23
+ if (ok)
24
+ return true;
25
+ }
26
+ catch (_a) {
27
+ // ignore and try next thumbnail
28
+ }
29
+ }
30
+ // Fallback: try oEmbed endpoint (may be subject to CORS in some environments)
31
+ try {
32
+ const watchUrl = `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`;
33
+ const oembedUrl = `https://www.youtube.com/oembed?url=${encodeURIComponent(watchUrl)}&format=json`;
34
+ const res = await fetch(oembedUrl, { method: "GET" });
35
+ return res.ok;
36
+ }
37
+ catch (_b) {
38
+ return false;
39
+ }
40
+ }
@@ -0,0 +1,40 @@
1
+ export async function validateYoutubeLink(videoUrl) {
2
+ const { extractYouTubeId } = await import("./extract-youtube-video-id");
3
+ const videoId = extractYouTubeId(videoUrl);
4
+ if (!videoId)
5
+ return false;
6
+ // Try loading YouTube thumbnail images — avoids CORS problems because
7
+ // creating an Image and listening for load/error is not blocked by CORS.
8
+ const thumbs = [
9
+ `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
10
+ `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
11
+ `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`,
12
+ `https://img.youtube.com/vi/${videoId}/default.jpg`,
13
+ ];
14
+ const loadImage = (src) => new Promise((resolve) => {
15
+ const img = new Image();
16
+ img.onload = () => resolve(true);
17
+ img.onerror = () => resolve(false);
18
+ img.src = src;
19
+ });
20
+ for (const url of thumbs) {
21
+ try {
22
+ const ok = await loadImage(url);
23
+ if (ok)
24
+ return true;
25
+ }
26
+ catch (_a) {
27
+ // ignore and try next thumbnail
28
+ }
29
+ }
30
+ // Fallback: try oEmbed endpoint (may be subject to CORS in some environments)
31
+ try {
32
+ const watchUrl = `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`;
33
+ const oembedUrl = `https://www.youtube.com/oembed?url=${encodeURIComponent(watchUrl)}&format=json`;
34
+ const res = await fetch(oembedUrl, { method: "GET" });
35
+ return res.ok;
36
+ }
37
+ catch (_b) {
38
+ return false;
39
+ }
40
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-youtube-link.js","sourceRoot":"","sources":["../../src/youtube/validate-youtube-link.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,kDA0CC;AA1CM,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IACxD,MAAM,EAAE,gBAAgB,EAAE,GAAG,wDAAa,4BAA4B,GAAC,CAAA;IACvE,MAAM,OAAO,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAA;IAC1C,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAA;IAE1B,sEAAsE;IACtE,yEAAyE;IACzE,MAAM,MAAM,GAAG;QACb,8BAA8B,OAAO,oBAAoB;QACzD,8BAA8B,OAAO,gBAAgB;QACrD,8BAA8B,OAAO,gBAAgB;QACrD,8BAA8B,OAAO,cAAc;KACpD,CAAA;IAED,MAAM,SAAS,GAAG,CAAC,GAAW,EAAE,EAAE,CAChC,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;QAC/B,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE,CAAA;QACvB,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QAChC,GAAG,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QAClC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAA;IACf,CAAC,CAAC,CAAA;IAEJ,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAA;YAC/B,IAAI,EAAE;gBAAE,OAAO,IAAI,CAAA;QACrB,CAAC;QAAC,WAAM,CAAC;YACP,gCAAgC;QAClC,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,mCAAmC,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAA;QACjF,MAAM,SAAS,GAAG,sCAAsC,kBAAkB,CACxE,QAAQ,CACT,cAAc,CAAA;QACf,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;QACrD,OAAO,GAAG,CAAC,EAAE,CAAA;IACf,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herowcode/utils",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "A lightweight collection of utility functions for everyday JavaScript/TypeScript development",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -35,6 +35,11 @@
35
35
  "types": "./dist/function.d.ts",
36
36
  "import": "./dist/function.esm.js",
37
37
  "require": "./dist/function.js"
38
+ },
39
+ "./youtube": {
40
+ "types": "./dist/youtube.d.ts",
41
+ "import": "./dist/youtube.esm.js",
42
+ "require": "./dist/youtube.js"
38
43
  }
39
44
  },
40
45
  "files": [
@@ -47,14 +52,15 @@
47
52
  "build:types": "tsc --project tsconfig.types.json",
48
53
  "lint": "biome check --write --unsafe",
49
54
  "clean": "rm -rf dist",
50
- "test": "jest",
51
- "test:watch": "jest --watch",
52
- "test:coverage": "jest --coverage",
55
+ "test": "vitest",
56
+ "test:ui": "vitest --ui",
57
+ "test:run": "vitest run",
58
+ "test:coverage": "vitest run --coverage",
53
59
  "prepare": "yarn build",
54
- "prepublishOnly": "yarn lint && yarn test && yarn build",
55
- "version:patch": "yarn lint && yarn test && yarn build && npm version patch && git push && git push --tags",
56
- "version:minor": "yarn lint && yarn test && yarn build && npm version minor && git push && git push --tags",
57
- "version:major": "yarn lint && yarn test && yarn build && npm version major && git push && git push --tags"
60
+ "prepublishOnly": "yarn lint && yarn test:run && yarn build",
61
+ "version:patch": "yarn lint && yarn test:run && yarn build && npm version patch && git push && git push --tags",
62
+ "version:minor": "yarn lint && yarn test:run && yarn build && npm version minor && git push && git push --tags",
63
+ "version:major": "yarn lint && yarn test:run && yarn build && npm version major && git push && git push --tags"
58
64
  },
59
65
  "keywords": [
60
66
  "utils",
@@ -72,15 +78,24 @@
72
78
  },
73
79
  "license": "MIT",
74
80
  "dependencies": {
75
- "dayjs": "^1.11.10"
81
+ "dayjs": "^1.11.10",
82
+ "react": "19.1.1"
76
83
  },
77
84
  "devDependencies": {
78
85
  "@biomejs/biome": "2.2.3",
79
- "@types/jest": "^29.5.5",
80
- "jest": "^29.7.0",
86
+ "@testing-library/dom": "10.4.1",
87
+ "@testing-library/jest-dom": "6.8.0",
88
+ "@testing-library/react": "16.3.0",
89
+ "@testing-library/react-hooks": "8.0.1",
90
+ "@types/react": "19.1.12",
91
+ "@types/react-dom": "19.1.9",
92
+ "@vitest/coverage-v8": "3.2.4",
93
+ "@vitest/ui": "3.2.4",
81
94
  "jest-environment-jsdom": "30.1.2",
82
- "ts-jest": "^29.1.1",
83
- "typescript": "^5.2.2"
95
+ "jsdom": "26.1.0",
96
+ "react-dom": "19.1.1",
97
+ "typescript": "^5.2.2",
98
+ "vitest": "3.2.4"
84
99
  },
85
100
  "repository": {
86
101
  "type": "git",