@shopify/cli-hydrogen 8.4.6 → 9.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,152 @@
1
1
  # skeleton
2
2
 
3
+ ## 2024.10.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Stabilize `getSitemap`, `getSitemapIndex` and implement on skeleton ([#2589](https://github.com/Shopify/hydrogen/pull/2589)) by [@juanpprieto](https://github.com/juanpprieto)
8
+
9
+ 1. Update the `getSitemapIndex` at `/app/routes/[sitemap.xml].tsx`
10
+
11
+ ```diff
12
+ - import {unstable__getSitemapIndex as getSitemapIndex} from '@shopify/hydrogen';
13
+ + import {getSitemapIndex} from '@shopify/hydrogen';
14
+ ```
15
+
16
+ 2. Update the `getSitemap` at `/app/routes/sitemap.$type.$page[.xml].tsx`
17
+
18
+ ```diff
19
+ - import {unstable__getSitemap as getSitemap} from '@shopify/hydrogen';
20
+ + import {getSitemap} from '@shopify/hydrogen';
21
+ ```
22
+
23
+ For a reference implementation please see the skeleton template sitemap routes
24
+
25
+ - [**Breaking change**] ([#2588](https://github.com/Shopify/hydrogen/pull/2588)) by [@wizardlyhel](https://github.com/wizardlyhel)
26
+
27
+ Set up Customer Privacy without the Shopify's cookie banner by default.
28
+
29
+ If you are using Shopify's cookie banner to handle user consent in your app, you need to set `withPrivacyBanner: true` to the consent config. Without this update, the Shopify cookie banner will not appear.
30
+
31
+ ```diff
32
+ return defer({
33
+ ...
34
+ consent: {
35
+ checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN,
36
+ storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
37
+ + withPrivacyBanner: true,
38
+ // localize the privacy banner
39
+ country: args.context.storefront.i18n.country,
40
+ language: args.context.storefront.i18n.language,
41
+ },
42
+ });
43
+ ```
44
+
45
+ - Update to 2024-10 SFAPI ([#2570](https://github.com/Shopify/hydrogen/pull/2570)) by [@wizardlyhel](https://github.com/wizardlyhel)
46
+
47
+ - [**Breaking change**] ([#2546](https://github.com/Shopify/hydrogen/pull/2546)) by [@frandiox](https://github.com/frandiox)
48
+
49
+ Update `createWithCache` to make it harder to accidentally cache undesired results. `request` is now mandatory prop when initializing `createWithCache`.
50
+
51
+ ```diff
52
+ // server.ts
53
+ export default {
54
+ async fetch(
55
+ request: Request,
56
+ env: Env,
57
+ executionContext: ExecutionContext,
58
+ ): Promise<Response> {
59
+ try {
60
+ // ...
61
+ - const withCache = createWithCache({cache, waitUntil});
62
+ + const withCache = createWithCache({cache, waitUntil, request});
63
+ ```
64
+
65
+ `createWithCache` now returns an object with two utility functions: `withCache.run` and `withCache.fetch`. Both have a new prop `shouldCacheResult` that must be defined.
66
+
67
+ The original `withCache` callback function is now `withCache.run`. This is useful to run _multiple_ fetch calls and merge their responses, or run any arbitrary code. It caches anything you return, but you can throw if you don't want to cache anything.
68
+
69
+ ```diff
70
+ const withCache = createWithCache({cache, waitUntil, request});
71
+
72
+ const fetchMyCMS = (query) => {
73
+ - return withCache(['my-cms', query], CacheLong(), async (params) => {
74
+ + return withCache.run({
75
+ + cacheKey: ['my-cms', query],
76
+ + cacheStrategy: CacheLong(),
77
+ + // Cache if there are no data errors or a specific data that make this result not suited for caching
78
+ + shouldCacheResult: (result) => !result?.errors,
79
+ + }, async(params) => {
80
+ const response = await fetch('my-cms.com/api', {
81
+ method: 'POST',
82
+ body: query,
83
+ });
84
+ if (!response.ok) throw new Error(response.statusText);
85
+ const {data, error} = await response.json();
86
+ if (error || !data) throw new Error(error ?? 'Missing data');
87
+ params.addDebugData({displayName: 'My CMS query', response});
88
+ return data;
89
+ });
90
+ };
91
+ ```
92
+
93
+ New `withCache.fetch` is for caching simple fetch requests. This method caches the responses if they are OK responses, and you can pass `shouldCacheResponse`, `cacheKey`, etc. to modify behavior. `data` is the consumed body of the response (we need to consume to cache it).
94
+
95
+ ```ts
96
+ const withCache = createWithCache({cache, waitUntil, request});
97
+
98
+ const {data, response} = await withCache.fetch<{data: T; error: string}>(
99
+ 'my-cms.com/api',
100
+ {
101
+ method: 'POST',
102
+ headers: {'Content-type': 'application/json'},
103
+ body,
104
+ },
105
+ {
106
+ cacheStrategy: CacheLong(),
107
+ // Cache if there are no data errors or a specific data that make this result not suited for caching
108
+ shouldCacheResponse: (result) => !result?.error,
109
+ cacheKey: ['my-cms', body],
110
+ displayName: 'My CMS query',
111
+ },
112
+ );
113
+ ```
114
+
115
+ - [**Breaking change**] ([#2585](https://github.com/Shopify/hydrogen/pull/2585)) by [@wizardlyhel](https://github.com/wizardlyhel)
116
+
117
+ Deprecate usages of `product.options.values` and use `product.options.optionValues` instead.
118
+
119
+ 1. Update your product graphql query to use the new `optionValues` field.
120
+
121
+ ```diff
122
+ const PRODUCT_FRAGMENT = `#graphql
123
+ fragment Product on Product {
124
+ id
125
+ title
126
+ options {
127
+ name
128
+ - values
129
+ + optionValues {
130
+ + name
131
+ + }
132
+ }
133
+ ```
134
+
135
+ 2. Update your `<VariantSelector>` to use the new `optionValues` field.
136
+
137
+ ```diff
138
+ <VariantSelector
139
+ handle={product.handle}
140
+ - options={product.options.filter((option) => option.values.length > 1)}
141
+ + options={product.options.filter((option) => option.optionValues.length > 1)}
142
+ variants={variants}
143
+ >
144
+ ```
145
+
146
+ - Updated dependencies [[`d97cd56e`](https://github.com/Shopify/hydrogen/commit/d97cd56e859abf8dd005fef2589d99e07fa87b6e), [`809c9f3d`](https://github.com/Shopify/hydrogen/commit/809c9f3d342b56dd3c0d340cb733e6f00053b71d), [`8c89f298`](https://github.com/Shopify/hydrogen/commit/8c89f298a8d9084ee510fb4d0d17766ec43c249c), [`a253ef97`](https://github.com/Shopify/hydrogen/commit/a253ef971acb08f2ee3a2743ca5c901c2922acc0), [`84a66b1e`](https://github.com/Shopify/hydrogen/commit/84a66b1e9d07bd6d6a10e5379ad3350b6bbecde9), [`227035e7`](https://github.com/Shopify/hydrogen/commit/227035e7e11df5fec5ac475b98fa6a318bdbe366), [`ac12293c`](https://github.com/Shopify/hydrogen/commit/ac12293c7b36e1b278bc929c682c65779c300cc7), [`c7c9f2eb`](https://github.com/Shopify/hydrogen/commit/c7c9f2ebd869a9d361504a10566c268e88b6096a), [`76cd4f9b`](https://github.com/Shopify/hydrogen/commit/76cd4f9ba3dd8eff4433d72f4422c06a7d567537), [`8337e534`](https://github.com/Shopify/hydrogen/commit/8337e5342ecca563fab557c3e833485466456cd5)]:
147
+ - @shopify/hydrogen@2024.10.0
148
+ - @shopify/remix-oxygen@2.0.9
149
+
3
150
  ## 2024.7.10
4
151
 
5
152
  ### Patch Changes
@@ -21,7 +21,7 @@ export function ProductForm({
21
21
  <div className="product-form">
22
22
  <VariantSelector
23
23
  handle={product.handle}
24
- options={product.options.filter((option) => option.values.length > 1)}
24
+ options={product.options.filter((option) => option.optionValues.length > 1)}
25
25
  variants={variants}
26
26
  >
27
27
  {({option}) => <ProductOptions key={option.name} option={option} />}
@@ -73,7 +73,7 @@ export async function loader(args: LoaderFunctionArgs) {
73
73
  consent: {
74
74
  checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN,
75
75
  storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
76
- withPrivacyBanner: true,
76
+ withPrivacyBanner: false,
77
77
  // localize the privacy banner
78
78
  country: args.context.storefront.i18n.country,
79
79
  language: args.context.storefront.i18n.language,
@@ -1,177 +1,17 @@
1
- import {flattenConnection} from '@shopify/hydrogen';
2
1
  import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
3
- import type {SitemapQuery} from 'storefrontapi.generated';
4
-
5
- /**
6
- * the google limit is 50K, however, the storefront API
7
- * allows querying only 250 resources per pagination page
8
- */
9
- const MAX_URLS = 250;
10
-
11
- type Entry = {
12
- url: string;
13
- lastMod?: string;
14
- changeFreq?: string;
15
- image?: {
16
- url: string;
17
- title?: string;
18
- caption?: string;
19
- };
20
- };
2
+ import {getSitemapIndex} from '@shopify/hydrogen';
21
3
 
22
4
  export async function loader({
23
5
  request,
24
6
  context: {storefront},
25
7
  }: LoaderFunctionArgs) {
26
- const data = await storefront.query(SITEMAP_QUERY, {
27
- variables: {
28
- urlLimits: MAX_URLS,
29
- language: storefront.i18n.language,
30
- },
31
- });
32
-
33
- if (!data) {
34
- throw new Response('No data found', {status: 404});
35
- }
36
-
37
- const sitemap = generateSitemap({data, baseUrl: new URL(request.url).origin});
38
-
39
- return new Response(sitemap, {
40
- headers: {
41
- 'Content-Type': 'application/xml',
42
-
43
- 'Cache-Control': `max-age=${60 * 60 * 24}`,
44
- },
8
+ const response = await getSitemapIndex({
9
+ storefront,
10
+ request,
45
11
  });
46
- }
47
-
48
- function xmlEncode(string: string) {
49
- return string.replace(/[&<>'"]/g, (char) => `&#${char.charCodeAt(0)};`);
50
- }
51
-
52
- function generateSitemap({
53
- data,
54
- baseUrl,
55
- }: {
56
- data: SitemapQuery;
57
- baseUrl: string;
58
- }) {
59
- const products = flattenConnection(data.products)
60
- .filter((product) => product.onlineStoreUrl)
61
- .map((product) => {
62
- const url = `${baseUrl}/products/${xmlEncode(product.handle)}`;
63
-
64
- const productEntry: Entry = {
65
- url,
66
- lastMod: product.updatedAt,
67
- changeFreq: 'daily',
68
- };
69
-
70
- if (product.featuredImage?.url) {
71
- productEntry.image = {
72
- url: xmlEncode(product.featuredImage.url),
73
- };
74
-
75
- if (product.title) {
76
- productEntry.image.title = xmlEncode(product.title);
77
- }
78
-
79
- if (product.featuredImage.altText) {
80
- productEntry.image.caption = xmlEncode(product.featuredImage.altText);
81
- }
82
- }
83
-
84
- return productEntry;
85
- });
86
-
87
- const collections = flattenConnection(data.collections)
88
- .filter((collection) => collection.onlineStoreUrl)
89
- .map((collection) => {
90
- const url = `${baseUrl}/collections/${collection.handle}`;
91
-
92
- return {
93
- url,
94
- lastMod: collection.updatedAt,
95
- changeFreq: 'daily',
96
- };
97
- });
98
-
99
- const pages = flattenConnection(data.pages)
100
- .filter((page) => page.onlineStoreUrl)
101
- .map((page) => {
102
- const url = `${baseUrl}/pages/${page.handle}`;
103
-
104
- return {
105
- url,
106
- lastMod: page.updatedAt,
107
- changeFreq: 'weekly',
108
- };
109
- });
110
-
111
- const urls = [...products, ...collections, ...pages];
112
-
113
- return `
114
- <urlset
115
- xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
116
- xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
117
- >
118
- ${urls.map(renderUrlTag).join('')}
119
- </urlset>`;
120
- }
121
12
 
122
- function renderUrlTag({url, lastMod, changeFreq, image}: Entry) {
123
- const imageTag = image
124
- ? `<image:image>
125
- <image:loc>${image.url}</image:loc>
126
- <image:title>${image.title ?? ''}</image:title>
127
- <image:caption>${image.caption ?? ''}</image:caption>
128
- </image:image>`.trim()
129
- : '';
13
+ response.headers.set('Cache-Control', `max-age=${60 * 60 * 24}`);
130
14
 
131
- return `
132
- <url>
133
- <loc>${url}</loc>
134
- <lastmod>${lastMod}</lastmod>
135
- <changefreq>${changeFreq}</changefreq>
136
- ${imageTag}
137
- </url>
138
- `.trim();
15
+ return response;
139
16
  }
140
17
 
141
- const SITEMAP_QUERY = `#graphql
142
- query Sitemap($urlLimits: Int, $language: LanguageCode)
143
- @inContext(language: $language) {
144
- products(
145
- first: $urlLimits
146
- query: "published_status:'online_store:visible'"
147
- ) {
148
- nodes {
149
- updatedAt
150
- handle
151
- onlineStoreUrl
152
- title
153
- featuredImage {
154
- url
155
- altText
156
- }
157
- }
158
- }
159
- collections(
160
- first: $urlLimits
161
- query: "published_status:'online_store:visible'"
162
- ) {
163
- nodes {
164
- updatedAt
165
- handle
166
- onlineStoreUrl
167
- }
168
- }
169
- pages(first: $urlLimits, query: "published_status:'published'") {
170
- nodes {
171
- updatedAt
172
- handle
173
- onlineStoreUrl
174
- }
175
- }
176
- }
177
- ` as const;
@@ -74,7 +74,7 @@ export async function action({request, context}: ActionFunctionArgs) {
74
74
 
75
75
  const cartId = result?.cart?.id;
76
76
  const headers = cartId ? cart.setCartId(result.cart.id) : new Headers();
77
- const {cart: cartResult, errors} = result;
77
+ const {cart: cartResult, errors, warnings} = result;
78
78
 
79
79
  const redirectTo = formData.get('redirectTo') ?? null;
80
80
  if (typeof redirectTo === 'string') {
@@ -86,6 +86,7 @@ export async function action({request, context}: ActionFunctionArgs) {
86
86
  {
87
87
  cart: cartResult,
88
88
  errors,
89
+ warnings,
89
90
  analytics: {
90
91
  cartId,
91
92
  },
@@ -242,7 +242,9 @@ const PRODUCT_FRAGMENT = `#graphql
242
242
  description
243
243
  options {
244
244
  name
245
- values
245
+ optionValues {
246
+ name
247
+ }
246
248
  }
247
249
  selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
248
250
  ...ProductVariant
@@ -0,0 +1,24 @@
1
+ import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import {getSitemap} from '@shopify/hydrogen';
3
+
4
+ export async function loader({
5
+ request,
6
+ params,
7
+ context: {storefront},
8
+ }: LoaderFunctionArgs) {
9
+ const response = await getSitemap({
10
+ storefront,
11
+ request,
12
+ params,
13
+ locales: ['EN-US', 'EN-CA', 'FR-CA'],
14
+ getLink: ({type, baseUrl, handle, locale}) => {
15
+ if (!locale) return `${baseUrl}/${type}/${handle}`;
16
+ return `${baseUrl}/${locale}/${type}/${handle}`;
17
+ },
18
+ });
19
+
20
+ response.headers.set('Cache-Control', `max-age=${60 * 60 * 24}`);
21
+
22
+ return response;
23
+ }
24
+
@@ -2,7 +2,7 @@
2
2
  "name": "skeleton",
3
3
  "private": true,
4
4
  "sideEffects": false,
5
- "version": "2024.7.10",
5
+ "version": "2024.10.0",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "shopify hydrogen build --codegen",
@@ -14,10 +14,10 @@
14
14
  },
15
15
  "prettier": "@shopify/prettier-config",
16
16
  "dependencies": {
17
- "@remix-run/react": "^2.10.1",
18
- "@remix-run/server-runtime": "^2.10.1",
19
- "@shopify/hydrogen": "2024.7.9",
20
- "@shopify/remix-oxygen": "^2.0.8",
17
+ "@remix-run/react": "^2.13.1",
18
+ "@remix-run/server-runtime": "^2.13.1",
19
+ "@shopify/hydrogen": "2024.10.0",
20
+ "@shopify/remix-oxygen": "^2.0.9",
21
21
  "graphql": "^16.6.0",
22
22
  "graphql-tag": "^2.12.6",
23
23
  "isbot": "^3.8.0",
@@ -26,11 +26,11 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@graphql-codegen/cli": "5.0.2",
29
- "@remix-run/dev": "^2.10.1",
30
- "@remix-run/eslint-config": "^2.10.1",
31
- "@shopify/cli": "~3.68.0",
32
- "@shopify/hydrogen-codegen": "^0.3.1",
33
- "@shopify/mini-oxygen": "^3.0.5",
29
+ "@remix-run/dev": "^2.13.1",
30
+ "@remix-run/eslint-config": "^2.13.1",
31
+ "@shopify/cli": "~3.69.3",
32
+ "@shopify/hydrogen-codegen": "^0.3.2",
33
+ "@shopify/mini-oxygen": "^3.1.0",
34
34
  "@shopify/oxygen-workers-types": "^4.1.2",
35
35
  "@shopify/prettier-config": "^1.1.2",
36
36
  "@total-typescript/ts-reset": "^0.4.2",
@@ -295,36 +295,6 @@ export type StoreRobotsQueryVariables = StorefrontAPI.Exact<{
295
295
 
296
296
  export type StoreRobotsQuery = {shop: Pick<StorefrontAPI.Shop, 'id'>};
297
297
 
298
- export type SitemapQueryVariables = StorefrontAPI.Exact<{
299
- urlLimits?: StorefrontAPI.InputMaybe<StorefrontAPI.Scalars['Int']['input']>;
300
- language?: StorefrontAPI.InputMaybe<StorefrontAPI.LanguageCode>;
301
- }>;
302
-
303
- export type SitemapQuery = {
304
- products: {
305
- nodes: Array<
306
- Pick<
307
- StorefrontAPI.Product,
308
- 'updatedAt' | 'handle' | 'onlineStoreUrl' | 'title'
309
- > & {
310
- featuredImage?: StorefrontAPI.Maybe<
311
- Pick<StorefrontAPI.Image, 'url' | 'altText'>
312
- >;
313
- }
314
- >;
315
- };
316
- collections: {
317
- nodes: Array<
318
- Pick<StorefrontAPI.Collection, 'updatedAt' | 'handle' | 'onlineStoreUrl'>
319
- >;
320
- };
321
- pages: {
322
- nodes: Array<
323
- Pick<StorefrontAPI.Page, 'updatedAt' | 'handle' | 'onlineStoreUrl'>
324
- >;
325
- };
326
- };
327
-
328
298
  export type FeaturedCollectionFragment = Pick<
329
299
  StorefrontAPI.Collection,
330
300
  'id' | 'title' | 'handle'
@@ -782,7 +752,11 @@ export type ProductFragment = Pick<
782
752
  StorefrontAPI.Product,
783
753
  'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description'
784
754
  > & {
785
- options: Array<Pick<StorefrontAPI.ProductOption, 'name' | 'values'>>;
755
+ options: Array<
756
+ Pick<StorefrontAPI.ProductOption, 'name'> & {
757
+ optionValues: Array<Pick<StorefrontAPI.ProductOptionValue, 'name'>>;
758
+ }
759
+ >;
786
760
  selectedVariant?: StorefrontAPI.Maybe<
787
761
  Pick<
788
762
  StorefrontAPI.ProductVariant,
@@ -851,7 +825,11 @@ export type ProductQuery = {
851
825
  StorefrontAPI.Product,
852
826
  'id' | 'title' | 'vendor' | 'handle' | 'descriptionHtml' | 'description'
853
827
  > & {
854
- options: Array<Pick<StorefrontAPI.ProductOption, 'name' | 'values'>>;
828
+ options: Array<
829
+ Pick<StorefrontAPI.ProductOption, 'name'> & {
830
+ optionValues: Array<Pick<StorefrontAPI.ProductOptionValue, 'name'>>;
831
+ }
832
+ >;
855
833
  selectedVariant?: StorefrontAPI.Maybe<
856
834
  Pick<
857
835
  StorefrontAPI.ProductVariant,
@@ -1212,10 +1190,6 @@ interface GeneratedQueryTypes {
1212
1190
  return: StoreRobotsQuery;
1213
1191
  variables: StoreRobotsQueryVariables;
1214
1192
  };
1215
- '#graphql\n query Sitemap($urlLimits: Int, $language: LanguageCode)\n @inContext(language: $language) {\n products(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n title\n featuredImage {\n url\n altText\n }\n }\n }\n collections(\n first: $urlLimits\n query: "published_status:\'online_store:visible\'"\n ) {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n pages(first: $urlLimits, query: "published_status:\'published\'") {\n nodes {\n updatedAt\n handle\n onlineStoreUrl\n }\n }\n }\n': {
1216
- return: SitemapQuery;
1217
- variables: SitemapQueryVariables;
1218
- };
1219
1193
  '#graphql\n fragment FeaturedCollection on Collection {\n id\n title\n image {\n id\n url\n altText\n width\n height\n }\n handle\n }\n query FeaturedCollection($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n collections(first: 1, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...FeaturedCollection\n }\n }\n }\n': {
1220
1194
  return: FeaturedCollectionQuery;
1221
1195
  variables: FeaturedCollectionQueryVariables;
@@ -1260,7 +1234,7 @@ interface GeneratedQueryTypes {
1260
1234
  return: PoliciesQuery;
1261
1235
  variables: PoliciesQueryVariables;
1262
1236
  };
1263
- '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n options {\n name\n values\n }\n selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n variants(first: 1) {\n nodes {\n ...ProductVariant\n }\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': {
1237
+ '#graphql\n query Product(\n $country: CountryCode\n $handle: String!\n $language: LanguageCode\n $selectedOptions: [SelectedOptionInput!]!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...Product\n }\n }\n #graphql\n fragment Product on Product {\n id\n title\n vendor\n handle\n descriptionHtml\n description\n options {\n name\n optionValues {\n name\n }\n }\n selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {\n ...ProductVariant\n }\n variants(first: 1) {\n nodes {\n ...ProductVariant\n }\n }\n seo {\n description\n title\n }\n }\n #graphql\n fragment ProductVariant on ProductVariant {\n availableForSale\n compareAtPrice {\n amount\n currencyCode\n }\n id\n image {\n __typename\n id\n url\n altText\n width\n height\n }\n price {\n amount\n currencyCode\n }\n product {\n title\n handle\n }\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': {
1264
1238
  return: ProductQuery;
1265
1239
  variables: ProductQueryVariables;
1266
1240
  };
@@ -8,7 +8,7 @@ import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
8
8
  import { prepareDiffDirectory } from '../../lib/template-diff.js';
9
9
  import { isViteProject, getViteConfig } from '../../lib/vite-config.js';
10
10
  import { checkLockfileStatus } from '../../lib/check-lockfile.js';
11
- import { findMissingRoutes } from '../../lib/missing-routes.js';
11
+ import { findMissingRoutes, warnReservedRoutes, findReservedRoutes } from '../../lib/route-validator.js';
12
12
  import { runClassicCompilerBuild } from '../../lib/classic-compiler/build.js';
13
13
  import { hydrogenBundleAnalyzer } from '../../lib/bundle/vite-plugin.js';
14
14
  import { BUNDLE_ANALYZER_HTML_FILE, getBundleAnalysisSummary } from '../../lib/bundle/analyzer.js';
@@ -242,6 +242,9 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
242
242
  );
243
243
  }
244
244
  }
245
+ if (!watch && !disableRouteWarning) {
246
+ warnReservedRoutes(findReservedRoutes(remixConfig));
247
+ }
245
248
  return {
246
249
  async close() {
247
250
  codegenProcess?.removeAllListeners("close");
@@ -2,7 +2,7 @@ import Command from '@shopify/cli-kit/node/base-command';
2
2
  import { resolvePath } from '@shopify/cli-kit/node/path';
3
3
  import { commonFlags } from '../../lib/flags.js';
4
4
  import { getRemixConfig } from '../../lib/remix-config.js';
5
- import { logMissingRoutes, findMissingRoutes } from '../../lib/missing-routes.js';
5
+ import { logMissingRoutes, findMissingRoutes, warnReservedRoutes, findReservedRoutes } from '../../lib/route-validator.js';
6
6
  import { Args } from '@oclif/core';
7
7
 
8
8
  class GenerateRoute extends Command {
@@ -32,6 +32,7 @@ class GenerateRoute extends Command {
32
32
  async function runCheckRoutes({ directory }) {
33
33
  const remixConfig = await getRemixConfig(directory);
34
34
  logMissingRoutes(findMissingRoutes(remixConfig));
35
+ warnReservedRoutes(findReservedRoutes(remixConfig));
35
36
  }
36
37
 
37
38
  export { GenerateRoute as default, runCheckRoutes };
@@ -42,7 +42,7 @@ class Deploy extends Command {
42
42
  }
43
43
  }),
44
44
  preview: Flags.boolean({
45
- description: "Deploys to the Preview environment. Overrides --env-branch and Git metadata.",
45
+ description: "Deploys to the Preview environment.",
46
46
  required: false,
47
47
  default: false
48
48
  }),
@@ -7,7 +7,7 @@ import { renderSuccess, renderInfo } from '@shopify/cli-kit/node/ui';
7
7
  import { AbortError } from '@shopify/cli-kit/node/error';
8
8
  import { removeFile } from '@shopify/cli-kit/node/fs';
9
9
  import { setH2OVerbose, isH2Verbose, muteDevLogs, enhanceH2Logs } from '../../lib/log.js';
10
- import { commonFlags, overrideFlag, deprecated, flagsToCamelObject, DEFAULT_INSPECTOR_PORT, DEFAULT_APP_PORT } from '../../lib/flags.js';
10
+ import { commonFlags, overrideFlag, flagsToCamelObject, DEFAULT_INSPECTOR_PORT, DEFAULT_APP_PORT } from '../../lib/flags.js';
11
11
  import { spawnCodegenProcess } from '../../lib/codegen.js';
12
12
  import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
13
13
  import { displayDevUpgradeNotice } from './upgrade.js';
@@ -65,7 +65,6 @@ class Dev extends Command {
65
65
  default: false
66
66
  }),
67
67
  // For the classic compiler:
68
- worker: deprecated("--worker", { isBoolean: true }),
69
68
  ...overrideFlag(commonFlags.legacyRuntime, {
70
69
  "legacy-runtime": {
71
70
  description: "[Classic Remix Compiler] " + commonFlags.legacyRuntime["legacy-runtime"].description
@@ -5,7 +5,7 @@ import { outputInfo } from '@shopify/cli-kit/node/output';
5
5
  import { resolvePath, joinPath } from '@shopify/cli-kit/node/path';
6
6
  import { setH2OVerbose, isH2Verbose, muteDevLogs } from '../../lib/log.js';
7
7
  import { getProjectPaths, isClassicProject } from '../../lib/remix-config.js';
8
- import { commonFlags, deprecated, overrideFlag, flagsToCamelObject, DEFAULT_APP_PORT } from '../../lib/flags.js';
8
+ import { commonFlags, overrideFlag, flagsToCamelObject, DEFAULT_APP_PORT } from '../../lib/flags.js';
9
9
  import { startMiniOxygen } from '../../lib/mini-oxygen/index.js';
10
10
  import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
11
11
  import { getConfig } from '../../lib/shopify-config.js';
@@ -23,7 +23,6 @@ class Preview extends Command {
23
23
  static flags = {
24
24
  ...commonFlags.path,
25
25
  ...commonFlags.port,
26
- worker: deprecated("--worker", { isBoolean: true }),
27
26
  ...commonFlags.legacyRuntime,
28
27
  ...commonFlags.env,
29
28
  ...commonFlags.envBranch,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as _oclif_core from '@oclif/core';
2
2
  import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
3
3
  import Command from '@shopify/cli-kit/node/base-command';
4
- import * as _oclif_core_lib_interfaces_alphabet_js from '@oclif/core/lib/interfaces/alphabet.js';
5
4
  import Init from './commands/hydrogen/init.js';
6
5
  import '@shopify/cli-kit/node/node-package-manager';
7
6
 
@@ -89,37 +88,6 @@ declare class Dev extends Command {
89
88
  'legacy-runtime': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
90
89
  host: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
91
90
  'disable-deps-optimizer': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
92
- worker: {
93
- type: "option";
94
- name: string;
95
- char?: _oclif_core_lib_interfaces_alphabet_js.AlphabetLowercase | _oclif_core_lib_interfaces_alphabet_js.AlphabetUppercase;
96
- summary?: string;
97
- description?: string;
98
- helpLabel?: string;
99
- helpGroup?: string;
100
- env?: string;
101
- hidden?: boolean;
102
- required?: boolean;
103
- dependsOn?: string[];
104
- exclusive?: string[];
105
- exactlyOne?: string[];
106
- relationships?: _oclif_core_lib_interfaces_parser_js.Relationship[];
107
- deprecated?: true | _oclif_core_lib_interfaces_parser_js.Deprecation;
108
- aliases?: string[];
109
- charAliases?: (_oclif_core_lib_interfaces_alphabet_js.AlphabetLowercase | _oclif_core_lib_interfaces_alphabet_js.AlphabetUppercase)[];
110
- deprecateAliases?: boolean;
111
- noCacheDefault?: boolean;
112
- helpValue?: string;
113
- options?: readonly string[];
114
- multiple?: boolean;
115
- multipleNonGreedy?: boolean;
116
- delimiter?: ",";
117
- allowStdin?: boolean | "only";
118
- parse: _oclif_core_lib_interfaces_parser_js.FlagParser<unknown, string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
119
- defaultHelp?: unknown;
120
- input: string[];
121
- default?: unknown;
122
- };
123
91
  verbose: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
124
92
  'customer-account-push__unstable': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
125
93
  diff: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
@@ -264,37 +232,6 @@ declare class Preview extends Command {
264
232
  'env-branch': _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
265
233
  env: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
266
234
  'legacy-runtime': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
267
- worker: {
268
- type: "option";
269
- name: string;
270
- char?: _oclif_core_lib_interfaces_alphabet_js.AlphabetLowercase | _oclif_core_lib_interfaces_alphabet_js.AlphabetUppercase;
271
- summary?: string;
272
- description?: string;
273
- helpLabel?: string;
274
- helpGroup?: string;
275
- env?: string;
276
- hidden?: boolean;
277
- required?: boolean;
278
- dependsOn?: string[];
279
- exclusive?: string[];
280
- exactlyOne?: string[];
281
- relationships?: _oclif_core_lib_interfaces_parser_js.Relationship[];
282
- deprecated?: true | _oclif_core_lib_interfaces_parser_js.Deprecation;
283
- aliases?: string[];
284
- charAliases?: (_oclif_core_lib_interfaces_alphabet_js.AlphabetLowercase | _oclif_core_lib_interfaces_alphabet_js.AlphabetUppercase)[];
285
- deprecateAliases?: boolean;
286
- noCacheDefault?: boolean;
287
- helpValue?: string;
288
- options?: readonly string[];
289
- multiple?: boolean;
290
- multipleNonGreedy?: boolean;
291
- delimiter?: ",";
292
- allowStdin?: boolean | "only";
293
- parse: _oclif_core_lib_interfaces_parser_js.FlagParser<unknown, string, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
294
- defaultHelp?: unknown;
295
- input: string[];
296
- default?: unknown;
297
- };
298
235
  port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
299
236
  path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
300
237
  };
@@ -5,7 +5,7 @@ import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
5
5
  import colors from '@shopify/cli-kit/node/colors';
6
6
  import { getProjectPaths, getRemixConfig, handleRemixImportFail, assertOxygenChecks } from '../remix-config.js';
7
7
  import { checkLockfileStatus } from '../check-lockfile.js';
8
- import { findMissingRoutes } from '../missing-routes.js';
8
+ import { findMissingRoutes } from '../route-validator.js';
9
9
  import { muteRemixLogs, createRemixLogger } from '../log.js';
10
10
  import { codegen } from '../codegen.js';
11
11
  import { classicBuildBundleAnalysis, classicGetBundleAnalysisSummary } from '../bundle/analyzer.js';
@@ -1,5 +1,19 @@
1
1
  import { renderWarning, renderSuccess } from '@shopify/cli-kit/node/ui';
2
2
 
3
+ const RESERVED_ROUTES = ["^api/[^/]+/graphql.json", "^cdn/", "^_t/"];
4
+ function findReservedRoutes(config) {
5
+ const routes = /* @__PURE__ */ new Set();
6
+ Object.values(config.routes).filter(
7
+ (route) => RESERVED_ROUTES.some(
8
+ (pattern) => new RegExp(pattern).test(route.path ?? "")
9
+ )
10
+ ).forEach((route) => {
11
+ if (route.path) {
12
+ routes.add(route.path);
13
+ }
14
+ });
15
+ return [...routes];
16
+ }
3
17
  const REQUIRED_ROUTES = [
4
18
  "",
5
19
  "cart",
@@ -84,5 +98,17 @@ Including these routes improves compatibility with Shopify\u2019s platform:
84
98
  });
85
99
  }
86
100
  }
101
+ function warnReservedRoutes(routes) {
102
+ if (routes.length) {
103
+ renderWarning({
104
+ headline: "Reserved routes present",
105
+ body: `Your Hydrogen project is using ${routes.length} reserved route${routes.length > 1 ? "s" : ""}.
106
+ These routes are reserved by Shopify and may cause issues with your storefront:
107
+
108
+ ` + routes.slice(0, LINE_LIMIT - (routes.length <= LINE_LIMIT ? 0 : 1)).map((route) => `\u2022 /${route}`).join("\n") + (routes.length > LINE_LIMIT ? `
109
+ \u2022 ...and ${routes.length - LINE_LIMIT + 1} more` : "")
110
+ });
111
+ }
112
+ }
87
113
 
88
- export { findMissingRoutes, logMissingRoutes };
114
+ export { findMissingRoutes, findReservedRoutes, logMissingRoutes, warnReservedRoutes };
@@ -22,7 +22,7 @@ const ROUTE_MAP = {
22
22
  account: "account*",
23
23
  search: ["search", "api.predictive-search"],
24
24
  robots: "[robots.txt]",
25
- sitemap: "[sitemap.xml]"
25
+ sitemap: ["[sitemap.xml]", "sitemap.$type.$page[.xml]"]
26
26
  };
27
27
  let allRouteTemplateFiles = [];
28
28
  async function getResolvedRoutes(routeKeys = Object.keys(ROUTE_MAP)) {
@@ -369,7 +369,7 @@
369
369
  "type": "option"
370
370
  },
371
371
  "preview": {
372
- "description": "Deploys to the Preview environment. Overrides --env-branch and Git metadata.",
372
+ "description": "Deploys to the Preview environment.",
373
373
  "name": "preview",
374
374
  "required": false,
375
375
  "allowNo": false,
@@ -669,11 +669,6 @@
669
669
  "allowNo": false,
670
670
  "type": "boolean"
671
671
  },
672
- "worker": {
673
- "hidden": true,
674
- "name": "worker",
675
- "type": "boolean"
676
- },
677
672
  "legacy-runtime": {
678
673
  "description": "[Classic Remix Compiler] Runs the app in a Node.js sandbox instead of an Oxygen worker.",
679
674
  "env": "SHOPIFY_HYDROGEN_FLAG_LEGACY_RUNTIME",
@@ -1323,11 +1318,6 @@
1323
1318
  "multiple": false,
1324
1319
  "type": "option"
1325
1320
  },
1326
- "worker": {
1327
- "hidden": true,
1328
- "name": "worker",
1329
- "type": "boolean"
1330
- },
1331
1321
  "legacy-runtime": {
1332
1322
  "description": "Runs the app in a Node.js sandbox instead of an Oxygen worker.",
1333
1323
  "env": "SHOPIFY_HYDROGEN_FLAG_LEGACY_RUNTIME",
@@ -1758,5 +1748,5 @@
1758
1748
  ]
1759
1749
  }
1760
1750
  },
1761
- "version": "8.4.6"
1751
+ "version": "9.0.1"
1762
1752
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "@shopify:registry": "https://registry.npmjs.org"
6
6
  },
7
- "version": "8.4.6",
7
+ "version": "9.0.1",
8
8
  "license": "MIT",
9
9
  "type": "module",
10
10
  "scripts": {
@@ -16,7 +16,7 @@
16
16
  "test:watch": "cross-env SHOPIFY_UNIT_TEST=1 vitest --test-timeout=60000"
17
17
  },
18
18
  "devDependencies": {
19
- "@remix-run/dev": "^2.10.1",
19
+ "@remix-run/dev": "^2.13.1",
20
20
  "@types/diff": "^5.0.2",
21
21
  "@types/gunzip-maybe": "^1.4.0",
22
22
  "@types/prettier": "^2.7.2",
@@ -27,16 +27,16 @@
27
27
  "fast-glob": "^3.2.12",
28
28
  "flame-chart-js": "2.3.2",
29
29
  "get-port": "^7.0.0",
30
- "type-fest": "^4.5.0",
30
+ "type-fest": "^4.26.1",
31
31
  "vite": "^5.1.0",
32
32
  "vitest": "^1.0.4"
33
33
  },
34
34
  "dependencies": {
35
35
  "@ast-grep/napi": "0.11.0",
36
36
  "@oclif/core": "3.26.5",
37
- "@shopify/cli-kit": "3.68.0",
38
- "@shopify/oxygen-cli": "4.4.9",
39
- "@shopify/plugin-cloudflare": "3.68.0",
37
+ "@shopify/cli": "3.69.3",
38
+ "@shopify/oxygen-cli": "4.5.3",
39
+ "@shopify/plugin-cloudflare": "3.69.3",
40
40
  "ansi-escapes": "^6.2.0",
41
41
  "chokidar": "3.5.3",
42
42
  "cli-truncate": "^4.0.0",
@@ -55,8 +55,8 @@
55
55
  "peerDependencies": {
56
56
  "@graphql-codegen/cli": "^5.0.2",
57
57
  "@remix-run/dev": "^2.1.0",
58
- "@shopify/hydrogen-codegen": "^0.3.1",
59
- "@shopify/mini-oxygen": "^3.0.6",
58
+ "@shopify/hydrogen-codegen": "^0.3.2",
59
+ "@shopify/mini-oxygen": "^3.1.0",
60
60
  "graphql-config": "^5.0.3",
61
61
  "vite": "^5.1.0"
62
62
  },