fontdue-js 2.20.0 → 2.21.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/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 2.21.0
2
+
3
+ - Fixed `CharacterViewer` silently dropping characters from supplementary-plane Unicode blocks (e.g. Latin Extended-F, Enclosed Alphanumeric Supplement). Range expansion now uses `codePointAt` / `String.fromCodePoint` so characters above U+FFFF render correctly instead of getting filtered out as unpaired surrogates
4
+ - Fixed false-positive CORS error modal appearing on network failures unrelated to CORS (DNS, timeout, server unreachable). The modal now only shows for genuine CORS misconfigurations; other fetch failures log a warning instead
5
+
6
+ ## 2.20.1
7
+
8
+ - Fixed `useAutofit` measuring multi-line text as a single line, causing the font size to be too small when type tester content contains line breaks
9
+
1
10
  ## 2.20.0
2
11
 
3
12
  - **Font loading now uses the FontFace API instead of CSS `@font-face`**. Fonts loaded by fontdue-js components (TypeTester, StoreModal, CharacterViewer, etc.) are no longer loaded via CSS stylesheet injection. Instead, font files are fetched as binary data and loaded via the browser's `FontFace` API. **Note:** components like TypeTesters and CharacterViewer previously loaded the entire family's CSS, which made all styles available page-wide as a side effect. They now only load the specific style they're rendering. If you render font styles outside of fontdue-js components (e.g., in a specimen section), you'll need to load those fonts yourself — either with the new `useFont` hook and `webfontSources`, or with your own `@font-face` rules.
@@ -15,6 +15,7 @@ var _CharacterViewer_family2 = _interopRequireDefault(require("../../__generated
15
15
  var _react = _interopRequireWildcard(require("react"));
16
16
  var _reactRelay = require("react-relay");
17
17
  var _resizeObserver = _interopRequireDefault(require("@react-hook/resize-observer"));
18
+ var _retryImport = _interopRequireDefault(require("../../retryImport"));
18
19
  var _utils = require("../../utils");
19
20
  var _useFontStyle = _interopRequireDefault(require("../useFontStyle"));
20
21
  var _useSerializablePreloadedQuery = _interopRequireDefault(require("../../relay/useSerializablePreloadedQuery"));
@@ -83,9 +84,9 @@ function useUnicodeData() {
83
84
  const [data, setData] = (0, _react.useState)();
84
85
  (0, _react.useEffect)(() => {
85
86
  function fetchData() {
86
- Promise.resolve().then(() => _interopRequireWildcard(require('../../data/unicodeData'))).then(data => {
87
+ (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../../data/unicodeData')))).then(data => {
87
88
  if (!ignore) setData(data.default);
88
- });
89
+ }).catch(() => {});
89
90
  }
90
91
  let ignore = false;
91
92
  fetchData();
@@ -109,10 +110,10 @@ function compareGlyphs(a, b) {
109
110
  return true;
110
111
  }
111
112
  function charCode(c) {
112
- return c.charCodeAt(0);
113
+ return c.codePointAt(0) ?? 0;
113
114
  }
114
115
  function fromCharCode(code) {
115
- return String.fromCharCode(code);
116
+ return String.fromCodePoint(code);
116
117
  }
117
118
  function flattenCharacterList(charSet, glyphNames) {
118
119
  if (!glyphNames || !charSet || !charSet.characters) return null;
@@ -14,12 +14,13 @@ var _ConfigContext = _interopRequireWildcard(require("../ConfigContext"));
14
14
  var _reducer = require("../../reducer");
15
15
  var _ComponentsContext = _interopRequireDefault(require("../ComponentsContext"));
16
16
  var _UrlContext = _interopRequireDefault(require("../UrlContext"));
17
+ var _retryImport = _interopRequireDefault(require("../../retryImport"));
17
18
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
19
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
19
20
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
20
- const ConsentBanner = /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../ConsentBanner'))));
21
- const Tracking = /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../Tracking'))));
22
- const ServerConfigProvider = /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../ServerConfigProvider'))));
21
+ const ConsentBanner = /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../ConsentBanner')))));
22
+ const Tracking = /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../Tracking')))));
23
+ const ServerConfigProvider = /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../ServerConfigProvider')))));
23
24
  function FontdueProviderClientComponent(_ref) {
24
25
  let {
25
26
  children,
@@ -9,6 +9,7 @@ var _reactErrorBoundary = require("react-error-boundary");
9
9
  var _reactDom = _interopRequireDefault(require("react-dom"));
10
10
  var _uuid = require("uuid");
11
11
  var _reducer = require("../../reducer");
12
+ var _retryImport = _interopRequireDefault(require("../../retryImport"));
12
13
  var _utils = require("../../utils");
13
14
  var _FontdueProvider = _interopRequireDefault(require("../FontdueProvider"));
14
15
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -17,31 +18,31 @@ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return
17
18
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
18
19
  const customElementMap = {
19
20
  // @ts-ignore
20
- 'fontdue-add-to-cart-banner': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../AddToCartBanner')))),
21
+ 'fontdue-add-to-cart-banner': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../AddToCartBanner'))))),
21
22
  // @ts-ignore
22
- 'fontdue-buying-options': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../BuyingOptions')))),
23
- 'fontdue-buy-button': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../BuyButton')))),
24
- 'fontdue-cart': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../Cart')))),
25
- 'fontdue-cart-button': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CartButton')))),
26
- 'fontdue-character-viewer': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CharacterViewer')))),
23
+ 'fontdue-buying-options': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../BuyingOptions'))))),
24
+ 'fontdue-buy-button': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../BuyButton'))))),
25
+ 'fontdue-cart': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../Cart'))))),
26
+ 'fontdue-cart-button': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CartButton'))))),
27
+ 'fontdue-character-viewer': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CharacterViewer'))))),
27
28
  // @ts-ignore
28
- 'fontdue-collection-aa': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CollectionAa')))),
29
+ 'fontdue-collection-aa': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CollectionAa'))))),
29
30
  // @ts-ignore
30
- 'fontdue-cookie-notification': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CookieNotification')))),
31
- 'fontdue-font-families': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../FontFamilies')))),
32
- 'fontdue-customer-login-form': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CustomerLoginForm')))),
33
- 'fontdue-newsletter-signup': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../NewsletterSignup/NewsletterSignupElement')))),
34
- 'fontdue-node-password-form': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../NodePasswordForm')))),
31
+ 'fontdue-cookie-notification': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CookieNotification'))))),
32
+ 'fontdue-font-families': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../FontFamilies'))))),
33
+ 'fontdue-customer-login-form': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../CustomerLoginForm'))))),
34
+ 'fontdue-newsletter-signup': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../NewsletterSignup/NewsletterSignupElement'))))),
35
+ 'fontdue-node-password-form': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../NodePasswordForm'))))),
35
36
  // @ts-ignore
36
- 'fontdue-precart': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../Precart')))),
37
+ 'fontdue-precart': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../Precart'))))),
37
38
  // @ts-ignore
38
- 'fontdue-specimen-link': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../SpecimenLink')))),
39
+ 'fontdue-specimen-link': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../SpecimenLink'))))),
39
40
  // @ts-ignore
40
- 'fontdue-sticky-nav': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../StickyNav')))),
41
- 'fontdue-store-modal': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../StoreModal')))),
42
- 'fontdue-test-fonts-form': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../TestFontsForm/TestFontsFormElement')))),
43
- 'fontdue-type-tester': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../TypeTester/TypeTesterStandaloneElement')))),
44
- 'fontdue-type-testers': /*#__PURE__*/(0, _react.lazy)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../TypeTesters/TypeTestersElement'))))
41
+ 'fontdue-sticky-nav': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../StickyNav'))))),
42
+ 'fontdue-store-modal': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../StoreModal'))))),
43
+ 'fontdue-test-fonts-form': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../TestFontsForm/TestFontsFormElement'))))),
44
+ 'fontdue-type-tester': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../TypeTester/TypeTesterStandaloneElement'))))),
45
+ 'fontdue-type-testers': /*#__PURE__*/(0, _react.lazy)(() => (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('../TypeTesters/TypeTestersElement')))))
45
46
  };
46
47
  Object.keys(customElementMap).forEach(elementName => {
47
48
  // this might be more sophisticated in the future with a shadow DOM, etc,
package/dist/corsError.js CHANGED
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.handlePossibleCorsError = handlePossibleCorsError;
7
+ var _retryImport = _interopRequireDefault(require("./retryImport"));
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
7
9
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
8
10
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
9
11
  let detected = false;
@@ -35,6 +37,32 @@ function startPolling(fetchUrl) {
35
37
  }
36
38
  }, 3000);
37
39
  }
40
+ async function isCorsBlocked(fetchUrl) {
41
+ // A no-cors fetch bypasses CORS entirely — if it also fails,
42
+ // the server is unreachable (network error), not CORS-blocked.
43
+ try {
44
+ await fetch(fetchUrl, {
45
+ method: 'HEAD',
46
+ mode: 'no-cors'
47
+ });
48
+ return true; // server reachable, so the original error was CORS
49
+ } catch {
50
+ return false; // server unreachable, network error
51
+ }
52
+ }
53
+ function showCorsError(origin, fetchUrl) {
54
+ console.error(`[Fontdue] Cross-origin request to ${fetchUrl} was blocked.\n\n` + `Your website (${origin}) is not listed as an allowed origin ` + `in your Fontdue CORS settings.\n\n` + `To fix this:\n` + `1. Log in to your Fontdue dashboard\n` + `2. Go to Settings \u2192 Security\n` + `3. Add "${origin}" to the "Cross-origin API access" field\n` + `4. Save \u2014 this page will reload automatically`);
55
+ (0, _retryImport.default)(() => Promise.resolve().then(() => _interopRequireWildcard(require('./components/CorsErrorModal')))).then(_ref => {
56
+ let {
57
+ renderCorsErrorModal
58
+ } = _ref;
59
+ renderCorsErrorModal(origin, fetchUrl);
60
+ }).catch(() => {
61
+ // Chunk failed to load — the console.error above already
62
+ // told the developer what's wrong.
63
+ });
64
+ startPolling(fetchUrl);
65
+ }
38
66
  function handlePossibleCorsError(error, fetchUrl) {
39
67
  if (typeof window === 'undefined') return false;
40
68
  if (!(error instanceof TypeError)) return false;
@@ -43,13 +71,20 @@ function handlePossibleCorsError(error, fetchUrl) {
43
71
  if (detected) return true;
44
72
  detected = true;
45
73
  const origin = window.location.origin;
46
- console.error(`[Fontdue] Cross-origin request to ${fetchUrl} was blocked.\n\n` + `Your website (${origin}) is not listed as an allowed origin ` + `in your Fontdue CORS settings.\n\n` + `To fix this:\n` + `1. Log in to your Fontdue dashboard\n` + `2. Go to Settings \u2192 Security\n` + `3. Add "${origin}" to the "Cross-origin API access" field\n` + `4. Save \u2014 this page will reload automatically`);
47
- Promise.resolve().then(() => _interopRequireWildcard(require('./components/CorsErrorModal'))).then(_ref => {
48
- let {
49
- renderCorsErrorModal
50
- } = _ref;
51
- renderCorsErrorModal(origin, fetchUrl);
74
+
75
+ // Verify this is actually a CORS error and not a network failure.
76
+ // We do this async — handlePossibleCorsError still returns true
77
+ // synchronously so the caller can return a stub response instead
78
+ // of throwing, but we only show the modal if CORS is confirmed.
79
+ isCorsBlocked(fetchUrl).then(blocked => {
80
+ if (blocked) {
81
+ showCorsError(origin, fetchUrl);
82
+ } else {
83
+ // Network error, not CORS — reset so a real CORS error
84
+ // can still be detected if connectivity recovers.
85
+ detected = false;
86
+ console.warn(`[Fontdue] Request to ${fetchUrl} failed — this looks like a network issue, not a CORS configuration problem.`);
87
+ }
52
88
  });
53
- startPolling(fetchUrl);
54
89
  return true;
55
90
  }
@@ -87,10 +87,11 @@ const useAutofit = _ref => {
87
87
  const availableWidth = containerWidth - padding;
88
88
  if (availableWidth <= 0 || !text) return;
89
89
 
90
- // Use DOM measurement when variable font settings are active
91
- // (canvas doesn't support fontVariationSettings in most browsers).
92
- // Otherwise use canvas for better performance (no layout reflow).
93
- const refWidth = fontVariationSettings ? measureWithDOM(text, fontFamily, fontWeight, fontStyle, letterSpacing, fontVariationSettings) : measureWithCanvas(text, fontFamily, fontWeight, fontStyle, letterSpacing);
90
+ // Split by newlines and measure the widest line, since the rendered
91
+ // text preserves line breaks (Draft.js uses white-space: pre-wrap).
92
+ const lines = text.split('\n');
93
+ const measure = fontVariationSettings ? line => measureWithDOM(line, fontFamily, fontWeight, fontStyle, letterSpacing, fontVariationSettings) : line => measureWithCanvas(line, fontFamily, fontWeight, fontStyle, letterSpacing);
94
+ const refWidth = Math.max(...lines.map(measure));
94
95
  if (refWidth <= 0) return;
95
96
 
96
97
  // Text width scales linearly with font size.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Wraps a dynamic import in retry logic.
3
+ *
4
+ * Retries the factory up to `retries` times with a delay between attempts.
5
+ * Re-invoking the factory works on Safari (which doesn't cache module-map
6
+ * failures) and handles transient network blips on all browsers. Chrome and
7
+ * Firefox cache failed module fetches in the module map, so retries of the
8
+ * same specifier may return the cached rejection without a network request;
9
+ * the delay helps partially mitigate that, but is not a guaranteed fix.
10
+ *
11
+ * See: https://github.com/whatwg/html/issues/6768
12
+ */
13
+ export default function retryImport<T>(load: () => Promise<T>, retries?: number, delay?: number): Promise<T>;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = retryImport;
7
+ /**
8
+ * Wraps a dynamic import in retry logic.
9
+ *
10
+ * Retries the factory up to `retries` times with a delay between attempts.
11
+ * Re-invoking the factory works on Safari (which doesn't cache module-map
12
+ * failures) and handles transient network blips on all browsers. Chrome and
13
+ * Firefox cache failed module fetches in the module map, so retries of the
14
+ * same specifier may return the cached rejection without a network request;
15
+ * the delay helps partially mitigate that, but is not a guaranteed fix.
16
+ *
17
+ * See: https://github.com/whatwg/html/issues/6768
18
+ */
19
+ function retryImport(load) {
20
+ let retries = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
21
+ let delay = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1000;
22
+ return load().catch(error => {
23
+ if (retries === 0) throw error;
24
+ return new Promise(resolve => setTimeout(resolve, delay)).then(() => retryImport(load, retries - 1, delay));
25
+ });
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdue-js",
3
- "version": "2.20.0",
3
+ "version": "2.21.0",
4
4
  "scripts": {
5
5
  "build": "npm run relay && run-p build-js build-css build-ts",
6
6
  "build-js": "babel src --out-dir dist --extensions .ts,.tsx,.js,.jsx",