@shopify/hydrogen 1.4.3 → 1.5.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 (28) hide show
  1. package/dist/esnext/components/CartLineProvider/tests/fixtures.d.ts +86 -0
  2. package/dist/esnext/components/CartLineProvider/tests/fixtures.js +34 -0
  3. package/dist/esnext/components/CartProvider/CartProviderV2.client.js +40 -10
  4. package/dist/esnext/components/CartProvider/tests/fixtures.d.ts +254 -0
  5. package/dist/esnext/components/CartProvider/tests/fixtures.js +53 -0
  6. package/dist/esnext/components/Metafield/Metafield.client.js +1 -3
  7. package/dist/esnext/foundation/Router/BrowserRouter.client.js +7 -1
  8. package/dist/esnext/framework/plugins/vite-plugin-css-rsc.d.ts +1 -1
  9. package/dist/esnext/framework/plugins/vite-plugin-css-rsc.js +100 -3
  10. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-rsc.js +2 -2
  11. package/dist/esnext/framework/plugins/vite-plugin-platform-entry.js +1 -1
  12. package/dist/esnext/storefront-api-types.d.ts +334 -116
  13. package/dist/esnext/storefront-api-types.js +3 -1
  14. package/dist/esnext/testing.d.ts +2 -0
  15. package/dist/esnext/testing.js +2 -0
  16. package/dist/esnext/utilities/tests/MockedServerRequestProvider.server.d.ts +6 -0
  17. package/dist/esnext/utilities/tests/MockedServerRequestProvider.server.js +9 -0
  18. package/dist/esnext/utilities/tests/price.d.ts +5 -0
  19. package/dist/esnext/utilities/tests/price.js +9 -0
  20. package/dist/esnext/utilities/tests/provider-helpers.d.ts +31 -0
  21. package/dist/esnext/utilities/tests/provider-helpers.js +36 -0
  22. package/dist/esnext/version.d.ts +1 -1
  23. package/dist/esnext/version.js +1 -1
  24. package/dist/node/framework/plugins/vite-plugin-css-rsc.d.ts +1 -1
  25. package/dist/node/framework/plugins/vite-plugin-css-rsc.js +99 -2
  26. package/dist/node/framework/plugins/vite-plugin-hydrogen-rsc.js +2 -2
  27. package/dist/node/framework/plugins/vite-plugin-platform-entry.js +1 -1
  28. package/package.json +3 -5
@@ -137,6 +137,8 @@ export var CheckoutErrorCode;
137
137
  CheckoutErrorCode["LineItemNotFound"] = "LINE_ITEM_NOT_FOUND";
138
138
  /** Checkout is locked. */
139
139
  CheckoutErrorCode["Locked"] = "LOCKED";
140
+ /** Maximum number of discount codes limit reached. */
141
+ CheckoutErrorCode["MaximumDiscountCodeLimitReached"] = "MAXIMUM_DISCOUNT_CODE_LIMIT_REACHED";
140
142
  /** Missing payment input. */
141
143
  CheckoutErrorCode["MissingPaymentInput"] = "MISSING_PAYMENT_INPUT";
142
144
  /** Not enough in stock. */
@@ -1119,7 +1121,7 @@ export var DiscountApplicationTargetType;
1119
1121
  * The type of data that the filter group represents.
1120
1122
  *
1121
1123
  * For more information, refer to [Filter products in a collection with the Storefront API]
1122
- * (https://shopify.dev/api/examples/filter-products).
1124
+ * (https://shopify.dev/custom-storefronts/products-collections/filter-products).
1123
1125
  *
1124
1126
  */
1125
1127
  export var FilterType;
@@ -0,0 +1,2 @@
1
+ export { MockedServerRequestProvider } from './utilities/tests/MockedServerRequestProvider.server.js';
2
+ export { ShopifyTestProviders } from './utilities/tests/provider-helpers.js';
@@ -0,0 +1,2 @@
1
+ export { MockedServerRequestProvider } from './utilities/tests/MockedServerRequestProvider.server.js';
2
+ export { ShopifyTestProviders } from './utilities/tests/provider-helpers.js';
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { ShopifyProviderOptions } from '../../utilities/tests/provider-helpers.js';
3
+ export declare function MockedServerRequestProvider({ children, setServerProps, serverProps, requestUrl, }: Omit<ShopifyProviderOptions, 'history'> & {
4
+ children: React.ReactElement;
5
+ requestUrl?: string;
6
+ }): JSX.Element;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { ServerRequestProvider } from '../../foundation/ServerRequestProvider/ServerRequestProvider.js';
3
+ import { ServerPropsProvider } from '../../foundation/ServerPropsProvider/ServerPropsProvider.js';
4
+ import { HydrogenRequest } from '../../foundation/HydrogenRequest/HydrogenRequest.server.js';
5
+ export function MockedServerRequestProvider({ children, setServerProps = () => { }, serverProps = { pathname: '', search: '' }, requestUrl = 'https://examples.com', }) {
6
+ const request = new HydrogenRequest(new Request(requestUrl));
7
+ return (React.createElement(ServerRequestProvider, { request: request },
8
+ React.createElement(ServerPropsProvider, { setServerPropsForRsc: setServerProps, initialServerProps: serverProps, setRscResponseFromApiRoute: () => { } }, children)));
9
+ }
@@ -0,0 +1,5 @@
1
+ import { CurrencyCode, MoneyV2 } from '../../storefront-api-types.js';
2
+ export declare function getPrice(price?: Partial<MoneyV2>): {
3
+ currencyCode: CurrencyCode;
4
+ amount: string;
5
+ };
@@ -0,0 +1,9 @@
1
+ // eslint-disable-next-line node/no-extraneous-import
2
+ import faker from 'faker';
3
+ import { CurrencyCode } from '../../storefront-api-types.js';
4
+ export function getPrice(price = {}) {
5
+ return {
6
+ currencyCode: price.currencyCode ?? CurrencyCode.Cad,
7
+ amount: price.amount ?? faker.finance.amount(),
8
+ };
9
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { BrowserHistory } from 'history';
3
+ import { ShopifyConfig } from '../../types.js';
4
+ import { LocationServerProps, ServerProps } from '../../foundation/ServerPropsProvider/ServerPropsProvider.js';
5
+ import { type CartWithActions, type Cart } from '../../components/CartProvider/types.js';
6
+ export declare function ShopifyTestProviders({ children, setServerProps, serverProps, shopifyConfig, history, }: ShopifyProviderOptions & {
7
+ children: React.ReactNode;
8
+ }): JSX.Element;
9
+ export declare function CartTestProviders({ children, cartProviderValues, }: {
10
+ children: React.ReactNode;
11
+ cartProviderValues?: Parameters<typeof getCartProviderValues>[0];
12
+ }): JSX.Element;
13
+ export declare function getShopifyConfig(config?: Partial<ShopifyConfig>): {
14
+ countryCode: string;
15
+ languageCode: string;
16
+ storeDomain: string;
17
+ storefrontToken: string;
18
+ storefrontApiVersion: string;
19
+ };
20
+ declare function getCartProviderValues({ cart, ...config }?: CartProviderOptions): CartWithActions;
21
+ declare type SetServerProps = React.Dispatch<React.SetStateAction<ServerProps>>;
22
+ export interface ShopifyProviderOptions {
23
+ shopifyConfig?: Partial<ShopifyConfig>;
24
+ setServerProps?: SetServerProps;
25
+ serverProps?: LocationServerProps;
26
+ history?: BrowserHistory;
27
+ }
28
+ declare type CartProviderOptions = {
29
+ cart?: Partial<Cart>;
30
+ } & Partial<CartWithActions>;
31
+ export {};
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { DEFAULT_COUNTRY, DEFAULT_LANGUAGE } from '../../foundation/constants.js';
3
+ import { ShopifyProvider } from '../../foundation/ShopifyProvider/ShopifyProvider.server.js';
4
+ import { BrowserRouter } from '../../foundation/Router/BrowserRouter.client.js';
5
+ import { ServerPropsProvider, } from '../../foundation/ServerPropsProvider/ServerPropsProvider.js';
6
+ import { CartContext } from '../../components/CartProvider/context.js';
7
+ import { CART_ACTIONS } from '../../components/CartProvider/tests/fixtures.js';
8
+ export function ShopifyTestProviders({ children, setServerProps = (() => { }), serverProps = { pathname: '', search: '' }, shopifyConfig, history, }) {
9
+ const finalShopifyConfig = getShopifyConfig(shopifyConfig);
10
+ return (React.createElement(ServerPropsProvider, { setServerPropsForRsc: setServerProps, initialServerProps: serverProps, setRscResponseFromApiRoute: () => { } },
11
+ React.createElement(ShopifyProvider, { shopifyConfig: finalShopifyConfig, languageCode: "EN", countryCode: "US" },
12
+ React.createElement(BrowserRouter, { history: history }, children))));
13
+ }
14
+ export function CartTestProviders({ children, cartProviderValues, }) {
15
+ const finalValue = getCartProviderValues(cartProviderValues);
16
+ return (React.createElement(CartContext.Provider, { value: finalValue }, children));
17
+ }
18
+ export function getShopifyConfig(config = {}) {
19
+ const languageCode = config.defaultLanguageCode ?? DEFAULT_LANGUAGE;
20
+ const countryCode = config.defaultCountryCode ?? DEFAULT_COUNTRY;
21
+ return {
22
+ countryCode: countryCode.toUpperCase(),
23
+ languageCode: languageCode.toUpperCase(),
24
+ storeDomain: config.storeDomain ?? 'notashop.myshopify.io',
25
+ storefrontToken: config.storefrontToken ?? 'abc123',
26
+ storefrontApiVersion: config.storefrontApiVersion ?? '2022-07',
27
+ };
28
+ }
29
+ function getCartProviderValues({ cart = {}, ...config } = {}) {
30
+ const finalConfig = {
31
+ ...CART_ACTIONS,
32
+ ...cart,
33
+ ...config,
34
+ };
35
+ return finalConfig;
36
+ }
@@ -1 +1 @@
1
- export declare const LIB_VERSION = "1.4.3";
1
+ export declare const LIB_VERSION = "1.5.0";
@@ -1 +1 @@
1
- export const LIB_VERSION = '1.4.3';
1
+ export const LIB_VERSION = '1.5.0';
@@ -1,2 +1,2 @@
1
- import { Plugin } from 'vite';
1
+ import { type Plugin } from 'vite';
2
2
  export default function cssRsc(): Plugin;
@@ -8,6 +8,31 @@ const magic_string_1 = __importDefault(require("magic-string"));
8
8
  const vite_1 = require("vite");
9
9
  const VITE_CSS_CHUNK_NAME = 'style.css';
10
10
  const INJECT_STYLES_COMMENT = '<!--__INJECT_STYLES__-->';
11
+ const CSS_EXTENSIONS_RE = /\.(css|sass|scss|stylus|less)(\.|\?|$)/;
12
+ const CSS_MODULES_EXTENSIONS_RE = /\.module\.(css|sass|scss|stylus|less)(\?|$)/;
13
+ const EVENT_CSS_IMPORT = 'hydrogen-css-modules-update-imports';
14
+ const EVENT_CSS_CLASSES = 'hydrogen-css-modules-update-classes';
15
+ const CSS_MODULES_HMR_INJECT = `
16
+ import {createHotContext, injectQuery} from "/@vite/client";
17
+
18
+ if (!import.meta.hot) {
19
+ import.meta.hot = createHotContext("/index.html");
20
+ }
21
+
22
+ import.meta.hot.on('${EVENT_CSS_IMPORT}', ({ids, timestamp}) => {
23
+ ids.forEach((id) => {
24
+ import(injectQuery(id, 't=' + timestamp));
25
+ });
26
+ });
27
+
28
+ import.meta.hot.on('${EVENT_CSS_CLASSES}', ({replacements}) => {
29
+ replacements.forEach(([oldClass, newClass]) => {
30
+ document.querySelectorAll('.' + oldClass).forEach(node => {
31
+ node.classList.replace(oldClass, newClass);
32
+ })
33
+ });
34
+ });
35
+ `;
11
36
  // Keep this in the outer scope to share it
12
37
  // across client <> server builds.
13
38
  let clientBuildPath;
@@ -21,6 +46,10 @@ let clientBuildPath;
21
46
  */
22
47
  function cssRsc() {
23
48
  let config;
49
+ let server;
50
+ let isUsingCssModules = false;
51
+ const hmrCssCopy = new Map();
52
+ const hmrCssQueue = new Set();
24
53
  return {
25
54
  name: 'hydrogen:css-rsc',
26
55
  enforce: 'post',
@@ -32,6 +61,9 @@ function cssRsc() {
32
61
  configResolved(_config) {
33
62
  config = _config;
34
63
  },
64
+ configureServer(_server) {
65
+ server = _server;
66
+ },
35
67
  transform(code, id, options) {
36
68
  if (options?.ssr && id.includes('index.html?raw')) {
37
69
  // Mark the client build index.html to inject styles later
@@ -42,17 +74,63 @@ function cssRsc() {
42
74
  map: s.generateMap({ file: id, source: id }),
43
75
  };
44
76
  }
77
+ // Manual HMR for CSS Modules
78
+ if (server && CSS_MODULES_EXTENSIONS_RE.test(id)) {
79
+ isUsingCssModules = true;
80
+ const file = id.split('?')[0];
81
+ // Note: this "CSS" file is actually JavaScript code.
82
+ // Get a copy of how this CSS was before the current update
83
+ const oldCode = hmrCssCopy.get(file);
84
+ // Save a copy of the current CSS for future updates
85
+ hmrCssCopy.set(file, code);
86
+ if (!oldCode || !hmrCssQueue.has(file))
87
+ return;
88
+ hmrCssQueue.delete(file);
89
+ // Diff old code with new code and use the exported class names as a reference
90
+ // to find out how the resulting CSS classes are renamed. With this, we can
91
+ // update classes in the DOM without requesting a full rendering from the server.
92
+ // Example:
93
+ // Previous code => export const red = ".red_k3tz4_module";
94
+ // New code => export const red = ".red_t93kw_module";
95
+ const classRE = /export const (.+?) = "(.+?)"/g;
96
+ const oldClasses = [...oldCode.matchAll(classRE)];
97
+ const replacements = [];
98
+ for (const [, newKey, newClass] of code.matchAll(classRE)) {
99
+ const oldClass = oldClasses.find(([, oldKey]) => oldKey === newKey)?.[2];
100
+ if (oldClass && oldClass !== newClass) {
101
+ replacements.push([oldClass, newClass]);
102
+ }
103
+ }
104
+ if (replacements.length > 0) {
105
+ // This event asks the browser to replace old
106
+ // hash-based CSS classes with new ones.
107
+ // Example: from `.red_k3tz4_module` to `.red_t93kw_module`
108
+ server.ws.send({
109
+ type: 'custom',
110
+ event: EVENT_CSS_CLASSES,
111
+ data: { replacements },
112
+ });
113
+ }
114
+ }
45
115
  },
46
116
  transformIndexHtml(html, { server }) {
47
117
  // Add discovered styles during dev
48
118
  if (server) {
49
- const tags = [];
119
+ const tags = (isUsingCssModules
120
+ ? [
121
+ {
122
+ tag: 'script',
123
+ attrs: { type: 'module' },
124
+ children: CSS_MODULES_HMR_INJECT,
125
+ },
126
+ ]
127
+ : []);
50
128
  const foundCssFiles = new Set();
51
129
  for (const [key, value] of server.moduleGraph.idToModuleMap.entries()) {
52
130
  if (
53
131
  // Note: Some CSS-in-JS libraries use `.css.js`
54
132
  // extension and we should match it here:
55
- /\.(css|sass|scss|stylus|less)(\.|\?|$)/.test((0, vite_1.normalizePath)(key).split('/').pop())) {
133
+ CSS_EXTENSIONS_RE.test((0, vite_1.normalizePath)(key).split('/').pop())) {
56
134
  let { url, file, lastHMRTimestamp, importers } = value;
57
135
  if (!foundCssFiles.has(file) &&
58
136
  !Array.from(importers).some((importer) => foundCssFiles.has(importer.file))) {
@@ -117,6 +195,25 @@ function cssRsc() {
117
195
  }
118
196
  }
119
197
  },
198
+ async handleHotUpdate({ modules, server }) {
199
+ if (modules.every((m) => CSS_MODULES_EXTENSIONS_RE.test(m.file || ''))) {
200
+ // Opt-out of Vite's default HMR for CSS Modules, we'll handle this manually
201
+ const file = modules[0].file;
202
+ hmrCssQueue.add(file);
203
+ // This event asks the browser to download fresh CSS files.
204
+ // Fetching these fresh CSS files will trigger another event
205
+ // from the `transform` hook to replace classes in the DOM.
206
+ server.ws.send({
207
+ type: 'custom',
208
+ event: EVENT_CSS_IMPORT,
209
+ data: {
210
+ ids: modules.map((m) => m.id),
211
+ timestamp: modules[0].lastHMRTimestamp || Date.now(),
212
+ },
213
+ });
214
+ return [];
215
+ }
216
+ },
120
217
  };
121
218
  }
122
219
  exports.default = cssRsc;
@@ -18,10 +18,10 @@ function default_1(options) {
18
18
  // Always allow the entry server (e.g. App.server.jsx) to be imported
19
19
  // in other files such as worker.js or server.js.
20
20
  source.includes(vite_plugin_hydrogen_middleware_js_1.HYDROGEN_DEFAULT_SERVER_ENTRY) ||
21
- /(index|entry-server|hydrogen\.config)\.[jt]s/.test(importer) ||
21
+ /(index|provider-helpers|entry-server|testing|hydrogen\.config)\.[jt]s/.test(importer) ||
22
22
  // Support importing server components for testing
23
23
  // TODO: revisit this when RSC splits into two bundles
24
- /\.test\.[tj]sx?$/.test(importer));
24
+ /\.(test|vitest|spec)\.[tj]sx?$/.test(importer));
25
25
  },
26
26
  ...options,
27
27
  });
@@ -54,7 +54,7 @@ exports.default = () => {
54
54
  async transform(code, id, options) {
55
55
  if (config.command === 'build' &&
56
56
  options?.ssr &&
57
- /@shopify\/hydrogen\/.+platforms\/virtual\./.test((0, vite_1.normalizePath)(id))) {
57
+ /\/hydrogen\/.+platforms\/virtual\./.test((0, vite_1.normalizePath)(id))) {
58
58
  const ms = new magic_string_1.default(code);
59
59
  ms.replace('__HYDROGEN_ENTRY__', vite_plugin_hydrogen_middleware_js_1.HYDROGEN_DEFAULT_SERVER_ENTRY);
60
60
  if (!clientBuildPath) {
package/package.json CHANGED
@@ -7,16 +7,14 @@
7
7
  "engines": {
8
8
  "node": ">=14"
9
9
  },
10
- "version": "1.4.3",
10
+ "version": "1.5.0",
11
11
  "description": "Modern custom Shopify storefronts",
12
12
  "license": "MIT",
13
13
  "main": "dist/esnext/index.js",
14
14
  "exports": {
15
15
  ".": "./dist/esnext/index.js",
16
- "./experimental": {
17
- "import": "./dist/esnext/experimental.js",
18
- "require": "./dist/node/experimental.js"
19
- },
16
+ "./experimental": "./dist/esnext/experimental.js",
17
+ "./testing": "./dist/esnext/testing.js",
20
18
  "./plugin": {
21
19
  "import": "./dist/esnext/framework/plugin.js",
22
20
  "require": "./dist/node/framework/plugin.js"