favicon-stealer 1.9.0 → 3.0.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/README.md CHANGED
@@ -27,14 +27,14 @@ import { Favicon } from 'favicon-stealer';
27
27
  | `alt` | `string` | The alt text for the favicon image. |
28
28
  | `size` | `number` | The size of the favicon in pixels. Default is 32. |
29
29
  | `className` | `string` | A class name to apply to the element. |
30
- | `timeout` | `number` | The timeout in milliseconds for fetching the favicon. Default is 3000 (3 seconds). |
30
+ | `timeout` | `number` | The timeout in milliseconds before giving up on a slow/unresponsive **auto-detected** source and trying the next one. A provided `src` is exempt — it is only abandoned on a real load failure, never on a timeout. Default is 2000 (2 seconds). |
31
31
  | `lazy` | `boolean` | Whether to load the favicon lazily. Default is false. |
32
32
  | `border` | `boolean` | Whether to show a border around the favicon. Default is false. |
33
33
  | `padding` | `number` | The padding in pixels.(px) Default is 0. |
34
34
  | `background` | `string` | The background color of the favicon. Default is transparent.(in hex) |
35
35
  | `borderRadius` | `number` | The border radius in pixels.(px) Default is 0. |
36
36
  | `preferFallback` | `boolean` | Whether to prefer fallback service (e.g.Google's favicon service) over the website's own favicon. Default is false. |
37
- | `preferSrc` | `boolean` | Whether to prefer the local image source over the website's own favicon(if both are provided). Default is true. |
37
+ | `preferSrc` | `boolean` | Whether to try the provided `src` before auto-detecting the website's own favicon (if both are provided). If `src` fails to load it falls back to auto-detection either way. Default is true. |
38
38
 
39
39
 
40
40
  # NPM Package
@@ -59,4 +59,10 @@ MIT License
59
59
  - v1.5.0: Update default timeout to 3000(3 seconds) (2025.2.27)
60
60
  - v1.6.0: change prop preferGoogle to preferFallback (2025.2.27)
61
61
  - v1.8.0: Add props(src, alt, preferSrc), add new fallback(favicon.im)(2025.3.13)
62
- - v1.9.0: Fix show bug when use 'src'(2025.3.26)
62
+ - v1.9.0: Fix show bug when use 'src'(2025.3.26)
63
+ - v2.0.0: Dual ESM/CJS build with an `exports` map — fixes Vite 8 / modern bundler SSR "exports is not defined". Breaking: output is now a single bundle, deep sub-path imports (e.g. `favicon-stealer/dist/Favicon`) are gone (2026.6.20)
64
+ - v3.0.0: Rewrite of favicon resolution + rendering.
65
+ - **BREAKING**: `react` is now a `peerDependency` (consumer must provide React); the unused `react-dom` peer was dropped.
66
+ - **BREAKING**: styling is fully self-contained inline `style` — no Tailwind classes are emitted, and default `padding`/`borderRadius`/`background` no longer override your `className`.
67
+ - **BREAKING**: `src` behavior — `preferSrc` is now honored, a provided `src` no longer times out (it falls back only on a real load failure), the default `timeout` is now 2000ms (was 3000), and the candidate source list was trimmed, so some sites/inputs resolve differently.
68
+ - Fixes: `src` load-failure no longer hangs on the skeleton; reload on `url`/`src` change without a stale frame or wasted request; cache/SSR hits use `img.decode()` (feature-detected — falls back to `naturalWidth` on engines without it, e.g. old WebViews / jsdom; no false-negative on dimension-less SVGs); offscreen `lazy` images wait for the viewport instead of being timed out through every source to the letter fallback; de-duped sources (no `key` collision); `border` works; `alt=""` is honored for decorative icons (and the loading image is silent to screen readers until shown); empty/invalid `url` no longer renders a blank box; pulse keyframes inject once per document (DOM-id de-duped across HMR / duplicate copies); `FaviconProps` is exported. (2026.6.21)
@@ -1,5 +1,6 @@
1
- import React from "react";
2
- interface IProps {
1
+ import React from 'react';
2
+
3
+ interface FaviconProps {
3
4
  url: string;
4
5
  src?: string;
5
6
  alt?: string;
@@ -14,5 +15,6 @@ interface IProps {
14
15
  preferFallback?: boolean;
15
16
  preferSrc?: boolean;
16
17
  }
17
- declare const Favicon: ({ url, src, alt, size, className, timeout, border, padding, background, borderRadius, lazy, preferFallback, preferSrc, }: IProps) => React.JSX.Element;
18
- export default Favicon;
18
+ declare const Favicon: ({ url, src, alt, size, className, timeout, border, padding, background, borderRadius, lazy, preferFallback, preferSrc, }: FaviconProps) => React.ReactElement;
19
+
20
+ export { Favicon, type FaviconProps };
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+
3
+ interface FaviconProps {
4
+ url: string;
5
+ src?: string;
6
+ alt?: string;
7
+ size?: number;
8
+ className?: string;
9
+ timeout?: number;
10
+ border?: boolean;
11
+ padding?: number;
12
+ background?: string;
13
+ borderRadius?: number;
14
+ lazy?: boolean;
15
+ preferFallback?: boolean;
16
+ preferSrc?: boolean;
17
+ }
18
+ declare const Favicon: ({ url, src, alt, size, className, timeout, border, padding, background, borderRadius, lazy, preferFallback, preferSrc, }: FaviconProps) => React.ReactElement;
19
+
20
+ export { Favicon, type FaviconProps };
package/dist/index.js CHANGED
@@ -1,8 +1,248 @@
1
+ "use client";
1
2
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __defProps = Object.defineProperties;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
13
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
14
+ var __spreadValues = (a, b) => {
15
+ for (var prop in b || (b = {}))
16
+ if (__hasOwnProp.call(b, prop))
17
+ __defNormalProp(a, prop, b[prop]);
18
+ if (__getOwnPropSymbols)
19
+ for (var prop of __getOwnPropSymbols(b)) {
20
+ if (__propIsEnum.call(b, prop))
21
+ __defNormalProp(a, prop, b[prop]);
22
+ }
23
+ return a;
4
24
  };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Favicon = void 0;
7
- var Favicon_1 = require("./Favicon");
8
- Object.defineProperty(exports, "Favicon", { enumerable: true, get: function () { return __importDefault(Favicon_1).default; } });
25
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
26
+ var __export = (target, all) => {
27
+ for (var name in all)
28
+ __defProp(target, name, { get: all[name], enumerable: true });
29
+ };
30
+ var __copyProps = (to, from, except, desc) => {
31
+ if (from && typeof from === "object" || typeof from === "function") {
32
+ for (let key of __getOwnPropNames(from))
33
+ if (!__hasOwnProp.call(to, key) && key !== except)
34
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
35
+ }
36
+ return to;
37
+ };
38
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
39
+ // If the importer is in node compatibility mode or this is not an ESM
40
+ // file that has been converted to a CommonJS file using a Babel-
41
+ // compatible transform (i.e. "__esModule" has not been set), then set
42
+ // "default" to the CommonJS "module.exports" for node compatibility.
43
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
44
+ mod
45
+ ));
46
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
47
+
48
+ // src/index.tsx
49
+ var index_exports = {};
50
+ __export(index_exports, {
51
+ Favicon: () => Favicon_default
52
+ });
53
+ module.exports = __toCommonJS(index_exports);
54
+
55
+ // src/lib/utils/index.ts
56
+ var getDomain = (url) => {
57
+ try {
58
+ const hasProtocol = /^https?:\/\//i.test(url);
59
+ const urlWithProtocol = hasProtocol ? url : `https://${url}`;
60
+ const domain = new URL(urlWithProtocol).hostname;
61
+ return domain.replace(/^www\./, "");
62
+ } catch (error) {
63
+ return url;
64
+ }
65
+ };
66
+
67
+ // src/Favicon.tsx
68
+ var import_react = __toESM(require("react"));
69
+ var reducer = (state, action) => {
70
+ switch (action.type) {
71
+ case "reset":
72
+ return { index: 0, status: "loading" };
73
+ case "load":
74
+ return state.status === "loaded" ? state : __spreadProps(__spreadValues({}, state), { status: "loaded" });
75
+ case "error": {
76
+ const next = state.index + 1;
77
+ return next < action.total ? { index: next, status: "loading" } : { index: state.index, status: "error" };
78
+ }
79
+ default:
80
+ return state;
81
+ }
82
+ };
83
+ var KEYFRAMES_ID = "favicon-stealer-keyframes";
84
+ var PULSE_KEYFRAMES = "@keyframes favicon-stealer-pulse{0%,100%{opacity:1}50%{opacity:.4}}";
85
+ var NEUTRAL_BG = "color-mix(in srgb, currentColor 10%, transparent)";
86
+ var keyframesInjected = false;
87
+ var injectKeyframes = () => {
88
+ if (keyframesInjected || typeof document === "undefined") return;
89
+ keyframesInjected = true;
90
+ if (document.getElementById(KEYFRAMES_ID)) return;
91
+ const el = document.createElement("style");
92
+ el.id = KEYFRAMES_ID;
93
+ el.textContent = PULSE_KEYFRAMES;
94
+ document.head.appendChild(el);
95
+ };
96
+ var Favicon = ({
97
+ url,
98
+ src,
99
+ alt,
100
+ size = 32,
101
+ className = "",
102
+ timeout = 2e3,
103
+ border = false,
104
+ padding = 0,
105
+ background = "transparent",
106
+ borderRadius = 0,
107
+ lazy = false,
108
+ preferFallback = false,
109
+ preferSrc = true
110
+ }) => {
111
+ const domain = (0, import_react.useMemo)(() => getDomain(url), [url]);
112
+ const sources = (0, import_react.useMemo)(() => {
113
+ const standard = [
114
+ `https://${domain}/favicon.ico`,
115
+ `https://${domain}/apple-touch-icon.png`,
116
+ `https://${domain}/logo.svg`,
117
+ `https://${domain}/logo.png`
118
+ ];
119
+ const services = [
120
+ `https://favicon.im/${domain}?larger=true`,
121
+ `https://favicon.im/${domain}`,
122
+ `https://www.google.com/s2/favicons?domain=https://${domain}&sz=64`,
123
+ `https://www.google.com/s2/favicons?domain=http://${domain}&sz=64`
124
+ ];
125
+ const base = preferFallback ? [...services, ...standard] : [...standard, ...services];
126
+ const withSrc = !src ? base : preferSrc ? [src, ...base] : [...base, src];
127
+ return [...new Set(withSrc)];
128
+ }, [domain, src, preferFallback, preferSrc]);
129
+ const [state, dispatch] = (0, import_react.useReducer)(reducer, {
130
+ index: 0,
131
+ status: "loading"
132
+ });
133
+ const [prevSources, setPrevSources] = (0, import_react.useState)(sources);
134
+ if (sources !== prevSources) {
135
+ setPrevSources(sources);
136
+ dispatch({ type: "reset" });
137
+ }
138
+ const currentSrc = sources[state.index];
139
+ const isLoading = state.status === "loading";
140
+ const hasError = state.status === "error";
141
+ const isUserSrc = currentSrc === src;
142
+ (0, import_react.useEffect)(() => {
143
+ injectKeyframes();
144
+ }, []);
145
+ const imgRef = (0, import_react.useRef)(null);
146
+ (0, import_react.useEffect)(() => {
147
+ const img = imgRef.current;
148
+ if (!img || state.status !== "loading" || !img.complete) return;
149
+ let cancelled = false;
150
+ const onOk = () => !cancelled && dispatch({ type: "load" });
151
+ const onFail = () => !cancelled && dispatch({ type: "error", total: sources.length });
152
+ if (typeof img.decode === "function") {
153
+ img.decode().then(onOk, onFail);
154
+ } else {
155
+ img.naturalWidth > 0 ? onOk() : onFail();
156
+ }
157
+ return () => {
158
+ cancelled = true;
159
+ };
160
+ }, [state.status, currentSrc, sources]);
161
+ (0, import_react.useEffect)(() => {
162
+ if (state.status !== "loading" || isUserSrc || lazy) return;
163
+ const id = setTimeout(() => {
164
+ dispatch({ type: "error", total: sources.length });
165
+ }, timeout);
166
+ return () => clearTimeout(id);
167
+ }, [state.status, currentSrc, isUserSrc, lazy, timeout, sources]);
168
+ const label = alt != null ? alt : domain ? `${domain} logo` : "favicon";
169
+ const letter = domain.charAt(0).toUpperCase() || "?";
170
+ const neutralSurface = {
171
+ borderRadius: borderRadius || void 0,
172
+ background: NEUTRAL_BG
173
+ };
174
+ return /* @__PURE__ */ import_react.default.createElement(
175
+ "div",
176
+ {
177
+ className,
178
+ style: {
179
+ position: "relative",
180
+ display: "inline-block",
181
+ width: size,
182
+ height: size,
183
+ boxSizing: "border-box",
184
+ // 默认 / 零值不写内联,留给消费方的 className 决定。
185
+ padding: padding || void 0,
186
+ borderRadius: borderRadius || void 0,
187
+ background: background === "transparent" ? void 0 : background,
188
+ border: border ? "1px solid color-mix(in srgb, currentColor 15%, transparent)" : void 0
189
+ }
190
+ },
191
+ isLoading && /* @__PURE__ */ import_react.default.createElement(
192
+ "div",
193
+ {
194
+ style: __spreadProps(__spreadValues({
195
+ position: "absolute",
196
+ inset: padding || 0
197
+ }, neutralSurface), {
198
+ animation: "favicon-stealer-pulse 1.5s ease-in-out infinite"
199
+ })
200
+ }
201
+ ),
202
+ currentSrc && !hasError && /* @__PURE__ */ import_react.default.createElement(
203
+ "img",
204
+ {
205
+ key: currentSrc,
206
+ ref: imgRef,
207
+ src: currentSrc,
208
+ alt: isLoading ? "" : label,
209
+ width: size,
210
+ height: size,
211
+ loading: lazy ? "lazy" : "eager",
212
+ decoding: "async",
213
+ onError: () => dispatch({ type: "error", total: sources.length }),
214
+ onLoad: () => dispatch({ type: "load" }),
215
+ style: {
216
+ width: "100%",
217
+ height: "100%",
218
+ objectFit: "contain",
219
+ opacity: isLoading ? 0 : 1,
220
+ transition: "opacity 0.3s"
221
+ }
222
+ }
223
+ ),
224
+ hasError && /* @__PURE__ */ import_react.default.createElement(
225
+ "div",
226
+ {
227
+ role: "img",
228
+ "aria-label": label,
229
+ style: __spreadProps(__spreadValues({
230
+ width: "100%",
231
+ height: "100%",
232
+ display: "flex",
233
+ alignItems: "center",
234
+ justifyContent: "center"
235
+ }, neutralSurface), {
236
+ fontSize: size * 0.5,
237
+ lineHeight: 1
238
+ })
239
+ },
240
+ letter
241
+ )
242
+ );
243
+ };
244
+ var Favicon_default = Favicon;
245
+ // Annotate the CommonJS export names for ESM import in node:
246
+ 0 && (module.exports = {
247
+ Favicon
248
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,214 @@
1
+ "use client";
2
+ var __defProp = Object.defineProperty;
3
+ var __defProps = Object.defineProperties;
4
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
5
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __spreadValues = (a, b) => {
10
+ for (var prop in b || (b = {}))
11
+ if (__hasOwnProp.call(b, prop))
12
+ __defNormalProp(a, prop, b[prop]);
13
+ if (__getOwnPropSymbols)
14
+ for (var prop of __getOwnPropSymbols(b)) {
15
+ if (__propIsEnum.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ }
18
+ return a;
19
+ };
20
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
21
+
22
+ // src/lib/utils/index.ts
23
+ var getDomain = (url) => {
24
+ try {
25
+ const hasProtocol = /^https?:\/\//i.test(url);
26
+ const urlWithProtocol = hasProtocol ? url : `https://${url}`;
27
+ const domain = new URL(urlWithProtocol).hostname;
28
+ return domain.replace(/^www\./, "");
29
+ } catch (error) {
30
+ return url;
31
+ }
32
+ };
33
+
34
+ // src/Favicon.tsx
35
+ import React, { useEffect, useMemo, useReducer, useRef, useState } from "react";
36
+ var reducer = (state, action) => {
37
+ switch (action.type) {
38
+ case "reset":
39
+ return { index: 0, status: "loading" };
40
+ case "load":
41
+ return state.status === "loaded" ? state : __spreadProps(__spreadValues({}, state), { status: "loaded" });
42
+ case "error": {
43
+ const next = state.index + 1;
44
+ return next < action.total ? { index: next, status: "loading" } : { index: state.index, status: "error" };
45
+ }
46
+ default:
47
+ return state;
48
+ }
49
+ };
50
+ var KEYFRAMES_ID = "favicon-stealer-keyframes";
51
+ var PULSE_KEYFRAMES = "@keyframes favicon-stealer-pulse{0%,100%{opacity:1}50%{opacity:.4}}";
52
+ var NEUTRAL_BG = "color-mix(in srgb, currentColor 10%, transparent)";
53
+ var keyframesInjected = false;
54
+ var injectKeyframes = () => {
55
+ if (keyframesInjected || typeof document === "undefined") return;
56
+ keyframesInjected = true;
57
+ if (document.getElementById(KEYFRAMES_ID)) return;
58
+ const el = document.createElement("style");
59
+ el.id = KEYFRAMES_ID;
60
+ el.textContent = PULSE_KEYFRAMES;
61
+ document.head.appendChild(el);
62
+ };
63
+ var Favicon = ({
64
+ url,
65
+ src,
66
+ alt,
67
+ size = 32,
68
+ className = "",
69
+ timeout = 2e3,
70
+ border = false,
71
+ padding = 0,
72
+ background = "transparent",
73
+ borderRadius = 0,
74
+ lazy = false,
75
+ preferFallback = false,
76
+ preferSrc = true
77
+ }) => {
78
+ const domain = useMemo(() => getDomain(url), [url]);
79
+ const sources = useMemo(() => {
80
+ const standard = [
81
+ `https://${domain}/favicon.ico`,
82
+ `https://${domain}/apple-touch-icon.png`,
83
+ `https://${domain}/logo.svg`,
84
+ `https://${domain}/logo.png`
85
+ ];
86
+ const services = [
87
+ `https://favicon.im/${domain}?larger=true`,
88
+ `https://favicon.im/${domain}`,
89
+ `https://www.google.com/s2/favicons?domain=https://${domain}&sz=64`,
90
+ `https://www.google.com/s2/favicons?domain=http://${domain}&sz=64`
91
+ ];
92
+ const base = preferFallback ? [...services, ...standard] : [...standard, ...services];
93
+ const withSrc = !src ? base : preferSrc ? [src, ...base] : [...base, src];
94
+ return [...new Set(withSrc)];
95
+ }, [domain, src, preferFallback, preferSrc]);
96
+ const [state, dispatch] = useReducer(reducer, {
97
+ index: 0,
98
+ status: "loading"
99
+ });
100
+ const [prevSources, setPrevSources] = useState(sources);
101
+ if (sources !== prevSources) {
102
+ setPrevSources(sources);
103
+ dispatch({ type: "reset" });
104
+ }
105
+ const currentSrc = sources[state.index];
106
+ const isLoading = state.status === "loading";
107
+ const hasError = state.status === "error";
108
+ const isUserSrc = currentSrc === src;
109
+ useEffect(() => {
110
+ injectKeyframes();
111
+ }, []);
112
+ const imgRef = useRef(null);
113
+ useEffect(() => {
114
+ const img = imgRef.current;
115
+ if (!img || state.status !== "loading" || !img.complete) return;
116
+ let cancelled = false;
117
+ const onOk = () => !cancelled && dispatch({ type: "load" });
118
+ const onFail = () => !cancelled && dispatch({ type: "error", total: sources.length });
119
+ if (typeof img.decode === "function") {
120
+ img.decode().then(onOk, onFail);
121
+ } else {
122
+ img.naturalWidth > 0 ? onOk() : onFail();
123
+ }
124
+ return () => {
125
+ cancelled = true;
126
+ };
127
+ }, [state.status, currentSrc, sources]);
128
+ useEffect(() => {
129
+ if (state.status !== "loading" || isUserSrc || lazy) return;
130
+ const id = setTimeout(() => {
131
+ dispatch({ type: "error", total: sources.length });
132
+ }, timeout);
133
+ return () => clearTimeout(id);
134
+ }, [state.status, currentSrc, isUserSrc, lazy, timeout, sources]);
135
+ const label = alt != null ? alt : domain ? `${domain} logo` : "favicon";
136
+ const letter = domain.charAt(0).toUpperCase() || "?";
137
+ const neutralSurface = {
138
+ borderRadius: borderRadius || void 0,
139
+ background: NEUTRAL_BG
140
+ };
141
+ return /* @__PURE__ */ React.createElement(
142
+ "div",
143
+ {
144
+ className,
145
+ style: {
146
+ position: "relative",
147
+ display: "inline-block",
148
+ width: size,
149
+ height: size,
150
+ boxSizing: "border-box",
151
+ // 默认 / 零值不写内联,留给消费方的 className 决定。
152
+ padding: padding || void 0,
153
+ borderRadius: borderRadius || void 0,
154
+ background: background === "transparent" ? void 0 : background,
155
+ border: border ? "1px solid color-mix(in srgb, currentColor 15%, transparent)" : void 0
156
+ }
157
+ },
158
+ isLoading && /* @__PURE__ */ React.createElement(
159
+ "div",
160
+ {
161
+ style: __spreadProps(__spreadValues({
162
+ position: "absolute",
163
+ inset: padding || 0
164
+ }, neutralSurface), {
165
+ animation: "favicon-stealer-pulse 1.5s ease-in-out infinite"
166
+ })
167
+ }
168
+ ),
169
+ currentSrc && !hasError && /* @__PURE__ */ React.createElement(
170
+ "img",
171
+ {
172
+ key: currentSrc,
173
+ ref: imgRef,
174
+ src: currentSrc,
175
+ alt: isLoading ? "" : label,
176
+ width: size,
177
+ height: size,
178
+ loading: lazy ? "lazy" : "eager",
179
+ decoding: "async",
180
+ onError: () => dispatch({ type: "error", total: sources.length }),
181
+ onLoad: () => dispatch({ type: "load" }),
182
+ style: {
183
+ width: "100%",
184
+ height: "100%",
185
+ objectFit: "contain",
186
+ opacity: isLoading ? 0 : 1,
187
+ transition: "opacity 0.3s"
188
+ }
189
+ }
190
+ ),
191
+ hasError && /* @__PURE__ */ React.createElement(
192
+ "div",
193
+ {
194
+ role: "img",
195
+ "aria-label": label,
196
+ style: __spreadProps(__spreadValues({
197
+ width: "100%",
198
+ height: "100%",
199
+ display: "flex",
200
+ alignItems: "center",
201
+ justifyContent: "center"
202
+ }, neutralSurface), {
203
+ fontSize: size * 0.5,
204
+ lineHeight: 1
205
+ })
206
+ },
207
+ letter
208
+ )
209
+ );
210
+ };
211
+ var Favicon_default = Favicon;
212
+ export {
213
+ Favicon_default as Favicon
214
+ };
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "favicon-stealer",
3
- "version": "1.9.0",
3
+ "version": "3.0.0",
4
4
  "main": "dist/index.js",
5
- "types": "dist/types/index.d.ts",
5
+ "module": "dist/index.mjs",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
12
+ }
13
+ },
14
+ "sideEffects": false,
6
15
  "files": [
7
16
  "dist",
8
17
  "LICENSE",
@@ -10,7 +19,7 @@
10
19
  ],
11
20
  "scripts": {
12
21
  "test": "echo \"Error: no test specified\" && exit 1",
13
- "build": "tsc",
22
+ "build": "tsup",
14
23
  "prepublishOnly": "npm run build"
15
24
  },
16
25
  "keywords": [
@@ -24,13 +33,15 @@
24
33
  "author": "Corey Chiu",
25
34
  "license": "MIT",
26
35
  "description": "Get clear and consistent favicon of a website easily",
27
- "dependencies": {
28
- "react": "^19.0.0",
29
- "react-dom": "^19.0.0"
36
+ "peerDependencies": {
37
+ "react": ">=18"
30
38
  },
31
39
  "devDependencies": {
40
+ "react": "^19.0.0",
41
+ "react-dom": "^19.0.0",
32
42
  "@types/react": "^19.0.8",
33
43
  "@types/react-dom": "^19.0.3",
34
- "typescript": "^5.7.3"
44
+ "typescript": "^5.7.3",
45
+ "tsup": "^8.3.5"
35
46
  }
36
47
  }
package/dist/Favicon.js DELETED
@@ -1,130 +0,0 @@
1
- "use strict";
2
- "use client";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- Object.defineProperty(exports, "__esModule", { value: true });
37
- const utils_1 = require("./lib/utils");
38
- const react_1 = __importStar(require("react"));
39
- const Favicon = ({ url, src, alt, size = 32, className = "", timeout = 3000, // 增加到3秒,给网站自己的favicon更多加载时间
40
- border = false, padding = 0, background = "transparent", borderRadius = 0, lazy = false, preferFallback = false, preferSrc = true, }) => {
41
- const domain = (0, utils_1.getDomain)(url);
42
- const [imgSrc, setImgSrc] = (0, react_1.useState)("");
43
- const [fallbackIndex, setFallbackIndex] = (0, react_1.useState)(0);
44
- const [isLoading, setIsLoading] = (0, react_1.useState)(true);
45
- const [hasError, setHasError] = (0, react_1.useState)(false);
46
- const [isInitialized, setIsInitialized] = (0, react_1.useState)(false);
47
- const standardSources = [
48
- `https://${domain}/favicon.ico`,
49
- `https://${domain}/logo.svg`,
50
- `https://${domain}/logo.png`,
51
- `https://${domain}/apple-touch-icon.png`,
52
- `https://${domain}/apple-touch-icon-precomposed.png`,
53
- `https://${domain}/static/img/favicon.ico`,
54
- `https://${domain}/static/img/favicon.png`,
55
- `https://${domain}/img/favicon.png`,
56
- `https://${domain}/img/favicon.ico`,
57
- `https://${domain}/static/img/logo.svg`,
58
- `https://${domain}/apple-touch-icon-precomposed.png`,
59
- ];
60
- const fallbackServices = [
61
- `https://favicon.im/${domain}?larger=true`,
62
- `https://favicon.im/${domain}`,
63
- `https://www.google.com/s2/favicons?domain=https://${domain}&sz=64`,
64
- `https://www.google.com/s2/favicons?domain=http://${domain}&sz=64`,
65
- ];
66
- const fallbackSources = preferFallback
67
- ? [...fallbackServices, ...standardSources]
68
- : [...standardSources, ...fallbackServices];
69
- (0, react_1.useEffect)(() => {
70
- if (!isInitialized) {
71
- if (src) {
72
- setIsLoading(false);
73
- }
74
- else {
75
- setImgSrc(fallbackSources[0]);
76
- }
77
- setIsInitialized(true);
78
- }
79
- }, [isInitialized, fallbackSources, src]);
80
- (0, react_1.useEffect)(() => {
81
- let timeoutId;
82
- if (isLoading && imgSrc && !src) {
83
- timeoutId = setTimeout(() => {
84
- handleError();
85
- }, timeout);
86
- }
87
- return () => {
88
- if (timeoutId) {
89
- clearTimeout(timeoutId);
90
- }
91
- };
92
- }, [imgSrc, isLoading, timeout, src]);
93
- const handleError = () => {
94
- const nextIndex = fallbackIndex + 1;
95
- if (nextIndex < fallbackSources.length) {
96
- setFallbackIndex(nextIndex);
97
- setImgSrc(fallbackSources[nextIndex]);
98
- setIsLoading(true);
99
- }
100
- else {
101
- setHasError(true);
102
- setIsLoading(false);
103
- }
104
- };
105
- const handleLoad = () => {
106
- setIsLoading(false);
107
- setHasError(false);
108
- };
109
- return (react_1.default.createElement("div", { className: `relative inline-block
110
- ${className}
111
- ${border ? "border" : ""}
112
- ${hasError ? "opacity-0" : ""}
113
- ${padding ? `p-[${padding}px]` : ""}
114
- ${borderRadius ? `rounded-[${borderRadius}px]` : ""}
115
- `, style: {
116
- width: size,
117
- height: size,
118
- background: background,
119
- padding: padding ? `${padding}px` : 0,
120
- borderRadius: borderRadius ? `${borderRadius}px` : 0,
121
- } },
122
- isLoading && (react_1.default.createElement("div", { className: "absolute inset-0 animate-pulse" },
123
- react_1.default.createElement("div", { className: "w-full h-full rounded-md bg-gray-200/60" }))),
124
- (src || imgSrc) && (react_1.default.createElement("img", { src: src || imgSrc, alt: alt || `${domain} logo`, width: size, height: size, loading: lazy ? "lazy" : "eager", onError: handleError, onLoad: handleLoad, className: `inline-block transition-opacity duration-300 ${isLoading ? "opacity-0" : "opacity-100"}`, style: {
125
- objectFit: "contain",
126
- display: hasError ? "none" : "inline-block",
127
- } })),
128
- hasError && (react_1.default.createElement("div", { className: "w-full h-full flex items-center justify-center bg-gray-100 rounded-md", style: { fontSize: `${size * 0.5}px` } }, domain.charAt(0).toUpperCase()))));
129
- };
130
- exports.default = Favicon;
@@ -1,17 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getDomain = void 0;
4
- const getDomain = (url) => {
5
- try {
6
- // Add https:// protocol if not present
7
- const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
8
- const domain = new URL(urlWithProtocol).hostname;
9
- // Remove 'www.' prefix if exists
10
- return domain.replace(/^www\./, '');
11
- }
12
- catch (error) {
13
- // Return original input if URL parsing fails
14
- return url;
15
- }
16
- };
17
- exports.getDomain = getDomain;
@@ -1 +0,0 @@
1
- export { default as Favicon } from './Favicon';
@@ -1 +0,0 @@
1
- export declare const getDomain: (url: string) => string;