fontdue-js 3.0.0-alpha9 → 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.
Files changed (135) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +182 -13
  3. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
  4. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +9 -3
  5. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
  6. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +9 -3
  7. package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
  8. package/dist/__generated__/CartOrderUpdateMutation.graphql.js +9 -3
  9. package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
  10. package/dist/__generated__/CartQuery.graphql.js +9 -3
  11. package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
  12. package/dist/__generated__/CartStateUpdateMutation.graphql.js +9 -3
  13. package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
  14. package/dist/__generated__/CharacterViewerIDQuery.graphql.js +9 -3
  15. package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
  16. package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +9 -3
  17. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
  18. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +9 -3
  19. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
  20. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +9 -3
  21. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
  22. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +9 -3
  23. package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
  24. package/dist/__generated__/CollectionAa_Query.graphql.js +9 -3
  25. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
  26. package/dist/__generated__/FontFamiliesQuery.graphql.js +9 -3
  27. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.d.ts +20 -0
  28. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.js +80 -0
  29. package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.d.ts +18 -0
  30. package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.js +56 -0
  31. package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
  32. package/dist/__generated__/PrecartAddToCartMutation.graphql.js +9 -3
  33. package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
  34. package/dist/__generated__/StoreModalCartQuery.graphql.js +9 -3
  35. package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +1 -1
  36. package/dist/__generated__/StoreModalContainerQuery.graphql.js +9 -3
  37. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
  38. package/dist/__generated__/StoreModalIndexQuery.graphql.js +9 -3
  39. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
  40. package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -3
  41. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  42. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -3
  43. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
  44. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +9 -3
  45. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
  46. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +9 -3
  47. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
  48. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +9 -3
  49. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
  50. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +9 -3
  51. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  52. package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
  53. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  54. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +9 -3
  55. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  56. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
  57. package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +2 -1
  58. package/dist/__generated__/useFontStyle_fontStyle.graphql.js +8 -2
  59. package/dist/__tests__/createFontdueFetch.test.js +276 -0
  60. package/dist/__tests__/imageLoader.test.js +62 -0
  61. package/dist/__tests__/metricFallback.test.js +74 -0
  62. package/dist/__tests__/networkFetch.test.js +125 -3
  63. package/dist/__tests__/nextAdapter.test.js +175 -60
  64. package/dist/__tests__/preview.test.js +217 -0
  65. package/dist/__tests__/previewServer.test.js +118 -0
  66. package/dist/__tests__/previewState.test.js +63 -0
  67. package/dist/__tests__/serverConfig.test.js +62 -0
  68. package/dist/components/BuyButton/index.d.ts +2 -2
  69. package/dist/components/BuyButton/index.js +3 -3
  70. package/dist/components/CharacterViewer/index.d.ts +2 -2
  71. package/dist/components/CharacterViewer/index.js +20 -11
  72. package/dist/components/ConfigContext.d.ts +21 -2
  73. package/dist/components/ConfigContext.js +12 -2
  74. package/dist/components/ConnectionErrorToolbar.d.ts +1 -0
  75. package/dist/components/ConnectionErrorToolbar.js +106 -0
  76. package/dist/components/FontdueAdminToolbar/index.d.ts +2 -0
  77. package/dist/components/FontdueAdminToolbar/index.js +299 -0
  78. package/dist/components/FontdueAdminToolbar/previewState.d.ts +7 -0
  79. package/dist/components/FontdueAdminToolbar/previewState.js +58 -0
  80. package/dist/components/FontdueContextProvider/index.js +4 -2
  81. package/dist/components/FontdueProvider/index.js +6 -1
  82. package/dist/components/FontdueProvider/index.server.d.ts +1 -0
  83. package/dist/components/FontdueProvider/index.server.js +10 -0
  84. package/dist/components/NewsletterSignup/index.d.ts +2 -2
  85. package/dist/components/NewsletterSignup/index.js +2 -2
  86. package/dist/components/Root/index.js +16 -2
  87. package/dist/components/TestFontsForm/index.d.ts +2 -2
  88. package/dist/components/TestFontsForm/index.js +2 -2
  89. package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -2
  90. package/dist/components/TypeTester/TypeTesterStandalone.js +2 -2
  91. package/dist/components/TypeTesters/index.d.ts +2 -2
  92. package/dist/components/TypeTesters/index.js +3 -3
  93. package/dist/components/useFontStyle.d.ts +1 -0
  94. package/dist/components/useFontStyle.js +12 -3
  95. package/dist/corsError.d.ts +1 -5
  96. package/dist/corsError.js +23 -13
  97. package/dist/data/unicodeNamesUrl.d.ts +2 -0
  98. package/dist/data/unicodeNamesUrl.js +18 -0
  99. package/dist/data/unicodeNamesVersion.d.ts +1 -0
  100. package/dist/data/unicodeNamesVersion.js +4 -0
  101. package/dist/fallbackFontData.d.ts +2 -0
  102. package/dist/fallbackFontData.js +10 -0
  103. package/dist/fontdue.css +231 -4
  104. package/dist/loadFontdueProviderQuery.d.ts +2 -1
  105. package/dist/loadFontdueProviderQuery.js +5 -2
  106. package/dist/metricFallback.d.ts +48 -0
  107. package/dist/metricFallback.js +98 -0
  108. package/dist/next/image-loader.js +22 -3
  109. package/dist/next/index.d.ts +1 -2
  110. package/dist/next/index.js +14 -6
  111. package/dist/next/registerSingleTenantResolver.d.ts +1 -0
  112. package/dist/next/registerSingleTenantResolver.js +35 -0
  113. package/dist/next/revalidate.js +1 -1
  114. package/dist/next/tenant.d.ts +4 -4
  115. package/dist/next/tenant.js +89 -58
  116. package/dist/preview/constants.d.ts +9 -0
  117. package/dist/preview/constants.js +117 -0
  118. package/dist/preview/index.d.ts +53 -0
  119. package/dist/preview/index.js +190 -0
  120. package/dist/preview/server.d.ts +20 -0
  121. package/dist/preview/server.js +89 -0
  122. package/dist/relay/environment.d.ts +8 -0
  123. package/dist/relay/environment.js +81 -35
  124. package/dist/relay/loadSerializableQuery.d.ts +13 -3
  125. package/dist/relay/loadSerializableQuery.js +2 -0
  126. package/dist/relay/serverConfig.d.ts +5 -7
  127. package/dist/relay/serverConfig.js +83 -8
  128. package/dist/scripts/publishUnicodeData.js +68 -0
  129. package/dist/scripts/updateUnicodeData.js +41 -6
  130. package/dist/server/index.d.ts +37 -0
  131. package/dist/server/index.js +160 -0
  132. package/package.json +5 -1
  133. package/types/next-headers.d.ts +9 -0
  134. package/types/next-navigation.d.ts +4 -0
  135. package/vitest.config.ts +5 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<4baa139e690a04efc53c833f3dcd913b>>
2
+ * @generated SignedSource<<a94d6158d79c64567c40e76f81d7008d>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<4baa139e690a04efc53c833f3dcd913b>>
2
+ * @generated SignedSource<<a94d6158d79c64567c40e76f81d7008d>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -462,6 +462,12 @@ const node = function () {
462
462
  "kind": "ScalarField",
463
463
  "name": "lineGap",
464
464
  "storageKey": null
465
+ }, {
466
+ "alias": null,
467
+ "args": null,
468
+ "kind": "ScalarField",
469
+ "name": "avgCharWidth",
470
+ "storageKey": null
465
471
  }],
466
472
  "storageKey": null
467
473
  }, {
@@ -554,12 +560,12 @@ const node = function () {
554
560
  }]
555
561
  },
556
562
  "params": {
557
- "cacheID": "0846a9f2356431e317f88fd2c442500e",
563
+ "cacheID": "1e8ea8e801f6c97ced8499dc87a6afad",
558
564
  "id": null,
559
565
  "metadata": {},
560
566
  "name": "TypeTestersSlugQuery",
561
567
  "operationKind": "query",
562
- "text": "query TypeTestersSlugQuery(\n $collectionSlug: String!\n $tags: [String!]\n $excludeTags: [String!]\n) {\n viewer {\n slug(name: $collectionSlug) {\n collection: fontCollection {\n ...TypeTesters_collection_4Goyz5\n id\n }\n id\n }\n id\n }\n}\n\nfragment FontStyle_fontStyle on FontStyle {\n ...useFontStyle_fontStyle\n}\n\nfragment PriceBarSection_node_3BtHDv on FontCollection {\n id\n name\n collectionType\n sku {\n ...SelectButton_sku_3BtHDv\n id\n priceWithLicenseOptions: price(licenseOptions: [], orderVariables: []) {\n ...Price_price\n }\n }\n totalStyles\n totalStylesPrice(licenseOptions: []) {\n ...Price_price\n }\n}\n\nfragment PriceBar_node_3BtHDv on FontCollection {\n ...PriceBarSection_node_3BtHDv\n parent {\n ...PriceBarSection_node_3BtHDv\n id\n }\n}\n\nfragment Price_price on Money {\n amount\n currency\n}\n\nfragment SKUPrice_sku_3BtHDv on Sku {\n id\n price(licenseOptions: [], orderVariables: []) {\n amount\n ...Price_price\n }\n}\n\nfragment SelectButton_sku_3BtHDv on Sku {\n id\n ...SKUPrice_sku_3BtHDv\n}\n\nfragment TypeTesterFeaturesButton_fontStyle on FontStyle {\n ...useFeaturesData_fontStyle\n}\n\nfragment TypeTesterFeatures_fontStyle on FontStyle {\n ...useFeaturesData_fontStyle\n ...TypeTesterVariableAxes_fontStyle\n}\n\nfragment TypeTesterFloatingToolbar_testers on TypeTester {\n id\n fontStyle {\n ...TypeTesterToolbar_fontStyle\n ...TypeTesterFeatures_fontStyle\n id\n }\n}\n\nfragment TypeTesterStyleSelectData_fontStyle on FontStyle {\n id\n name\n supportedLanguages\n cssWeight\n cssStyle\n cssStretch\n variableInstances {\n name\n coordinates {\n axis\n value\n }\n }\n family {\n id\n name\n }\n}\n\nfragment TypeTesterToolbar_fontStyle on FontStyle {\n ...TypeTesterVariableAxes_fontStyle\n ...TypeTesterFeaturesButton_fontStyle\n}\n\nfragment TypeTesterVariableAxes_fontStyle on FontStyle {\n variableAxes {\n axis\n name\n minValue\n maxValue\n }\n}\n\nfragment TypeTester_fontStyle_3BtHDv on FontStyle {\n id\n ...TypeTesterFeatures_fontStyle\n ...TypeTesterStyleSelectData_fontStyle\n ...FontStyle_fontStyle\n ...TypeTesterVariableAxes_fontStyle\n ...TypeTesterToolbar_fontStyle\n sku {\n ...SelectButton_sku_3BtHDv\n ...SKUPrice_sku_3BtHDv\n id\n basePrice: price {\n amount\n }\n }\n}\n\nfragment TypeTesters_collection_4Goyz5 on FontCollection {\n typeTesters(first: 999, tags: $tags, excludeTags: $excludeTags) {\n edges {\n node {\n id\n content\n size\n lineHeight\n letterSpacing\n autofit\n direction\n fontStyle {\n ...TypeTester_fontStyle_3BtHDv\n id\n family {\n id\n }\n }\n variableSettings {\n axis\n value\n }\n featureSettings {\n feature\n value\n }\n tags\n ...TypeTesterFloatingToolbar_testers\n }\n }\n }\n typeTesterFeatures\n typeTesterAxes\n id\n ...PriceBar_node_3BtHDv\n families: children(collectionTypes: [FAMILY]) {\n id\n ...PriceBar_node_3BtHDv\n }\n parent {\n id\n }\n}\n\nfragment useFeaturesData_fontStyle on FontStyle {\n fontFeatures {\n supportedFeatures\n stylisticSetNames {\n featureName\n humanName\n }\n }\n}\n\nfragment useFontStyle_fontStyle on FontStyle {\n cssFamily\n name\n webfontSources {\n url\n format\n tech\n }\n verticalMetrics {\n unitsPerEm\n ascender\n descender\n lineGap\n }\n}\n"
568
+ "text": "query TypeTestersSlugQuery(\n $collectionSlug: String!\n $tags: [String!]\n $excludeTags: [String!]\n) {\n viewer {\n slug(name: $collectionSlug) {\n collection: fontCollection {\n ...TypeTesters_collection_4Goyz5\n id\n }\n id\n }\n id\n }\n}\n\nfragment FontStyle_fontStyle on FontStyle {\n ...useFontStyle_fontStyle\n}\n\nfragment PriceBarSection_node_3BtHDv on FontCollection {\n id\n name\n collectionType\n sku {\n ...SelectButton_sku_3BtHDv\n id\n priceWithLicenseOptions: price(licenseOptions: [], orderVariables: []) {\n ...Price_price\n }\n }\n totalStyles\n totalStylesPrice(licenseOptions: []) {\n ...Price_price\n }\n}\n\nfragment PriceBar_node_3BtHDv on FontCollection {\n ...PriceBarSection_node_3BtHDv\n parent {\n ...PriceBarSection_node_3BtHDv\n id\n }\n}\n\nfragment Price_price on Money {\n amount\n currency\n}\n\nfragment SKUPrice_sku_3BtHDv on Sku {\n id\n price(licenseOptions: [], orderVariables: []) {\n amount\n ...Price_price\n }\n}\n\nfragment SelectButton_sku_3BtHDv on Sku {\n id\n ...SKUPrice_sku_3BtHDv\n}\n\nfragment TypeTesterFeaturesButton_fontStyle on FontStyle {\n ...useFeaturesData_fontStyle\n}\n\nfragment TypeTesterFeatures_fontStyle on FontStyle {\n ...useFeaturesData_fontStyle\n ...TypeTesterVariableAxes_fontStyle\n}\n\nfragment TypeTesterFloatingToolbar_testers on TypeTester {\n id\n fontStyle {\n ...TypeTesterToolbar_fontStyle\n ...TypeTesterFeatures_fontStyle\n id\n }\n}\n\nfragment TypeTesterStyleSelectData_fontStyle on FontStyle {\n id\n name\n supportedLanguages\n cssWeight\n cssStyle\n cssStretch\n variableInstances {\n name\n coordinates {\n axis\n value\n }\n }\n family {\n id\n name\n }\n}\n\nfragment TypeTesterToolbar_fontStyle on FontStyle {\n ...TypeTesterVariableAxes_fontStyle\n ...TypeTesterFeaturesButton_fontStyle\n}\n\nfragment TypeTesterVariableAxes_fontStyle on FontStyle {\n variableAxes {\n axis\n name\n minValue\n maxValue\n }\n}\n\nfragment TypeTester_fontStyle_3BtHDv on FontStyle {\n id\n ...TypeTesterFeatures_fontStyle\n ...TypeTesterStyleSelectData_fontStyle\n ...FontStyle_fontStyle\n ...TypeTesterVariableAxes_fontStyle\n ...TypeTesterToolbar_fontStyle\n sku {\n ...SelectButton_sku_3BtHDv\n ...SKUPrice_sku_3BtHDv\n id\n basePrice: price {\n amount\n }\n }\n}\n\nfragment TypeTesters_collection_4Goyz5 on FontCollection {\n typeTesters(first: 999, tags: $tags, excludeTags: $excludeTags) {\n edges {\n node {\n id\n content\n size\n lineHeight\n letterSpacing\n autofit\n direction\n fontStyle {\n ...TypeTester_fontStyle_3BtHDv\n id\n family {\n id\n }\n }\n variableSettings {\n axis\n value\n }\n featureSettings {\n feature\n value\n }\n tags\n ...TypeTesterFloatingToolbar_testers\n }\n }\n }\n typeTesterFeatures\n typeTesterAxes\n id\n ...PriceBar_node_3BtHDv\n families: children(collectionTypes: [FAMILY]) {\n id\n ...PriceBar_node_3BtHDv\n }\n parent {\n id\n }\n}\n\nfragment useFeaturesData_fontStyle on FontStyle {\n fontFeatures {\n supportedFeatures\n stylisticSetNames {\n featureName\n humanName\n }\n }\n}\n\nfragment useFontStyle_fontStyle on FontStyle {\n cssFamily\n name\n webfontSources {\n url\n format\n tech\n }\n verticalMetrics {\n unitsPerEm\n ascender\n descender\n lineGap\n avgCharWidth\n }\n}\n"
563
569
  }
564
570
  };
565
571
  }();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<83fdf14f0519371d109651e3e11c221b>>
2
+ * @generated SignedSource<<fdda61b158e716b142895d69f2c1fae9>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -10,6 +10,7 @@ export type useFontStyle_fontStyle$data = {
10
10
  readonly name: string;
11
11
  readonly verticalMetrics: {
12
12
  readonly ascender: number;
13
+ readonly avgCharWidth: number | null;
13
14
  readonly descender: number;
14
15
  readonly lineGap: number | null;
15
16
  readonly unitsPerEm: number;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<83fdf14f0519371d109651e3e11c221b>>
2
+ * @generated SignedSource<<fdda61b158e716b142895d69f2c1fae9>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -83,11 +83,17 @@ const node = {
83
83
  "kind": "ScalarField",
84
84
  "name": "lineGap",
85
85
  "storageKey": null
86
+ }, {
87
+ "alias": null,
88
+ "args": null,
89
+ "kind": "ScalarField",
90
+ "name": "avgCharWidth",
91
+ "storageKey": null
86
92
  }],
87
93
  "storageKey": null
88
94
  }],
89
95
  "type": "FontStyle",
90
96
  "abstractKey": null
91
97
  };
92
- node.hash = "df4c7160c29373e86bea8b6082d19994";
98
+ node.hash = "171c546f40909692656c5e143a98d8a3";
93
99
  export default node;
@@ -0,0 +1,276 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { createFontdueFetch, FontdueNotFoundError } from '../server/index.js';
3
+ import { registerAmbientConfigResolver } from '../relay/serverConfig.js';
4
+ beforeEach(() => {
5
+ vi.unstubAllEnvs();
6
+ vi.unstubAllGlobals();
7
+ });
8
+
9
+ // Several tests inject a per-render config through the ambient resolver seam
10
+ // (the same seam runWithPreview and the Next slot use). Always clear it.
11
+ afterEach(() => {
12
+ registerAmbientConfigResolver(() => undefined);
13
+ });
14
+ function withConfig(config) {
15
+ registerAmbientConfigResolver(() => config);
16
+ }
17
+ function mockFetch(impl) {
18
+ const fetchMock = vi.fn(async (url, init) => impl(url, init));
19
+ vi.stubGlobal('fetch', fetchMock);
20
+ return fetchMock;
21
+ }
22
+ describe('createFontdueFetch', () => {
23
+ it('posts the query to the configured URL and unwraps data', async () => {
24
+ const fetchMock = mockFetch(() => ({
25
+ status: 200,
26
+ json: async () => ({
27
+ data: {
28
+ viewer: {
29
+ id: '1'
30
+ }
31
+ }
32
+ })
33
+ }));
34
+ const fetchGraphql = createFontdueFetch({
35
+ url: 'https://acme.fontdue.com'
36
+ });
37
+ const data = await fetchGraphql('Index', 'query Index { viewer { id } }', {
38
+ a: 1
39
+ });
40
+ expect(data).toEqual({
41
+ viewer: {
42
+ id: '1'
43
+ }
44
+ });
45
+ const [url, init] = fetchMock.mock.calls[0];
46
+ expect(url).toBe('https://acme.fontdue.com/graphql?query=Index');
47
+ expect(init.method).toBe('POST');
48
+ expect(JSON.parse(init.body)).toEqual({
49
+ query: 'query Index { viewer { id } }',
50
+ variables: {
51
+ a: 1
52
+ }
53
+ });
54
+ });
55
+ it('forwards bound headers (the preview Bearer token) on every call', async () => {
56
+ const fetchMock = mockFetch(() => ({
57
+ status: 200,
58
+ json: async () => ({
59
+ data: {}
60
+ })
61
+ }));
62
+ const fetchGraphql = createFontdueFetch({
63
+ url: 'https://acme.fontdue.com',
64
+ headers: {
65
+ authorization: 'Bearer admin-tok'
66
+ }
67
+ });
68
+ await fetchGraphql('A', 'query A { __typename }');
69
+ await fetchGraphql('B', 'query B { __typename }');
70
+ for (const call of fetchMock.mock.calls) {
71
+ const init = call[1];
72
+ expect(init.headers.authorization).toBe('Bearer admin-tok');
73
+ }
74
+ });
75
+ it('throws FontdueNotFoundError on a 404 (host did not resolve)', async () => {
76
+ mockFetch(() => ({
77
+ status: 404,
78
+ json: async () => ({})
79
+ }));
80
+ const fetchGraphql = createFontdueFetch({
81
+ url: 'https://acme.fontdue.com'
82
+ });
83
+ await expect(fetchGraphql('X', 'query X { __typename }')).rejects.toBeInstanceOf(FontdueNotFoundError);
84
+ });
85
+ it('throws on GraphQL errors in a 200 response', async () => {
86
+ mockFetch(() => ({
87
+ status: 200,
88
+ json: async () => ({
89
+ errors: [{
90
+ message: 'boom'
91
+ }]
92
+ })
93
+ }));
94
+ const fetchGraphql = createFontdueFetch({
95
+ url: 'https://acme.fontdue.com'
96
+ });
97
+ await expect(fetchGraphql('X', 'query X { __typename }')).rejects.toThrow('boom');
98
+ });
99
+ it('resolves the URL from the environment when none is passed', async () => {
100
+ vi.stubEnv('FONTDUE_URL', 'https://env.fontdue.com');
101
+ const fetchMock = mockFetch(() => ({
102
+ status: 200,
103
+ json: async () => ({
104
+ data: {}
105
+ })
106
+ }));
107
+ const fetchGraphql = createFontdueFetch();
108
+ await fetchGraphql('Q', 'query Q { __typename }');
109
+ expect(fetchMock.mock.calls[0][0]).toBe('https://env.fontdue.com/graphql?query=Q');
110
+ });
111
+ it('throws a helpful error when no URL is configured (resolved per call)', async () => {
112
+ mockFetch(() => ({
113
+ status: 200,
114
+ json: async () => ({
115
+ data: {}
116
+ })
117
+ }));
118
+ const fetchGraphql = createFontdueFetch();
119
+ await expect(fetchGraphql('Q', 'query Q { __typename }')).rejects.toThrow(/no Fontdue URL configured/);
120
+ });
121
+ describe('Next data cache tags', () => {
122
+ it('opts into force-cache + tags when cacheTags are given (production)', async () => {
123
+ var _init$next;
124
+ vi.stubEnv('NODE_ENV', 'production');
125
+ const fetchMock = mockFetch(() => ({
126
+ status: 200,
127
+ json: async () => ({
128
+ data: {}
129
+ })
130
+ }));
131
+ const fetchGraphql = createFontdueFetch({
132
+ url: 'https://acme.fontdue.com',
133
+ cacheTags: ['graphql:acme.fontdue.com']
134
+ });
135
+ await fetchGraphql('Q', 'query Q { __typename }');
136
+ const init = fetchMock.mock.calls[0][1];
137
+ expect(init.cache).toBe('force-cache');
138
+ // The global `graphql` tag is prepended automatically.
139
+ expect((_init$next = init.next) === null || _init$next === void 0 ? void 0 : _init$next.tags).toEqual(['graphql', 'graphql:acme.fontdue.com']);
140
+ });
141
+ it('leaves the fetch uncached in development even with cacheTags', async () => {
142
+ // In `next dev` the data cache + revalidateTag don't reliably refresh, so
143
+ // we skip the cache opt-in outside production and let every render be fresh.
144
+ vi.stubEnv('NODE_ENV', 'development');
145
+ const fetchMock = mockFetch(() => ({
146
+ status: 200,
147
+ json: async () => ({
148
+ data: {}
149
+ })
150
+ }));
151
+ const fetchGraphql = createFontdueFetch({
152
+ url: 'https://acme.fontdue.com',
153
+ cacheTags: ['graphql:acme.fontdue.com']
154
+ });
155
+ await fetchGraphql('Q', 'query Q { __typename }');
156
+ const init = fetchMock.mock.calls[0][1];
157
+ expect(init.cache).toBeUndefined();
158
+ expect(init.next).toBeUndefined();
159
+ });
160
+ it('leaves the fetch uncached when no cacheTags are given', async () => {
161
+ const fetchMock = mockFetch(() => ({
162
+ status: 200,
163
+ json: async () => ({
164
+ data: {}
165
+ })
166
+ }));
167
+ const fetchGraphql = createFontdueFetch({
168
+ url: 'https://acme.fontdue.com'
169
+ });
170
+ await fetchGraphql('Q', 'query Q { __typename }');
171
+ const init = fetchMock.mock.calls[0][1];
172
+ expect(init.cache).toBeUndefined();
173
+ expect(init.next).toBeUndefined();
174
+ });
175
+ it('treats an empty cacheTags list as uncached (preview renders)', async () => {
176
+ const fetchMock = mockFetch(() => ({
177
+ status: 200,
178
+ json: async () => ({
179
+ data: {}
180
+ })
181
+ }));
182
+ const fetchGraphql = createFontdueFetch({
183
+ url: 'https://acme.fontdue.com',
184
+ cacheTags: []
185
+ });
186
+ await fetchGraphql('Q', 'query Q { __typename }');
187
+ const init = fetchMock.mock.calls[0][1];
188
+ expect(init.cache).toBeUndefined();
189
+ expect(init.next).toBeUndefined();
190
+ });
191
+ });
192
+ describe('per-render config (the Next slot / ambient resolver)', () => {
193
+ it('resolves url, headers and cacheTags from the config', async () => {
194
+ var _init$next2;
195
+ vi.stubEnv('NODE_ENV', 'production');
196
+ const fetchMock = mockFetch(() => ({
197
+ status: 200,
198
+ json: async () => ({
199
+ data: {}
200
+ })
201
+ }));
202
+ withConfig({
203
+ url: 'https://tenant.fontdue.com',
204
+ headers: {
205
+ 'x-forwarded-host': 'tenant.fontdue.com'
206
+ },
207
+ cacheTags: ['graphql:tenant.fontdue.com']
208
+ });
209
+
210
+ // No options: a module-level fetcher picks up the current render's config.
211
+ const fetchGraphql = createFontdueFetch();
212
+ await fetchGraphql('Q', 'query Q { __typename }');
213
+ const [url, init] = fetchMock.mock.calls[0];
214
+ expect(url).toBe('https://tenant.fontdue.com/graphql?query=Q');
215
+ expect(init.headers['x-forwarded-host']).toBe('tenant.fontdue.com');
216
+ expect(init.cache).toBe('force-cache');
217
+ expect((_init$next2 = init.next) === null || _init$next2 === void 0 ? void 0 : _init$next2.tags).toEqual(['graphql', 'graphql:tenant.fontdue.com']);
218
+ });
219
+ it('explicit options override the config (url and cacheTags)', async () => {
220
+ const fetchMock = mockFetch(() => ({
221
+ status: 200,
222
+ json: async () => ({
223
+ data: {}
224
+ })
225
+ }));
226
+ withConfig({
227
+ url: 'https://tenant.fontdue.com',
228
+ cacheTags: ['graphql:tenant.fontdue.com']
229
+ });
230
+
231
+ // Explicit url + empty cacheTags (e.g. a preview render) win.
232
+ const fetchGraphql = createFontdueFetch({
233
+ url: 'https://explicit.fontdue.com',
234
+ cacheTags: []
235
+ });
236
+ await fetchGraphql('Q', 'query Q { __typename }');
237
+ const [url, init] = fetchMock.mock.calls[0];
238
+ expect(url).toBe('https://explicit.fontdue.com/graphql?query=Q');
239
+ expect(init.cache).toBeUndefined();
240
+ expect(init.next).toBeUndefined();
241
+ });
242
+ });
243
+ describe('fontdue-preview header', () => {
244
+ it('sends "false" by default so a public/session-only request never reveals hidden fonts', async () => {
245
+ const fetchMock = mockFetch(() => ({
246
+ status: 200,
247
+ json: async () => ({
248
+ data: {}
249
+ })
250
+ }));
251
+ const fetchGraphql = createFontdueFetch({
252
+ url: 'https://acme.fontdue.com'
253
+ });
254
+ await fetchGraphql('Q', 'query Q { __typename }');
255
+ const init = fetchMock.mock.calls[0][1];
256
+ expect(init.headers['fontdue-preview']).toBe('false');
257
+ });
258
+ it('sends "true" when bound with a preview Bearer token (reveal hidden fonts)', async () => {
259
+ const fetchMock = mockFetch(() => ({
260
+ status: 200,
261
+ json: async () => ({
262
+ data: {}
263
+ })
264
+ }));
265
+ const fetchGraphql = createFontdueFetch({
266
+ url: 'https://acme.fontdue.com',
267
+ headers: {
268
+ authorization: 'Bearer admin-tok'
269
+ }
270
+ });
271
+ await fetchGraphql('Q', 'query Q { __typename }');
272
+ const init = fetchMock.mock.calls[0][1];
273
+ expect(init.headers['fontdue-preview']).toBe('true');
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest';
2
+ import fontdueImageLoader from '../next/image-loader.js';
3
+
4
+ // The loader reads NEXT_PUBLIC_FONTDUE_IMAGE_HOST / _ORIGINS at call time, so
5
+ // each test stubs them and we clear stubs afterwards.
6
+ afterEach(() => {
7
+ vi.unstubAllEnvs();
8
+ });
9
+ function load(src) {
10
+ let origins = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
11
+ let host = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'img.fontdue.xyz';
12
+ vi.stubEnv('NEXT_PUBLIC_FONTDUE_IMAGE_HOST', host);
13
+ vi.stubEnv('NEXT_PUBLIC_FONTDUE_IMAGE_ORIGINS', origins);
14
+ return fontdueImageLoader({
15
+ src,
16
+ width: 800,
17
+ quality: 75
18
+ });
19
+ }
20
+ const transformed = src => `https://img.fontdue.xyz/cdn-cgi/image/width=800,quality=75,format=auto/${src}`;
21
+ describe('fontdueImageLoader', () => {
22
+ it('transforms any absolute src when no allowlist is set', () => {
23
+ const src = 'https://anywhere.example/a.jpg';
24
+ expect(load(src)).toBe(transformed(src));
25
+ });
26
+ it('serves relative srcs as-is', () => {
27
+ expect(load('/logo.svg', 'cdn.fontdue.xyz')).toBe('/logo.svg');
28
+ });
29
+ it('serves srcs as-is when no transform host is configured', () => {
30
+ const src = 'https://cdn.fontdue.xyz/a.jpg';
31
+ expect(load(src, 'cdn.fontdue.xyz', '')).toBe(src);
32
+ });
33
+ describe('origins allowlist', () => {
34
+ it('transforms an exact hostname match and skips a non-match', () => {
35
+ const ok = 'https://cdn.fontdue.xyz/a.jpg';
36
+ const no = 'https://other.example/a.jpg';
37
+ expect(load(ok, 'cdn.fontdue.xyz')).toBe(transformed(ok));
38
+ expect(load(no, 'cdn.fontdue.xyz')).toBe(no);
39
+ });
40
+ it('matches a "*." wildcard against any subdomain', () => {
41
+ const cdn = 'https://cdn.fontdue.xyz/a.jpg';
42
+ const assets = 'https://assets.fontdue.xyz/a.jpg';
43
+ expect(load(cdn, '*.fontdue.xyz')).toBe(transformed(cdn));
44
+ expect(load(assets, '*.fontdue.xyz')).toBe(transformed(assets));
45
+ });
46
+ it('does not let a wildcard match the apex or a lookalike domain', () => {
47
+ const apex = 'https://fontdue.xyz/a.jpg';
48
+ const lookalike = 'https://evilfontdue.xyz/a.jpg';
49
+ expect(load(apex, '*.fontdue.xyz')).toBe(apex);
50
+ expect(load(lookalike, '*.fontdue.xyz')).toBe(lookalike);
51
+ });
52
+ it('ignores an optional scheme on allowlist entries', () => {
53
+ const src = 'https://cdn.fontdue.xyz/a.jpg';
54
+ expect(load(src, 'https://cdn.fontdue.xyz')).toBe(transformed(src));
55
+ expect(load(src, 'https://*.fontdue.xyz')).toBe(transformed(src));
56
+ });
57
+ it('supports a comma-separated list', () => {
58
+ const src = 'https://assets.fontdue.xyz/a.jpg';
59
+ expect(load(src, 'cdn.fontdue.xyz, assets.fontdue.xyz')).toBe(transformed(src));
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { fallbackDescriptors, metricFallbackFamily } from "../metricFallback.js";
3
+ describe('fallbackDescriptors', () => {
4
+ it('matches average width via size-adjust and divides overrides by it', () => {
5
+ // Browser-validated (see the metric-fallback render test): at 1000px this
6
+ // renders ascent 800, descent 200, advance 600 — the target's exact box.
7
+ const d = fallbackDescriptors({
8
+ unitsPerEm: 1000,
9
+ ascender: 800,
10
+ descender: -200,
11
+ lineGap: 0,
12
+ avgCharWidth: 600
13
+ });
14
+ // size-adjust = (600/1000) / 0.458
15
+ expect(d.sizeAdjust).toBe(`${0.6 / 0.458 * 100}%`);
16
+ // ascent-override = (800/1000) / sizeAdjust -> effective ascent = 0.8em
17
+ expect(d.ascentOverride).toBe(`${0.8 / (0.6 / 0.458) * 100}%`);
18
+ expect(d.descentOverride).toBe(`${0.2 / (0.6 / 0.458) * 100}%`);
19
+ expect(d.lineGapOverride).toBe('0%');
20
+ expect(d.display).toBe('block');
21
+ });
22
+ it('scales correctly for a non-1000 unitsPerEm', () => {
23
+ const d = fallbackDescriptors({
24
+ unitsPerEm: 2048,
25
+ ascender: 1536,
26
+ descender: -512,
27
+ lineGap: 0,
28
+ avgCharWidth: 820
29
+ });
30
+ const s = 820 / 2048 / 0.458;
31
+ expect(d.sizeAdjust).toBe(`${s * 100}%`);
32
+ expect(d.ascentOverride).toBe(`${1536 / 2048 / s * 100}%`);
33
+ expect(d.descentOverride).toBe(`${512 / 2048 / s * 100}%`);
34
+ });
35
+ it('omits size-adjust and uses raw ratios when avgCharWidth is missing', () => {
36
+ const d = fallbackDescriptors({
37
+ unitsPerEm: 1000,
38
+ ascender: 750,
39
+ descender: -250,
40
+ lineGap: 0,
41
+ avgCharWidth: null
42
+ });
43
+ expect(d.sizeAdjust).toBeUndefined();
44
+ expect(d.ascentOverride).toBe('75%');
45
+ expect(d.descentOverride).toBe('25%');
46
+ });
47
+ it('treats a zero/absent avgCharWidth like no width match', () => {
48
+ const d = fallbackDescriptors({
49
+ unitsPerEm: 1000,
50
+ ascender: 900,
51
+ descender: -100,
52
+ lineGap: 200,
53
+ avgCharWidth: 0
54
+ });
55
+ expect(d.sizeAdjust).toBeUndefined();
56
+ expect(d.lineGapOverride).toBe('20%');
57
+ });
58
+ it('returns null for unusable metrics', () => {
59
+ expect(fallbackDescriptors(null)).toBeNull();
60
+ expect(fallbackDescriptors(undefined)).toBeNull();
61
+ expect(fallbackDescriptors({
62
+ unitsPerEm: 0,
63
+ ascender: 800,
64
+ descender: -200,
65
+ lineGap: 0,
66
+ avgCharWidth: 500
67
+ })).toBeNull();
68
+ });
69
+ });
70
+ describe('metricFallbackFamily', () => {
71
+ it('derives a per-family name', () => {
72
+ expect(metricFallbackFamily('Archivo Bold')).toBe('Archivo Bold fallback');
73
+ });
74
+ });
@@ -10,8 +10,9 @@ const request = {
10
10
  text: 'query TestQuery { viewer { id } }'
11
11
  };
12
12
  describe('createNetworkFetch (server)', () => {
13
- it('opts fetches into the Next data cache, tagged for revalidation', async () => {
13
+ it('opts fetches into the Next data cache, tagged for revalidation (production)', async () => {
14
14
  vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
15
+ vi.stubEnv('NODE_ENV', 'production');
15
16
  const fetchMock = vi.fn(async () => ({
16
17
  json: async () => ({
17
18
  data: {}
@@ -32,8 +33,28 @@ describe('createNetworkFetch (server)', () => {
32
33
  expect(options.next.tags).toContain('graphql');
33
34
  expect(options.next.tags).toContain('operation:TestQuery');
34
35
  });
36
+ it('skips the data cache in development so local dev is always fresh', async () => {
37
+ vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
38
+ vi.stubEnv('NODE_ENV', 'development');
39
+ const fetchMock = vi.fn(async () => ({
40
+ json: async () => ({
41
+ data: {}
42
+ })
43
+ }));
44
+ vi.stubGlobal('fetch', fetchMock);
45
+ const {
46
+ createNetworkFetch
47
+ } = await import("../relay/environment.js");
48
+ await createNetworkFetch()(request, {});
49
+ const [, options] = fetchMock.mock.calls[0];
50
+ // `next dev`'s data cache + revalidateTag don't reliably refresh, so dev
51
+ // fetches stay out of it (no-store) and every render fetches fresh.
52
+ expect(options.cache).toBe('no-store');
53
+ expect(options.next).toBeUndefined();
54
+ });
35
55
  it('applies the per-render server config tags and headers', async () => {
36
56
  vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
57
+ vi.stubEnv('NODE_ENV', 'production');
37
58
  const fetchMock = vi.fn(async () => ({
38
59
  json: async () => ({
39
60
  data: {}
@@ -42,10 +63,11 @@ describe('createNetworkFetch (server)', () => {
42
63
  vi.stubGlobal('fetch', fetchMock);
43
64
 
44
65
  // React.cache doesn't memoize outside a React render, so the store is
45
- // mocked rather than set through setFontdueServerConfig.
66
+ // mocked rather than set through setFontdueServerConfig. The network layer
67
+ // reads it through resolveFontdueServerConfig (awaited per fetch).
46
68
  vi.doMock('../relay/serverConfig', async importActual => ({
47
69
  ...(await importActual()),
48
- getFontdueServerConfig: () => ({
70
+ resolveFontdueServerConfig: async () => ({
49
71
  url: 'http://app:4000',
50
72
  headers: {
51
73
  'x-forwarded-host': 'acme.fontdue.com'
@@ -63,4 +85,104 @@ describe('createNetworkFetch (server)', () => {
63
85
  expect(options.headers['x-forwarded-host']).toBe('acme.fontdue.com');
64
86
  expect(options.next.tags).toContain('graphql:acme.fontdue.com');
65
87
  });
88
+ it('forwards per-call options.headers (e.g. a preview Bearer token)', async () => {
89
+ vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
90
+ const fetchMock = vi.fn(async () => ({
91
+ json: async () => ({
92
+ data: {}
93
+ })
94
+ }));
95
+ vi.stubGlobal('fetch', fetchMock);
96
+
97
+ // This is the non-RSC path: the render-scoped serverConfig store is a
98
+ // no-op outside an RSC render, so apps forward the token per call instead.
99
+ const {
100
+ createNetworkFetch
101
+ } = await import("../relay/environment.js");
102
+ await createNetworkFetch({
103
+ headers: {
104
+ authorization: 'Bearer preview-tok'
105
+ }
106
+ })(request, {});
107
+ const [, options] = fetchMock.mock.calls[0];
108
+ expect(options.headers.authorization).toBe('Bearer preview-tok');
109
+ });
110
+ });
111
+ function headersOf(fetchMock) {
112
+ return fetchMock.mock.calls[0][1].headers;
113
+ }
114
+ describe('createNetworkFetch (fontdue-preview header)', () => {
115
+ it('sends fontdue-preview: false on a public server fetch (no token)', async () => {
116
+ vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
117
+ const fetchMock = vi.fn(async () => ({
118
+ json: async () => ({
119
+ data: {}
120
+ })
121
+ }));
122
+ vi.stubGlobal('fetch', fetchMock);
123
+ const {
124
+ createNetworkFetch
125
+ } = await import("../relay/environment.js");
126
+ await createNetworkFetch()(request, {});
127
+ expect(headersOf(fetchMock)['fontdue-preview']).toBe('false');
128
+ });
129
+ it('sends fontdue-preview: true when a preview Bearer token is forwarded (server)', async () => {
130
+ vi.stubEnv('FONTDUE_URL', 'https://acme.fontdue.com');
131
+ const fetchMock = vi.fn(async () => ({
132
+ json: async () => ({
133
+ data: {}
134
+ })
135
+ }));
136
+ vi.stubGlobal('fetch', fetchMock);
137
+ const {
138
+ createNetworkFetch
139
+ } = await import("../relay/environment.js");
140
+ await createNetworkFetch({
141
+ headers: {
142
+ authorization: 'Bearer tok'
143
+ }
144
+ })(request, {});
145
+ expect(headersOf(fetchMock)['fontdue-preview']).toBe('true');
146
+ });
147
+ it('on the client, sends true only when the preview marker cookie is set', async () => {
148
+ // typeof window must be defined at module load for IS_SERVER to be false.
149
+ vi.stubGlobal('window', {
150
+ addEventListener: () => {}
151
+ });
152
+ vi.stubGlobal('document', {
153
+ cookie: 'fontdue_preview=1'
154
+ });
155
+ vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', 'https://acme.fontdue.com');
156
+ const fetchMock = vi.fn(async () => ({
157
+ json: async () => ({
158
+ data: {}
159
+ })
160
+ }));
161
+ vi.stubGlobal('fetch', fetchMock);
162
+ const {
163
+ createNetworkFetch
164
+ } = await import("../relay/environment.js");
165
+ await createNetworkFetch()(request, {});
166
+ expect(headersOf(fetchMock)['fontdue-preview']).toBe('true');
167
+ });
168
+ it('on the client, sends false when the marker cookie is absent (logged-in admin browsing normally)', async () => {
169
+ vi.stubGlobal('window', {
170
+ addEventListener: () => {}
171
+ });
172
+ vi.stubGlobal('document', {
173
+ cookie: 'other=1'
174
+ });
175
+ vi.stubEnv('NEXT_PUBLIC_FONTDUE_URL', 'https://acme.fontdue.com');
176
+ const fetchMock = vi.fn(async () => ({
177
+ json: async () => ({
178
+ data: {}
179
+ })
180
+ }));
181
+ vi.stubGlobal('fetch', fetchMock);
182
+ const {
183
+ createNetworkFetch
184
+ } = await import("../relay/environment.js");
185
+ await createNetworkFetch()(request, {});
186
+ expect(headersOf(fetchMock)['fontdue-preview']).toBe('false');
187
+ });
66
188
  });