@shopify/cli-hydrogen 5.1.2 → 5.2.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.
Files changed (73) hide show
  1. package/dist/commands/hydrogen/build.js +4 -1
  2. package/dist/commands/hydrogen/dev.js +25 -17
  3. package/dist/commands/hydrogen/generate/route.test.js +0 -1
  4. package/dist/commands/hydrogen/init.js +6 -3
  5. package/dist/commands/hydrogen/init.test.js +2 -0
  6. package/dist/commands/hydrogen/preview.js +2 -2
  7. package/dist/commands/hydrogen/setup.js +3 -0
  8. package/dist/generator-templates/starter/app/components/Footer.tsx +1 -1
  9. package/dist/generator-templates/starter/app/components/Header.tsx +1 -1
  10. package/dist/generator-templates/starter/app/components/Search.tsx +3 -3
  11. package/dist/generator-templates/starter/app/entry.server.tsx +9 -1
  12. package/dist/generator-templates/starter/app/root.tsx +31 -5
  13. package/dist/generator-templates/starter/app/routes/$.tsx +4 -0
  14. package/dist/generator-templates/starter/app/routes/_index.tsx +6 -2
  15. package/dist/generator-templates/starter/app/routes/account.$.tsx +1 -2
  16. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +1 -1
  17. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +2 -7
  18. package/dist/generator-templates/starter/app/routes/account.profile.tsx +7 -2
  19. package/dist/generator-templates/starter/app/routes/account.tsx +4 -3
  20. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +6 -2
  21. package/dist/generator-templates/starter/app/routes/account_.login.tsx +6 -2
  22. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +2 -6
  23. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +1 -2
  24. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +1 -2
  25. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +2 -3
  26. package/dist/generator-templates/starter/app/routes/cart.tsx +1 -2
  27. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -2
  28. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +1 -2
  29. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +2 -3
  30. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +23 -15
  31. package/dist/generator-templates/starter/app/routes/search.tsx +1 -2
  32. package/dist/generator-templates/starter/package.json +5 -5
  33. package/dist/generator-templates/starter/remix.config.js +1 -0
  34. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +9 -9
  35. package/dist/generator-templates/starter/tsconfig.json +1 -0
  36. package/dist/lib/ast.js +9 -0
  37. package/dist/lib/check-version.test.js +1 -0
  38. package/dist/lib/codegen.js +17 -7
  39. package/dist/lib/environment-variables.js +15 -11
  40. package/dist/lib/file.js +1 -1
  41. package/dist/lib/find-port.js +9 -0
  42. package/dist/lib/flags.js +6 -5
  43. package/dist/lib/format-code.js +7 -4
  44. package/dist/lib/graphql/admin/client.js +18 -0
  45. package/dist/lib/graphql/admin/client.test.js +28 -3
  46. package/dist/lib/live-reload.js +62 -0
  47. package/dist/lib/log.js +6 -1
  48. package/dist/lib/mini-oxygen.js +28 -18
  49. package/dist/lib/missing-routes.js +17 -1
  50. package/dist/lib/onboarding/common.js +5 -0
  51. package/dist/lib/onboarding/local.js +21 -8
  52. package/dist/lib/remix-config.js +2 -0
  53. package/dist/lib/remix-version-check.test.js +1 -0
  54. package/dist/lib/setups/css/index.js +4 -2
  55. package/dist/lib/setups/css/replacers.js +7 -4
  56. package/dist/lib/setups/i18n/replacers.js +7 -5
  57. package/dist/lib/setups/routes/generate.js +15 -29
  58. package/dist/lib/setups/routes/generate.test.js +1 -3
  59. package/dist/lib/template-downloader.js +4 -0
  60. package/dist/lib/transpile-ts.js +5 -3
  61. package/dist/lib/virtual-routes.js +4 -1
  62. package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +29 -4
  63. package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +44 -10
  64. package/dist/virtual-routes/components/IconBanner.jsx +289 -44
  65. package/dist/virtual-routes/components/IconDiscord.jsx +18 -1
  66. package/dist/virtual-routes/components/IconError.jsx +58 -17
  67. package/dist/virtual-routes/components/IconGithub.jsx +20 -1
  68. package/dist/virtual-routes/components/IconTwitter.jsx +18 -1
  69. package/dist/virtual-routes/components/Layout.jsx +2 -1
  70. package/dist/virtual-routes/routes/index.jsx +199 -94
  71. package/dist/virtual-routes/virtual-root.jsx +62 -16
  72. package/oclif.manifest.json +3 -3
  73. package/package.json +8 -7
@@ -1,8 +1,12 @@
1
1
  import {Suspense} from 'react';
2
- import type {V2_MetaFunction} from '@shopify/remix-oxygen';
3
2
  import {defer, redirect, type LoaderArgs} from '@shopify/remix-oxygen';
4
- import type {FetcherWithComponents} from '@remix-run/react';
5
- import {Await, Link, useLoaderData} from '@remix-run/react';
3
+ import {
4
+ Await,
5
+ Link,
6
+ useLoaderData,
7
+ type V2_MetaFunction,
8
+ type FetcherWithComponents,
9
+ } from '@remix-run/react';
6
10
  import type {
7
11
  ProductFragment,
8
12
  ProductVariantsQuery,
@@ -17,7 +21,10 @@ import {
17
21
  getSelectedProductOptions,
18
22
  CartForm,
19
23
  } from '@shopify/hydrogen';
20
- import type {CartLineInput} from '@shopify/hydrogen/storefront-api-types';
24
+ import type {
25
+ CartLineInput,
26
+ SelectedOption,
27
+ } from '@shopify/hydrogen/storefront-api-types';
21
28
  import {getVariantUrl} from '~/utils';
22
29
 
23
30
  export const meta: V2_MetaFunction = ({data}) => {
@@ -47,15 +54,6 @@ export async function loader({params, request, context}: LoaderArgs) {
47
54
  variables: {handle, selectedOptions},
48
55
  });
49
56
 
50
- // In order to show which variants are available in the UI, we need to query
51
- // all of them. But there might be a *lot*, so instead separate the variants
52
- // into it's own separate query that is deferred. So there's a brief moment
53
- // where variant options might show as available when they're not, but after
54
- // this deffered query resolves, the UI will update.
55
- const variants = storefront.query(VARIANTS_QUERY, {
56
- variables: {handle},
57
- });
58
-
59
57
  if (!product?.id) {
60
58
  throw new Response(null, {status: 404});
61
59
  }
@@ -63,7 +61,8 @@ export async function loader({params, request, context}: LoaderArgs) {
63
61
  const firstVariant = product.variants.nodes[0];
64
62
  const firstVariantIsDefault = Boolean(
65
63
  firstVariant.selectedOptions.find(
66
- (option) => option.name === 'Title' && option.value === 'Default Title',
64
+ (option: SelectedOption) =>
65
+ option.name === 'Title' && option.value === 'Default Title',
67
66
  ),
68
67
  );
69
68
 
@@ -76,6 +75,16 @@ export async function loader({params, request, context}: LoaderArgs) {
76
75
  return redirectToFirstVariant({product, request});
77
76
  }
78
77
  }
78
+
79
+ // In order to show which variants are available in the UI, we need to query
80
+ // all of them. But there might be a *lot*, so instead separate the variants
81
+ // into it's own separate query that is deferred. So there's a brief moment
82
+ // where variant options might show as available when they're not, but after
83
+ // this deffered query resolves, the UI will update.
84
+ const variants = storefront.query(VARIANTS_QUERY, {
85
+ variables: {handle},
86
+ });
87
+
79
88
  return defer({product, variants});
80
89
  }
81
90
 
@@ -337,7 +346,6 @@ const PRODUCT_VARIANT_FRAGMENT = `#graphql
337
346
  title
338
347
  handle
339
348
  }
340
- quantityAvailable
341
349
  selectedOptions {
342
350
  name
343
351
  value
@@ -1,6 +1,5 @@
1
- import type {V2_MetaFunction} from '@shopify/remix-oxygen';
2
1
  import {defer, type LoaderArgs} from '@shopify/remix-oxygen';
3
- import {useLoaderData} from '@remix-run/react';
2
+ import {useLoaderData, type V2_MetaFunction} from '@remix-run/react';
4
3
  import {getPaginationVariables} from '@shopify/hydrogen';
5
4
 
6
5
  import {SearchForm, SearchResults, NoSearchResults} from '~/components/Search';
@@ -15,8 +15,8 @@
15
15
  "dependencies": {
16
16
  "@remix-run/react": "1.19.1",
17
17
  "@shopify/cli": "3.48.0",
18
- "@shopify/cli-hydrogen": "^5.1.2",
19
- "@shopify/hydrogen": "^2023.7.2",
18
+ "@shopify/cli-hydrogen": "^5.2.1",
19
+ "@shopify/hydrogen": "^2023.7.4",
20
20
  "@shopify/remix-oxygen": "^1.1.3",
21
21
  "graphql": "^16.6.0",
22
22
  "graphql-tag": "^2.12.6",
@@ -30,12 +30,12 @@
30
30
  "@shopify/prettier-config": "^1.1.2",
31
31
  "@total-typescript/ts-reset": "^0.4.2",
32
32
  "@types/eslint": "^8.4.10",
33
- "@types/react": "^18.0.20",
34
- "@types/react-dom": "^18.0.6",
33
+ "@types/react": "^18.2.20",
34
+ "@types/react-dom": "^18.2.7",
35
35
  "eslint": "^8.20.0",
36
36
  "eslint-plugin-hydrogen": "0.12.2",
37
37
  "prettier": "^2.8.4",
38
- "typescript": "^4.9.5"
38
+ "typescript": "^5.2.2"
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=16.13"
@@ -17,6 +17,7 @@ module.exports = {
17
17
  serverPlatform: 'neutral',
18
18
  serverMinify: process.env.NODE_ENV === 'production',
19
19
  future: {
20
+ v2_dev: true,
20
21
  v2_meta: true,
21
22
  v2_headers: true,
22
23
  v2_errorBoundary: true,
@@ -1350,7 +1350,7 @@ export type PoliciesQuery = {
1350
1350
 
1351
1351
  export type ProductVariantFragment = Pick<
1352
1352
  StorefrontAPI.ProductVariant,
1353
- 'availableForSale' | 'id' | 'quantityAvailable' | 'sku' | 'title'
1353
+ 'availableForSale' | 'id' | 'sku' | 'title'
1354
1354
  > & {
1355
1355
  compareAtPrice?: StorefrontAPI.Maybe<
1356
1356
  Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>
@@ -1377,7 +1377,7 @@ export type ProductFragment = Pick<
1377
1377
  selectedVariant?: StorefrontAPI.Maybe<
1378
1378
  Pick<
1379
1379
  StorefrontAPI.ProductVariant,
1380
- 'availableForSale' | 'id' | 'quantityAvailable' | 'sku' | 'title'
1380
+ 'availableForSale' | 'id' | 'sku' | 'title'
1381
1381
  > & {
1382
1382
  compareAtPrice?: StorefrontAPI.Maybe<
1383
1383
  Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>
@@ -1402,7 +1402,7 @@ export type ProductFragment = Pick<
1402
1402
  nodes: Array<
1403
1403
  Pick<
1404
1404
  StorefrontAPI.ProductVariant,
1405
- 'availableForSale' | 'id' | 'quantityAvailable' | 'sku' | 'title'
1405
+ 'availableForSale' | 'id' | 'sku' | 'title'
1406
1406
  > & {
1407
1407
  compareAtPrice?: StorefrontAPI.Maybe<
1408
1408
  Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>
@@ -1446,7 +1446,7 @@ export type ProductQuery = {
1446
1446
  selectedVariant?: StorefrontAPI.Maybe<
1447
1447
  Pick<
1448
1448
  StorefrontAPI.ProductVariant,
1449
- 'availableForSale' | 'id' | 'quantityAvailable' | 'sku' | 'title'
1449
+ 'availableForSale' | 'id' | 'sku' | 'title'
1450
1450
  > & {
1451
1451
  compareAtPrice?: StorefrontAPI.Maybe<
1452
1452
  Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>
@@ -1471,7 +1471,7 @@ export type ProductQuery = {
1471
1471
  nodes: Array<
1472
1472
  Pick<
1473
1473
  StorefrontAPI.ProductVariant,
1474
- 'availableForSale' | 'id' | 'quantityAvailable' | 'sku' | 'title'
1474
+ 'availableForSale' | 'id' | 'sku' | 'title'
1475
1475
  > & {
1476
1476
  compareAtPrice?: StorefrontAPI.Maybe<
1477
1477
  Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>
@@ -1503,7 +1503,7 @@ export type ProductVariantsFragment = {
1503
1503
  nodes: Array<
1504
1504
  Pick<
1505
1505
  StorefrontAPI.ProductVariant,
1506
- 'availableForSale' | 'id' | 'quantityAvailable' | 'sku' | 'title'
1506
+ 'availableForSale' | 'id' | 'sku' | 'title'
1507
1507
  > & {
1508
1508
  compareAtPrice?: StorefrontAPI.Maybe<
1509
1509
  Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>
@@ -1539,7 +1539,7 @@ export type ProductVariantsQuery = {
1539
1539
  nodes: Array<
1540
1540
  Pick<
1541
1541
  StorefrontAPI.ProductVariant,
1542
- 'availableForSale' | 'id' | 'quantityAvailable' | 'sku' | 'title'
1542
+ 'availableForSale' | 'id' | 'sku' | 'title'
1543
1543
  > & {
1544
1544
  compareAtPrice?: StorefrontAPI.Maybe<
1545
1545
  Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>
@@ -1839,11 +1839,11 @@ interface GeneratedQueryTypes {
1839
1839
  return: PoliciesQuery;
1840
1840
  variables: PoliciesQueryVariables;
1841
1841
  };
1842
- '#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) {\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 quantityAvailable\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n': {
1842
+ '#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) {\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': {
1843
1843
  return: ProductQuery;
1844
1844
  variables: ProductQueryVariables;
1845
1845
  };
1846
- '#graphql\n #graphql\n fragment ProductVariants on Product {\n variants(first: 250) {\n nodes {\n ...ProductVariant\n }\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 quantityAvailable\n selectedOptions {\n name\n value\n }\n sku\n title\n unitPrice {\n amount\n currencyCode\n }\n }\n\n\n query ProductVariants(\n $country: CountryCode\n $language: LanguageCode\n $handle: String!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...ProductVariants\n }\n }\n': {
1846
+ '#graphql\n #graphql\n fragment ProductVariants on Product {\n variants(first: 250) {\n nodes {\n ...ProductVariant\n }\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 query ProductVariants(\n $country: CountryCode\n $language: LanguageCode\n $handle: String!\n ) @inContext(country: $country, language: $language) {\n product(handle: $handle) {\n ...ProductVariants\n }\n }\n': {
1847
1847
  return: ProductVariantsQuery;
1848
1848
  variables: ProductVariantsQueryVariables;
1849
1849
  };
@@ -7,6 +7,7 @@
7
7
  "jsx": "react-jsx",
8
8
  "moduleResolution": "node",
9
9
  "resolveJsonModule": true,
10
+ "module": "ES2022",
10
11
  "target": "ES2022",
11
12
  "strict": true,
12
13
  "allowJs": true,
@@ -0,0 +1,9 @@
1
+ async function importLangAstGrep(lang) {
2
+ const astGrep = await import('@ast-grep/napi');
3
+ if (!(lang in astGrep)) {
4
+ throw new Error(`Wrong language for AST: ${lang}`);
5
+ }
6
+ return astGrep[lang];
7
+ }
8
+
9
+ export { importLangAstGrep };
@@ -34,6 +34,7 @@ describe("checkHydrogenVersion()", () => {
34
34
  await checkHydrogenVersion("dir");
35
35
  expect(checkForNewVersion).toHaveBeenCalledWith(
36
36
  "@shopify/hydrogen",
37
+ // Calver
37
38
  expect.stringMatching(/20\d{2}\.\d{1,2}\.\d{1,3}/)
38
39
  );
39
40
  });
@@ -2,7 +2,7 @@ import { loadCodegenConfig, generate } from '@graphql-codegen/cli';
2
2
  import { patchGqlPluck, pluckConfig, preset, schema } from '@shopify/hydrogen-codegen';
3
3
  import { getCodeFormatOptions, formatCode } from './format-code.js';
4
4
  import { renderWarning, renderFatalError } from '@shopify/cli-kit/node/ui';
5
- import { joinPath } from '@shopify/cli-kit/node/path';
5
+ import { relativePath, joinPath } from '@shopify/cli-kit/node/path';
6
6
  import { AbortError } from '@shopify/cli-kit/node/error';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { fileURLToPath } from 'node:url';
@@ -82,16 +82,22 @@ async function generateTypes({
82
82
  forceSfapiVersion,
83
83
  ...dirs
84
84
  }) {
85
- const { config: codegenConfig } = await loadCodegenConfig({
86
- configFilePath,
87
- searchPlaces: [dirs.rootDirectory]
88
- }) || generateDefaultConfig(dirs, forceSfapiVersion);
85
+ const { config: codegenConfig } = (
86
+ // Load <root>/codegen.ts if available
87
+ await loadCodegenConfig({
88
+ configFilePath,
89
+ searchPlaces: [dirs.rootDirectory]
90
+ }) || // Fall back to default config
91
+ generateDefaultConfig(dirs, forceSfapiVersion)
92
+ );
89
93
  await addHooksToHydrogenOptions(codegenConfig, dirs);
90
94
  await generate(
91
95
  {
92
96
  ...codegenConfig,
93
97
  cwd: dirs.rootDirectory,
94
98
  watch,
99
+ // Note: do not use `silent` without `watch`, it will swallow errors and
100
+ // won't hide all logs. `errorsOnly` flag doesn't work either.
95
101
  silent: !watch
96
102
  },
97
103
  true
@@ -100,6 +106,7 @@ async function generateTypes({
100
106
  }
101
107
  function generateDefaultConfig({ rootDirectory, appDirectory }, forceSfapiVersion) {
102
108
  const tsDefaultGlob = "*!(*.d).{ts,tsx}";
109
+ const appDirRelative = relativePath(rootDirectory, appDirectory);
103
110
  return {
104
111
  filepath: "virtual:codegen",
105
112
  config: {
@@ -110,8 +117,10 @@ function generateDefaultConfig({ rootDirectory, appDirectory }, forceSfapiVersio
110
117
  preset,
111
118
  schema,
112
119
  documents: [
113
- joinPath(rootDirectory, tsDefaultGlob),
114
- joinPath(appDirectory, "**", tsDefaultGlob)
120
+ tsDefaultGlob,
121
+ // E.g. ./server.ts
122
+ joinPath(appDirRelative, "**", tsDefaultGlob)
123
+ // E.g. app/routes/_index.tsx
115
124
  ],
116
125
  ...!!forceSfapiVersion && {
117
126
  presetConfig: { importTypes: false },
@@ -142,6 +151,7 @@ async function addHooksToHydrogenOptions(codegenConfig, { rootDirectory }) {
142
151
  const formatConfig = await getCodeFormatOptions(rootDirectory);
143
152
  hydrogenOptions.hooks = {
144
153
  beforeOneFileWrite: (file, content) => formatCode(content, formatConfig, file),
154
+ // Run Prettier before writing files
145
155
  ...hydrogenOptions.hooks
146
156
  };
147
157
  }
@@ -8,6 +8,10 @@ import colors from '@shopify/cli-kit/node/colors';
8
8
  import { getStorefrontEnvVariables } from './graphql/admin/pull-variables.js';
9
9
  import { login } from './auth.js';
10
10
 
11
+ const createEmptyRemoteVars = () => ({
12
+ remoteVariables: {},
13
+ remoteSecrets: {}
14
+ });
11
15
  async function getAllEnvironmentVariables({
12
16
  root,
13
17
  envBranch,
@@ -15,7 +19,15 @@ async function getAllEnvironmentVariables({
15
19
  }) {
16
20
  const dotEnvPath = resolvePath(root, ".env");
17
21
  const [{ remoteVariables, remoteSecrets }, { variables: localVariables }] = await Promise.all([
18
- fetchRemote ? getRemoteVariables(root, envBranch) : { remoteVariables: {}, remoteSecrets: {} },
22
+ // Get remote vars
23
+ fetchRemote ? getRemoteVariables(root, envBranch).catch((error) => {
24
+ renderWarning({
25
+ headline: "Failed to load environment variables from Shopify. The development server will still start, but the following error occurred:",
26
+ body: [error.message, error.tryMessage, error.nextSteps].filter(Boolean).join("\n\n")
27
+ });
28
+ return createEmptyRemoteVars();
29
+ }) : createEmptyRemoteVars(),
30
+ // Get local vars
19
31
  fileExists(dotEnvPath).then(
20
32
  (exists) => exists ? readAndParseDotEnv(dotEnvPath) : { variables: {} }
21
33
  )
@@ -29,6 +41,7 @@ async function getAllEnvironmentVariables({
29
41
  linesToColumns([
30
42
  ...remotePublicKeys.filter((key) => !localKeys.includes(key)).map((key) => [key, "from Oxygen"]),
31
43
  ...localKeys.map((key) => [key, "from local .env"]),
44
+ // Ensure secret variables always get added to the bottom of the list
32
45
  ...remoteSecretKeys.filter((key) => !localKeys.includes(key)).map((key) => [
33
46
  colors.dim(key),
34
47
  colors.dim("from Oxygen (Marked as secret)")
@@ -44,16 +57,7 @@ async function getAllEnvironmentVariables({
44
57
  }
45
58
  async function getRemoteVariables(root, envBranch) {
46
59
  const { session, config } = await login(root);
47
- const envVariables = (await getStorefrontEnvVariables(
48
- session,
49
- config.storefront.id,
50
- envBranch
51
- ).catch((error) => {
52
- renderWarning({
53
- headline: `Failed to load environment variables. The development server will still start, but the following error occurred:`,
54
- body: error?.stack ?? error?.message ?? error
55
- });
56
- }))?.environmentVariables || [];
60
+ const envVariables = (await getStorefrontEnvVariables(session, config.storefront.id, envBranch))?.environmentVariables || [];
57
61
  const remoteVariables = {};
58
62
  const remoteSecrets = {};
59
63
  for (const { key, value, isSecret } of envVariables) {
package/dist/lib/file.js CHANGED
@@ -8,7 +8,7 @@ async function replaceFileContent(filepath, formatConfig, replacer) {
8
8
  if (typeof content !== "string")
9
9
  return;
10
10
  if (formatConfig) {
11
- content = formatCode(content, formatConfig, filepath);
11
+ content = await formatCode(content, formatConfig, filepath);
12
12
  }
13
13
  return writeFile(filepath, content);
14
14
  }
@@ -0,0 +1,9 @@
1
+ import getPort, { portNumbers } from 'get-port';
2
+
3
+ function findPort(portPreference, range = 100) {
4
+ return getPort({
5
+ port: portNumbers(portPreference, portPreference + range)
6
+ });
7
+ }
8
+
9
+ export { findPort };
package/dist/lib/flags.js CHANGED
@@ -3,9 +3,10 @@ import { camelize } from '@shopify/cli-kit/common/string';
3
3
  import { renderInfo } from '@shopify/cli-kit/node/ui';
4
4
  import { normalizeStoreFqdn } from '@shopify/cli-kit/node/context/fqdn';
5
5
  import colors from '@shopify/cli-kit/node/colors';
6
- import { SETUP_CSS_STRATEGIES } from './setups/css/index.js';
6
+ import { STYLING_CHOICES } from './setups/css/index.js';
7
7
  import { I18N_CHOICES } from './setups/i18n/index.js';
8
8
 
9
+ const DEFAULT_PORT = 3e3;
9
10
  const commonFlags = {
10
11
  path: Flags.string({
11
12
  description: "The path to the directory of the Hydrogen storefront. The default is the current directory.",
@@ -14,7 +15,7 @@ const commonFlags = {
14
15
  port: Flags.integer({
15
16
  description: "Port to run the server on.",
16
17
  env: "SHOPIFY_HYDROGEN_FLAG_PORT",
17
- default: 3e3
18
+ default: DEFAULT_PORT
18
19
  }),
19
20
  force: Flags.boolean({
20
21
  description: "Overwrite the destination directory and files if they already exist.",
@@ -49,10 +50,10 @@ const commonFlags = {
49
50
  dependsOn: ["codegen-unstable"]
50
51
  }),
51
52
  styling: Flags.string({
52
- description: `Sets the styling strategy to use. One of ${SETUP_CSS_STRATEGIES.map(
53
+ description: `Sets the styling strategy to use. One of ${STYLING_CHOICES.map(
53
54
  (item) => `\`${item}\``
54
55
  ).join(", ")}.`,
55
- choices: SETUP_CSS_STRATEGIES,
56
+ choices: STYLING_CHOICES,
56
57
  env: "SHOPIFY_HYDROGEN_FLAG_STYLING"
57
58
  }),
58
59
  markets: Flags.string({
@@ -112,4 +113,4 @@ function overrideFlag(flag, extra) {
112
113
  };
113
114
  }
114
115
 
115
- export { commonFlags, deprecated, flagsToCamelObject, overrideFlag, parseProcessFlags };
116
+ export { DEFAULT_PORT, commonFlags, deprecated, flagsToCamelObject, overrideFlag, parseProcessFlags };
@@ -1,4 +1,3 @@
1
- import prettier from 'prettier';
2
1
  import { extname } from '@shopify/cli-kit/node/path';
3
2
 
4
3
  const DEFAULT_PRETTIER_CONFIG = {
@@ -9,18 +8,22 @@ const DEFAULT_PRETTIER_CONFIG = {
9
8
  };
10
9
  async function getCodeFormatOptions(filePath = process.cwd()) {
11
10
  try {
11
+ const prettier = await import('prettier');
12
12
  return await prettier.resolveConfig(filePath) || DEFAULT_PRETTIER_CONFIG;
13
13
  } catch {
14
14
  return DEFAULT_PRETTIER_CONFIG;
15
15
  }
16
16
  }
17
- function formatCode(content, config = DEFAULT_PRETTIER_CONFIG, filePath = "") {
17
+ async function formatCode(content, config = DEFAULT_PRETTIER_CONFIG, filePath = "") {
18
18
  const ext = extname(filePath);
19
- const formattedContent = prettier.format(content, {
19
+ const prettier = await import('prettier');
20
+ return prettier.format(content, {
21
+ // Specify the TypeScript parser for ts/tsx files. Otherwise
22
+ // we need to use the babel parser because the default parser
23
+ // Otherwise prettier will print a warning.
20
24
  parser: ext === ".tsx" || ext === ".ts" ? "typescript" : "babel",
21
25
  ...config
22
26
  });
23
- return formattedContent;
24
27
  }
25
28
 
26
29
  export { formatCode, getCodeFormatOptions };
@@ -20,6 +20,24 @@ async function adminRequest(query, session, variables) {
20
20
  "Install the Hydrogen sales channel on your store to start creating and linking Hydrogen storefronts: https://apps.shopify.com/hydrogen"
21
21
  );
22
22
  }
23
+ if (errors?.some?.(
24
+ (error2) => error2.message.includes(
25
+ "Access denied for hydrogenStorefrontCreate field"
26
+ )
27
+ )) {
28
+ throw new AbortError("Couldn't connect storefront to Shopify", [
29
+ "Common reasons for this error include:",
30
+ {
31
+ list: {
32
+ items: [
33
+ "The Hydrogen sales channel isn't installed on the store.",
34
+ "You don't have the required account permission to manage apps or channels.",
35
+ "You're trying to connect to an ineligible store type (Trial, Development store)"
36
+ ]
37
+ }
38
+ }
39
+ ]);
40
+ }
23
41
  throw error;
24
42
  }
25
43
  }
@@ -16,8 +16,8 @@ describe("adminRequest", () => {
16
16
  });
17
17
  expect(response).toContain(fakeResponse);
18
18
  });
19
- describe("error response", () => {
20
- it("sends a query to the Admin API and returns an unknown error response", async () => {
19
+ describe("when there is an unknown error response", () => {
20
+ it("passes along the error message", async () => {
21
21
  const fakeGraphqlError = {
22
22
  errors: [
23
23
  {
@@ -32,7 +32,9 @@ describe("adminRequest", () => {
32
32
  });
33
33
  await expect(response).rejects.toContain(fakeGraphqlError);
34
34
  });
35
- it("sends a query to the Admin API and returns an error where app isn't installed", async () => {
35
+ });
36
+ describe("when the app isn't installed", () => {
37
+ it("throws an AbortError", async () => {
36
38
  const fakeGraphqlError = {
37
39
  errors: [
38
40
  {
@@ -46,6 +48,29 @@ describe("adminRequest", () => {
46
48
  storeFqdn: ""
47
49
  });
48
50
  await expect(response).rejects.toThrowError(AbortError);
51
+ await expect(response).rejects.toMatch(
52
+ /Hydrogen sales channel isn\'t installed/
53
+ );
54
+ });
55
+ });
56
+ describe("when the user doesn't have access to hydrogenStorefrontCreate", () => {
57
+ it("throws an AbortError", async () => {
58
+ const fakeGraphqlError = {
59
+ errors: [
60
+ {
61
+ message: "Access denied for hydrogenStorefrontCreate field"
62
+ }
63
+ ]
64
+ };
65
+ vi.mocked(graphqlRequest).mockRejectedValue(fakeGraphqlError);
66
+ const response = adminRequest("", {
67
+ token: "",
68
+ storeFqdn: ""
69
+ });
70
+ await expect(response).rejects.toThrowError(AbortError);
71
+ await expect(response).rejects.toMatch(
72
+ /Couldn\'t connect storefront to Shopify/
73
+ );
49
74
  });
50
75
  });
51
76
  });
@@ -0,0 +1,62 @@
1
+ import http from 'node:http';
2
+
3
+ async function setupLiveReload(devServerPort) {
4
+ try {
5
+ const [{ updates: hmrUpdates }, { serve }, { detectLoaderChanges }, { ok, err }] = await Promise.all([
6
+ import('@remix-run/dev/dist/devServer_unstable/hmr.js'),
7
+ import('@remix-run/dev/dist/devServer_unstable/socket.js'),
8
+ import('@remix-run/dev/dist/devServer_unstable/hdr.js'),
9
+ import('@remix-run/dev/dist/result.js')
10
+ ]);
11
+ const state = {};
12
+ const server = http.createServer(function(req, res) {
13
+ res.writeHead(200);
14
+ res.end();
15
+ }).listen(devServerPort);
16
+ const socket = serve(server);
17
+ return {
18
+ onBuildStart: (ctx) => {
19
+ state.loaderChanges = detectLoaderChanges(ctx).then(ok, err);
20
+ },
21
+ onBuildManifest: (manifest) => {
22
+ state.manifest = manifest;
23
+ },
24
+ onAppReady: async (ctx) => {
25
+ const nextState = { prevManifest: state.manifest };
26
+ try {
27
+ const loaderChanges = await state.loaderChanges;
28
+ if (loaderChanges.ok) {
29
+ nextState.prevLoaderHashes = loaderChanges.value;
30
+ }
31
+ if (loaderChanges.ok && state.manifest && state.prevManifest) {
32
+ socket.hmr(
33
+ state.manifest,
34
+ hmrUpdates(
35
+ ctx.config,
36
+ state.manifest,
37
+ state.prevManifest,
38
+ loaderChanges.value,
39
+ state.prevLoaderHashes
40
+ )
41
+ );
42
+ } else if (state.prevManifest) {
43
+ socket.reload();
44
+ }
45
+ } finally {
46
+ Object.assign(state, nextState);
47
+ }
48
+ },
49
+ close: () => {
50
+ socket.close();
51
+ server.close();
52
+ }
53
+ };
54
+ } catch (error) {
55
+ console.warn(
56
+ "Could not start HMR server. Please make sure your Remix packages are in sync with Hydrogen. Defaulting to regular live reload.",
57
+ error.stack
58
+ );
59
+ }
60
+ }
61
+
62
+ export { setupLiveReload };
package/dist/lib/log.js CHANGED
@@ -121,7 +121,10 @@ function enhanceH2Logs(options) {
121
121
  injectLogReplacer("error");
122
122
  injectLogReplacer(
123
123
  "warn",
124
- ([first]) => first?.includes?.("[h2:warn:createStorefrontClient]") ? true : void 0
124
+ ([first]) => (
125
+ // Show createStorefrontClient warnings only once.
126
+ first?.includes?.("[h2:warn:createStorefrontClient]") ? true : void 0
127
+ )
125
128
  );
126
129
  addMessageReplacers("h2-warn", [
127
130
  ([first]) => {
@@ -164,7 +167,9 @@ function enhanceH2Logs(options) {
164
167
  if (firstAppLineIndex > 0 && lastAppLineIndex > firstAppLineIndex) {
165
168
  stack = [
166
169
  stackLines[0],
170
+ // Error message
167
171
  ...stackLines.slice(firstAppLineIndex, lastAppLineIndex)
172
+ // App code
168
173
  ].join("\n").trim() || void 0;
169
174
  }
170
175
  const error = new BugError(