@shopify/cli-hydrogen 5.1.2 → 5.2.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 (65) hide show
  1. package/dist/commands/hydrogen/build.js +4 -1
  2. package/dist/commands/hydrogen/dev.js +26 -17
  3. package/dist/commands/hydrogen/init.js +3 -0
  4. package/dist/commands/hydrogen/init.test.js +2 -0
  5. package/dist/commands/hydrogen/preview.js +4 -3
  6. package/dist/commands/hydrogen/setup.js +3 -0
  7. package/dist/generator-templates/starter/app/components/Footer.tsx +1 -1
  8. package/dist/generator-templates/starter/app/components/Header.tsx +1 -1
  9. package/dist/generator-templates/starter/app/components/Search.tsx +3 -3
  10. package/dist/generator-templates/starter/app/root.tsx +24 -1
  11. package/dist/generator-templates/starter/app/routes/$.tsx +4 -0
  12. package/dist/generator-templates/starter/app/routes/_index.tsx +6 -2
  13. package/dist/generator-templates/starter/app/routes/account.$.tsx +1 -2
  14. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +1 -1
  15. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +2 -7
  16. package/dist/generator-templates/starter/app/routes/account.profile.tsx +7 -2
  17. package/dist/generator-templates/starter/app/routes/account.tsx +4 -3
  18. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +6 -2
  19. package/dist/generator-templates/starter/app/routes/account_.login.tsx +6 -2
  20. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +2 -6
  21. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +1 -2
  22. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +1 -2
  23. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +1 -2
  24. package/dist/generator-templates/starter/app/routes/cart.tsx +1 -2
  25. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -2
  26. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +1 -2
  27. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +2 -3
  28. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +23 -15
  29. package/dist/generator-templates/starter/app/routes/search.tsx +1 -2
  30. package/dist/generator-templates/starter/package.json +2 -2
  31. package/dist/generator-templates/starter/remix.config.js +1 -0
  32. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +9 -9
  33. package/dist/lib/ast.js +9 -0
  34. package/dist/lib/check-version.test.js +1 -0
  35. package/dist/lib/codegen.js +17 -7
  36. package/dist/lib/environment-variables.js +15 -11
  37. package/dist/lib/find-port.js +9 -0
  38. package/dist/lib/flags.js +3 -2
  39. package/dist/lib/format-code.js +3 -0
  40. package/dist/lib/live-reload.js +62 -0
  41. package/dist/lib/log.js +6 -1
  42. package/dist/lib/mini-oxygen.js +28 -18
  43. package/dist/lib/missing-routes.js +17 -1
  44. package/dist/lib/onboarding/common.js +5 -0
  45. package/dist/lib/onboarding/local.js +21 -8
  46. package/dist/lib/remix-config.js +2 -0
  47. package/dist/lib/remix-version-check.test.js +1 -0
  48. package/dist/lib/setups/css/replacers.js +7 -4
  49. package/dist/lib/setups/i18n/replacers.js +7 -5
  50. package/dist/lib/setups/routes/generate.js +4 -1
  51. package/dist/lib/template-downloader.js +4 -0
  52. package/dist/lib/transpile-ts.js +1 -0
  53. package/dist/lib/virtual-routes.js +4 -1
  54. package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +26 -4
  55. package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +40 -10
  56. package/dist/virtual-routes/components/IconBanner.jsx +286 -44
  57. package/dist/virtual-routes/components/IconDiscord.jsx +17 -1
  58. package/dist/virtual-routes/components/IconError.jsx +55 -17
  59. package/dist/virtual-routes/components/IconGithub.jsx +19 -1
  60. package/dist/virtual-routes/components/IconTwitter.jsx +17 -1
  61. package/dist/virtual-routes/components/Layout.jsx +1 -1
  62. package/dist/virtual-routes/routes/index.jsx +110 -94
  63. package/dist/virtual-routes/virtual-root.jsx +7 -15
  64. package/oclif.manifest.json +1 -1
  65. package/package.json +7 -6
@@ -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
  };
@@ -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) {
@@ -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
@@ -6,6 +6,7 @@ import colors from '@shopify/cli-kit/node/colors';
6
6
  import { SETUP_CSS_STRATEGIES } 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.",
@@ -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 };
@@ -17,6 +17,9 @@ async function getCodeFormatOptions(filePath = process.cwd()) {
17
17
  function formatCode(content, config = DEFAULT_PRETTIER_CONFIG, filePath = "") {
18
18
  const ext = extname(filePath);
19
19
  const formattedContent = prettier.format(content, {
20
+ // Specify the TypeScript parser for ts/tsx files. Otherwise
21
+ // we need to use the babel parser because the default parser
22
+ // Otherwise prettier will print a warning.
20
23
  parser: ext === ".tsx" || ext === ".ts" ? "typescript" : "babel",
21
24
  ...config
22
25
  });
@@ -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(
@@ -1,27 +1,28 @@
1
1
  import { outputToken, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
2
2
  import { resolvePath } from '@shopify/cli-kit/node/path';
3
- import { fileExists } from '@shopify/cli-kit/node/fs';
3
+ import { readFile, fileExists } from '@shopify/cli-kit/node/fs';
4
4
  import colors from '@shopify/cli-kit/node/colors';
5
5
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
6
+ import { startServer } from '@shopify/mini-oxygen';
7
+ import { DEFAULT_PORT } from './flags.js';
6
8
 
7
9
  async function startMiniOxygen({
8
10
  root,
9
- port = 3e3,
11
+ port = DEFAULT_PORT,
10
12
  watch = false,
13
+ autoReload = watch,
11
14
  buildPathWorkerFile,
12
15
  buildPathClient,
13
16
  env
14
17
  }) {
15
- const { default: miniOxygenImport } = await import('@shopify/mini-oxygen');
16
- const miniOxygenPreview = miniOxygenImport.default ?? miniOxygenImport;
17
18
  const dotenvPath = resolvePath(root, ".env");
18
- const miniOxygen = await miniOxygenPreview({
19
- workerFile: buildPathWorkerFile,
19
+ const miniOxygen = await startServer({
20
+ script: await readFile(buildPathWorkerFile),
20
21
  assetsDir: buildPathClient,
21
22
  publicPath: "",
22
23
  port,
23
24
  watch,
24
- autoReload: watch,
25
+ autoReload,
25
26
  modules: true,
26
27
  env: {
27
28
  ...env,
@@ -30,23 +31,31 @@ async function startMiniOxygen({
30
31
  envPath: !env && await fileExists(dotenvPath) ? dotenvPath : void 0,
31
32
  log: () => {
32
33
  },
33
- buildWatchPaths: watch ? [resolvePath(root, buildPathWorkerFile)] : void 0,
34
- onResponse: (request, response) => logResponse(
35
- request,
36
- response
34
+ onResponse: (request, response) => (
35
+ // 'Request' and 'Response' types in MiniOxygen comes from
36
+ // Miniflare and are slightly different from standard types.
37
+ logResponse(
38
+ request,
39
+ response
40
+ )
37
41
  )
38
42
  });
39
43
  const listeningAt = `http://localhost:${miniOxygen.port}`;
40
44
  return {
41
45
  listeningAt,
42
46
  port: miniOxygen.port,
43
- reload(nextOptions) {
44
- return miniOxygen.reload({
45
- env: {
46
- ...nextOptions?.env ?? env,
47
+ async reload(options = {}) {
48
+ const nextOptions = {};
49
+ if (options.env) {
50
+ nextOptions.env = {
51
+ ...options.env,
47
52
  ...process.env
48
- }
49
- });
53
+ };
54
+ }
55
+ if (options.worker) {
56
+ nextOptions.script = await readFile(buildPathWorkerFile);
57
+ }
58
+ return miniOxygen.reload(nextOptions);
50
59
  },
51
60
  showBanner(options) {
52
61
  console.log("");
@@ -64,8 +73,9 @@ async function startMiniOxygen({
64
73
  function logResponse(request, response) {
65
74
  try {
66
75
  const url = new URL(request.url);
67
- if (["/graphiql"].includes(url.pathname))
76
+ if (["/graphiql"].includes(url.pathname)) {
68
77
  return;
78
+ }
69
79
  const isProxy = !!response.url && response.url !== request.url;
70
80
  const isDataRequest = !isProxy && url.searchParams.has("_data");
71
81
  let route = request.url.replace(url.origin, "");
@@ -3,20 +3,34 @@ import { renderWarning, renderSuccess } from '@shopify/cli-kit/node/ui';
3
3
  const REQUIRED_ROUTES = [
4
4
  "",
5
5
  "cart",
6
+ // 'products',
6
7
  "products/:productHandle",
7
8
  "collections",
9
+ // 'collections/all',
8
10
  "collections/:collectionHandle",
11
+ // 'collections/:collectionHandle/:constraint',
12
+ // 'collections/:collectionHandle/products/:productHandle',
9
13
  "sitemap.xml",
10
14
  "robots.txt",
11
15
  "pages/:pageHandle",
16
+ // 'blogs/:blogHandle/tagged/:tagHandle',
17
+ // 'blogs/:blogHandle/:articleHandle',
18
+ // 'blogs/:blogHandle/:articleHandle/comments',
12
19
  "policies/:policyHandle",
20
+ // 'variants/:variantId',
13
21
  "search",
22
+ // 'gift_cards/:storeId/:cardId',
23
+ // 'discount/:discountCode', => Handled in storefrontRedirect
14
24
  "account",
15
25
  "account/login",
16
26
  "account/register",
27
+ // 'account/addresses',
28
+ // 'account/orders',
17
29
  "account/orders/:orderId",
18
30
  "account/reset/:id/:token",
19
31
  "account/activate/:id/:token"
32
+ // 'password',
33
+ // 'opening_soon',
20
34
  ];
21
35
  function findMissingRoutes(config, requiredRoutes = REQUIRED_ROUTES) {
22
36
  const userRoutes = Object.values(config.routes);
@@ -40,7 +54,9 @@ function findMissingRoutes(config, requiredRoutes = REQUIRED_ROUTES) {
40
54
  currentRoute.parentId = parentRoute.parentId;
41
55
  }
42
56
  const optionalSegment = ":?[^\\/\\?]+\\?";
43
- const reString = `^(${optionalSegment}\\/)?` + requiredRoute.replaceAll(".", "\\.").replace(/\//g, `\\/(${optionalSegment}\\/)?`).replace(/:[^/)?]+/g, ":[^\\/]+") + `(\\/${optionalSegment})?$`;
57
+ const reString = `^(${optionalSegment}\\/)?` + // Starts with an optional segment
58
+ requiredRoute.replaceAll(".", "\\.").replace(/\//g, `\\/(${optionalSegment}\\/)?`).replace(/:[^/)?]+/g, ":[^\\/]+") + // Replace params with regex
59
+ `(\\/${optionalSegment})?$`;
44
60
  if (new RegExp(reString).test(currentRoute.path)) {
45
61
  missingRoutes.delete(requiredRoute);
46
62
  }
@@ -163,7 +163,9 @@ async function handleProjectLocation({
163
163
  return {
164
164
  name: basename(location),
165
165
  location,
166
+ // User input. E.g. "./hydrogen-storefront"
166
167
  directory,
168
+ // Absolute path to location
167
169
  storefrontTitle: storefrontInfo?.title
168
170
  };
169
171
  }
@@ -201,6 +203,7 @@ async function handleCssStrategy(projectDir, controller, flagStyling) {
201
203
  {
202
204
  rootDirectory: projectDir,
203
205
  appDirectory: joinPath(projectDir, "app")
206
+ // Default value in new projects
204
207
  },
205
208
  true
206
209
  );
@@ -344,6 +347,8 @@ async function renderProjectReady(project, {
344
347
  body: bodyLines.map(
345
348
  ([label, value]) => ` ${(label + ":").padEnd(padMin, " ")} ${colors.dim(value)}`
346
349
  ).join("\n") + routeSummary,
350
+ // Use `customSections` instead of `nextSteps` and `references`
351
+ // here to enforce a newline between title and items.
347
352
  customSections: [
348
353
  hasErrors && {
349
354
  title: "Warnings\n",
@@ -18,7 +18,7 @@ async function setupLocalStarterTemplate(options, controller) {
18
18
  message: "Connect to Shopify",
19
19
  choices: [
20
20
  {
21
- label: "Use sample data from Mock.shop (no login required)",
21
+ label: "Use sample data from mock.shop (You can connect a Shopify account later)",
22
22
  value: "mock"
23
23
  },
24
24
  { label: "Link your Shopify account", value: "link" }
@@ -46,17 +46,22 @@ async function setupLocalStarterTemplate(options, controller) {
46
46
  let backgroundWorkPromise = copy(
47
47
  templateDir,
48
48
  project.directory,
49
+ // Filter out the `app` directory, which will be generated later
49
50
  {
50
51
  filter: (filepath) => !/^(app|dist|node_modules)\//i.test(
51
52
  relativePath(templateDir, filepath)
52
53
  )
53
54
  }
54
55
  ).then(
55
- () => generateProjectEntries({
56
- rootDirectory: project.directory,
57
- appDirectory: joinPath(project.directory, "app"),
58
- typescript: true
59
- })
56
+ () => (
57
+ // Generate project entries and their file dependencies
58
+ generateProjectEntries({
59
+ rootDirectory: project.directory,
60
+ appDirectory: joinPath(project.directory, "app"),
61
+ typescript: true
62
+ // Will be transpiled later
63
+ })
64
+ )
60
65
  ).catch(abort);
61
66
  const tasks = [
62
67
  {
@@ -74,6 +79,7 @@ async function setupLocalStarterTemplate(options, controller) {
74
79
  ];
75
80
  backgroundWorkPromise = backgroundWorkPromise.then(() => {
76
81
  const promises = [
82
+ // Add project name to package.json
77
83
  replaceFileContent(
78
84
  joinPath(project.directory, "package.json"),
79
85
  false,
@@ -83,17 +89,23 @@ async function setupLocalStarterTemplate(options, controller) {
83
89
  )
84
90
  )
85
91
  ];
86
- const envLeadingComment = "# The variables added in this file are only available locally in MiniOxygen\n";
92
+ const envLeadingComment = "# The variables added in this file are only available locally in MiniOxygen.\n# Run `h2 link` to also inject environment variables from your storefront,\n# or `h2 env pull` to populate this file.";
87
93
  if (storefrontInfo && createStorefrontPromise) {
88
94
  promises.push(
95
+ // Save linked storefront in project
89
96
  setUserAccount(project.directory, storefrontInfo),
90
97
  createStorefrontPromise.then(
91
- (storefront) => setStorefront(project.directory, storefront)
98
+ (storefront) => (
99
+ // Save linked storefront in project
100
+ setStorefront(project.directory, storefront)
101
+ )
92
102
  ),
103
+ // Write empty dotenv file to fallback to remote Oxygen variables
93
104
  writeFile(joinPath(project.directory, ".env"), envLeadingComment)
94
105
  );
95
106
  } else if (templateAction === "mock") {
96
107
  promises.push(
108
+ // Set required env vars
97
109
  writeFile(
98
110
  joinPath(project.directory, ".env"),
99
111
  envLeadingComment + "\n" + [
@@ -188,6 +200,7 @@ async function setupLocalStarterTemplate(options, controller) {
188
200
  const { setupRoutes } = await handleRouteGeneration(
189
201
  controller,
190
202
  options.routes || true
203
+ // TODO: Remove default value when multi-select UI component is available
191
204
  );
192
205
  setupSummary.i18n = i18nStrategy;
193
206
  backgroundWorkPromise = backgroundWorkPromise.then(async () => {
@@ -5,6 +5,7 @@ import { readdir } from 'node:fs/promises';
5
5
  import { AbortError } from '@shopify/cli-kit/node/error';
6
6
  import { outputWarn } from '@shopify/cli-kit/node/output';
7
7
  import { fileExists } from '@shopify/cli-kit/node/fs';
8
+ import { muteRemixLogs } from './log.js';
8
9
 
9
10
  const BUILD_DIR = "dist";
10
11
  const CLIENT_SUBDIR = "client";
@@ -25,6 +26,7 @@ function getProjectPaths(appPath, entry) {
25
26
  };
26
27
  }
27
28
  async function getRemixConfig(root, mode = process.env.NODE_ENV) {
29
+ await muteRemixLogs();
28
30
  const { readConfig } = await import('@remix-run/dev/dist/config.js');
29
31
  const config = await readConfig(root, mode);
30
32
  if (process.env.LOCAL_DEV) {
@@ -26,6 +26,7 @@ describe("remix-version-check", () => {
26
26
  it("warns when versions are out of sync", () => {
27
27
  const expectedVersion = "42.0.0-test";
28
28
  vi.mocked(requireMock).mockReturnValueOnce({
29
+ // Hydrogen expected version
29
30
  dependencies: { "@remix-run/dev": expectedVersion }
30
31
  });
31
32
  const outputMock = mockAndCaptureOutput();
@@ -1,8 +1,7 @@
1
1
  import { AbortError } from '@shopify/cli-kit/node/error';
2
- import { ts, tsx, js, jsx } from '@ast-grep/napi';
3
2
  import { findFileWithExtension, replaceFileContent } from '../../file.js';
3
+ import { importLangAstGrep } from '../../ast.js';
4
4
 
5
- const astGrep = { ts, tsx, js, jsx };
6
5
  async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
7
6
  const { filepath, astType } = await findFileWithExtension(
8
7
  rootDirectory,
@@ -14,7 +13,8 @@ async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
14
13
  );
15
14
  }
16
15
  await replaceFileContent(filepath, formatConfig, async (content) => {
17
- const root = astGrep[astType].parse(content).root();
16
+ const astGrep = await importLangAstGrep(astType);
17
+ const root = astGrep.parse(content).root();
18
18
  const remixConfigNode = root.find({
19
19
  rule: {
20
20
  kind: "object",
@@ -22,9 +22,11 @@ async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
22
22
  any: [
23
23
  {
24
24
  kind: "export_statement"
25
+ // ESM
25
26
  },
26
27
  {
27
28
  kind: "assignment_expression",
29
+ // CJS
28
30
  has: {
29
31
  kind: "member_expression",
30
32
  field: "left",
@@ -77,7 +79,8 @@ async function replaceRootLinks(appDirectory, formatConfig, importer) {
77
79
  if (content.includes(importStatement.split("from")[0])) {
78
80
  return;
79
81
  }
80
- const root = astGrep[astType].parse(content).root();
82
+ const astGrep = await importLangAstGrep(astType);
83
+ const root = astGrep.parse(content).root();
81
84
  const lastImportNode = root.findAll({ rule: { kind: "import_statement" } }).pop();
82
85
  const linksReturnNode = root.find({
83
86
  utils: {