@shopify/cli-hydrogen 5.3.1 → 5.4.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,8 +1,8 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
3
  import { outputInfo, outputWarn, outputContent, outputToken } from '@shopify/cli-kit/node/output';
4
- import { rmdir, fileSize, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
5
- import { resolvePath, relativePath } from '@shopify/cli-kit/node/path';
4
+ import { rmdir, fileSize, glob, readFile, writeFile, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
5
+ import { resolvePath, joinPath, relativePath } from '@shopify/cli-kit/node/path';
6
6
  import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
7
7
  import colors from '@shopify/cli-kit/node/colors';
8
8
  import { getProjectPaths, getRemixConfig, handleRemixImportFail, assertOxygenChecks } from '../../lib/remix-config.js';
@@ -13,6 +13,7 @@ import { muteRemixLogs, createRemixLogger } from '../../lib/log.js';
13
13
  import { codegen } from '../../lib/codegen.js';
14
14
  import { hasMetafile, buildBundleAnalysis, getBundleAnalysisSummary } from '../../lib/bundle/analyzer.js';
15
15
  import { AbortError } from '@shopify/cli-kit/node/error';
16
+ import { isCI } from '../../lib/is-ci.js';
16
17
 
17
18
  const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
18
19
  const MAX_WORKER_BUNDLE_SIZE = 10;
@@ -26,21 +27,27 @@ class Build extends Command {
26
27
  allowNo: true,
27
28
  default: true
28
29
  }),
29
- ["bundle-stats"]: Flags.boolean({
30
+ "bundle-stats": Flags.boolean({
30
31
  description: "Show a bundle size summary after building.",
31
32
  default: true,
32
33
  allowNo: true
33
34
  }),
35
+ "lockfile-check": Flags.boolean({
36
+ description: "Checks that there is exactly 1 valid lockfile in the project.",
37
+ env: "SHOPIFY_HYDROGEN_FLAG_LOCKFILE_CHECK",
38
+ default: true,
39
+ allowNo: true
40
+ }),
34
41
  "disable-route-warning": Flags.boolean({
35
42
  description: "Disable warning about missing standard routes.",
36
43
  env: "SHOPIFY_HYDROGEN_FLAG_DISABLE_ROUTE_WARNING"
37
44
  }),
38
- ["codegen-unstable"]: Flags.boolean({
45
+ "codegen-unstable": Flags.boolean({
39
46
  description: "Generate types for the Storefront API queries found in your project.",
40
47
  required: false,
41
48
  default: false
42
49
  }),
43
- ["codegen-config-path"]: commonFlags.codegenConfigPath,
50
+ "codegen-config-path": commonFlags.codegenConfigPath,
44
51
  base: deprecated("--base")(),
45
52
  entry: deprecated("--entry")(),
46
53
  target: deprecated("--target")()
@@ -62,6 +69,7 @@ async function runBuild({
62
69
  sourcemap = false,
63
70
  disableRouteWarning = false,
64
71
  bundleStats = true,
72
+ lockfileCheck = true,
65
73
  assetPath
66
74
  }) {
67
75
  if (!process.env.NODE_ENV) {
@@ -71,7 +79,10 @@ async function runBuild({
71
79
  process.env.HYDROGEN_ASSET_BASE_URL = assetPath;
72
80
  }
73
81
  const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(directory);
74
- await Promise.all([checkLockfileStatus(root), muteRemixLogs()]);
82
+ if (lockfileCheck) {
83
+ await checkLockfileStatus(root, isCI());
84
+ }
85
+ await muteRemixLogs();
75
86
  console.time(LOG_WORKER_BUILT);
76
87
  outputInfo(`
77
88
  \u{1F3D7}\uFE0F Building in ${process.env.NODE_ENV} mode...`);
@@ -135,10 +146,25 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
135
146
  );
136
147
  }
137
148
  }
149
+ if (process.env.NODE_ENV !== "development") {
150
+ await cleanClientSourcemaps(buildPathClient);
151
+ }
138
152
  if (!process.env.SHOPIFY_UNIT_TEST && !assetPath) {
139
153
  process.exit(0);
140
154
  }
141
155
  }
156
+ async function cleanClientSourcemaps(buildPathClient) {
157
+ const bundleFiles = await glob(joinPath(buildPathClient, "**/*.js"));
158
+ await Promise.all(
159
+ bundleFiles.map(async (filePath) => {
160
+ const file = await readFile(filePath);
161
+ return await writeFile(
162
+ filePath,
163
+ file.replace(/\/\/# sourceMappingURL=.+\.js\.map$/gm, "")
164
+ );
165
+ })
166
+ );
167
+ }
142
168
  async function writeBundleAnalysis(buildPath, root, buildPathWorkerFile, sizeMB, bundleStats, remixConfig) {
143
169
  const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
144
170
  outputInfo(
@@ -3,11 +3,13 @@ import Command from '@shopify/cli-kit/node/base-command';
3
3
  import colors from '@shopify/cli-kit/node/colors';
4
4
  import { outputWarn, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
5
5
  import { AbortError } from '@shopify/cli-kit/node/error';
6
+ import { getLatestGitCommit } from '@shopify/cli-kit/node/git';
6
7
  import { resolvePath } from '@shopify/cli-kit/node/path';
7
- import { renderFatalError, renderSuccess, renderTasks } from '@shopify/cli-kit/node/ui';
8
+ import { renderFatalError, renderSelectPrompt, renderSuccess, renderTasks } from '@shopify/cli-kit/node/ui';
9
+ import { ciPlatform } from '@shopify/cli-kit/node/context/local';
8
10
  import { parseToken, createDeploy } from '@shopify/oxygen-cli/deploy';
9
- import { commonFlags } from '../../lib/flags.js';
10
- import { getOxygenDeploymentToken } from '../../lib/get-oxygen-token.js';
11
+ import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
12
+ import { getOxygenDeploymentData } from '../../lib/get-oxygen-deployment-data.js';
11
13
  import { runBuild } from './build.js';
12
14
 
13
15
  const deploymentLogger = (message, level = "info") => {
@@ -17,25 +19,36 @@ const deploymentLogger = (message, level = "info") => {
17
19
  };
18
20
  class Deploy extends Command {
19
21
  static flags = {
22
+ "env-branch": Flags.string({
23
+ char: "e",
24
+ description: "Environment branch (tag) for environment to deploy to",
25
+ required: false
26
+ }),
20
27
  path: commonFlags.path,
21
28
  shop: commonFlags.shop,
22
- publicDeployment: Flags.boolean({
29
+ "public-deployment": Flags.boolean({
23
30
  env: "SHOPIFY_HYDROGEN_FLAG_PUBLIC_DEPLOYMENT",
24
31
  description: "Marks a preview deployment as publicly accessible.",
25
32
  required: false,
26
33
  default: false
27
34
  }),
28
- metadataUrl: Flags.string({
35
+ token: Flags.string({
36
+ char: "t",
37
+ description: "Oxygen deployment token",
38
+ env: "SHOPIFY_HYDROGEN_DEPLOYMENT_TOKEN",
39
+ required: false
40
+ }),
41
+ "metadata-url": Flags.string({
29
42
  description: "URL that links to the deployment. Will be saved and displayed in the Shopify admin",
30
43
  required: false,
31
44
  env: "SHOPIFY_HYDROGEN_FLAG_METADATA_URL"
32
45
  }),
33
- metadataUser: Flags.string({
46
+ "metadata-user": Flags.string({
34
47
  description: "User that initiated the deployment. Will be saved and displayed in the Shopify admin",
35
48
  required: false,
36
49
  env: "SHOPIFY_HYDROGEN_FLAG_METADATA_USER"
37
50
  }),
38
- metadataVersion: Flags.string({
51
+ "metadata-version": Flags.string({
39
52
  description: "A version identifier for the deployment. Will be saved and displayed in the Shopify admin",
40
53
  required: false,
41
54
  env: "SHOPIFY_HYDROGEN_FLAG_METADATA_VERSION"
@@ -44,24 +57,26 @@ class Deploy extends Command {
44
57
  static hidden = true;
45
58
  async run() {
46
59
  const { flags } = await this.parse(Deploy);
47
- const actualPath = flags.path ? resolvePath(flags.path) : process.cwd();
48
- await oxygenDeploy({
49
- path: actualPath,
50
- shop: flags.shop,
51
- publicDeployment: flags.publicDeployment,
52
- metadataUrl: flags.metadataUrl,
53
- metadataUser: flags.metadataUser,
54
- metadataVersion: flags.metadataVersion
55
- }).catch((error) => {
60
+ const deploymentOptions = this.flagsToOxygenDeploymentOptions(flags);
61
+ await oxygenDeploy(deploymentOptions).catch((error) => {
56
62
  renderFatalError(error);
57
63
  process.exit(1);
58
64
  }).finally(() => {
59
65
  process.exit(0);
60
66
  });
61
67
  }
68
+ flagsToOxygenDeploymentOptions(flags) {
69
+ const camelFlags = flagsToCamelObject(flags);
70
+ return {
71
+ ...camelFlags,
72
+ environmentTag: flags["env-branch"],
73
+ path: flags.path ? resolvePath(flags.path) : process.cwd()
74
+ };
75
+ }
62
76
  }
63
77
  async function oxygenDeploy(options) {
64
78
  const {
79
+ environmentTag,
65
80
  path,
66
81
  shop,
67
82
  publicDeployment,
@@ -69,25 +84,69 @@ async function oxygenDeploy(options) {
69
84
  metadataUser,
70
85
  metadataVersion
71
86
  } = options;
72
- const token = await getOxygenDeploymentToken({
73
- root: path,
74
- flagShop: shop
75
- });
87
+ const ci = ciPlatform();
88
+ let token = options.token;
89
+ let branch;
90
+ let deploymentData;
91
+ let deploymentEnvironmentTag = void 0;
92
+ let gitCommit;
93
+ try {
94
+ gitCommit = await getLatestGitCommit(path);
95
+ branch = (/HEAD -> ([^,]*)/.exec(gitCommit.refs) || [])[1];
96
+ } catch (error) {
97
+ outputWarn("Could not retrieve Git history.");
98
+ branch = void 0;
99
+ }
100
+ if (!ci.isCI) {
101
+ deploymentData = await getOxygenDeploymentData({
102
+ root: path,
103
+ flagShop: shop
104
+ });
105
+ if (!deploymentData) {
106
+ return;
107
+ }
108
+ token = token || deploymentData.oxygenDeploymentToken;
109
+ }
76
110
  if (!token) {
77
- throw new AbortError("Could not obtain Oxygen deployment token");
111
+ const errMessage = ci.isCI ? [
112
+ "No deployment token provided. Use the ",
113
+ { command: "--token" },
114
+ " flag to provide a token."
115
+ ] : `Could not obtain an Oxygen deployment token, please try again or contact Shopify support.`;
116
+ throw new AbortError(errMessage);
117
+ }
118
+ if (!ci.isCI && !environmentTag && deploymentData?.environments) {
119
+ if (deploymentData.environments.length > 1) {
120
+ const choices = [
121
+ ...deploymentData.environments.map(({ name, branch: branch2 }) => ({
122
+ label: name,
123
+ value: branch2
124
+ }))
125
+ ];
126
+ deploymentEnvironmentTag = await renderSelectPrompt({
127
+ message: "Select an environment to deploy to",
128
+ choices,
129
+ defaultValue: branch
130
+ });
131
+ } else {
132
+ outputInfo(
133
+ `Using current checked out branch ${branch} as environment tag`
134
+ );
135
+ }
78
136
  }
79
137
  const config = {
80
138
  assetsDir: "dist/client",
81
139
  deploymentUrl: "https://oxygen.shopifyapps.com",
82
140
  deploymentToken: parseToken(token),
83
- healthCheckMaxDuration: 180,
141
+ environmentTag: environmentTag || deploymentEnvironmentTag || branch,
142
+ verificationMaxDuration: 180,
84
143
  metadata: {
85
144
  ...metadataUrl ? { url: metadataUrl } : {},
86
145
  ...metadataUser ? { user: metadataUser } : {},
87
146
  ...metadataVersion ? { version: metadataVersion } : {}
88
147
  },
89
148
  publicDeployment,
90
- skipHealthCheck: false,
149
+ skipVerification: false,
91
150
  rootPath: path,
92
151
  skipBuild: false,
93
152
  workerOnly: false,
@@ -120,10 +179,10 @@ async function oxygenDeploy(options) {
120
179
  useCodegen: false
121
180
  });
122
181
  },
123
- onHealthCheckComplete: () => resolveHealthCheck(),
182
+ onVerificationComplete: () => resolveHealthCheck(),
124
183
  onUploadFilesStart: () => uploadStart(),
125
184
  onUploadFilesComplete: () => resolveUpload(),
126
- onHealthCheckError: (error) => {
185
+ onVerificationError: (error) => {
127
186
  deployError = new AbortError(
128
187
  error.message,
129
188
  "Please verify the deployment status in the Shopify Admin and retry deploying if necessary."
@@ -146,7 +205,7 @@ async function oxygenDeploy(options) {
146
205
  task: async () => await uploadPromise
147
206
  },
148
207
  {
149
- title: "Performing health check",
208
+ title: "Verifying deployment",
150
209
  task: async () => await healthCheckPromise
151
210
  }
152
211
  ]);
@@ -1,13 +1,14 @@
1
- import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
1
+ import { vi, describe, expect, beforeEach, afterEach, it } from 'vitest';
2
2
  import { login } from '../../lib/auth.js';
3
3
  import { getStorefronts } from '../../lib/graphql/admin/link-storefront.js';
4
4
  import { AbortError } from '@shopify/cli-kit/node/error';
5
5
  import { renderSelectPrompt, renderSuccess, renderFatalError } from '@shopify/cli-kit/node/ui';
6
+ import { getLatestGitCommit } from '@shopify/cli-kit/node/git';
6
7
  import { oxygenDeploy, deploymentLogger } from './deploy.js';
7
- import { getOxygenDeploymentToken } from '../../lib/get-oxygen-token.js';
8
+ import { getOxygenDeploymentData } from '../../lib/get-oxygen-deployment-data.js';
8
9
  import { createDeploy, parseToken } from '@shopify/oxygen-cli/deploy';
9
10
 
10
- vi.mock("../../lib/get-oxygen-token.js");
11
+ vi.mock("../../lib/get-oxygen-deployment-data.js");
11
12
  vi.mock("@shopify/oxygen-cli/deploy");
12
13
  vi.mock("../../lib/auth.js");
13
14
  vi.mock("../../lib/shopify-config.js");
@@ -19,6 +20,8 @@ vi.mock("@shopify/cli-kit/node/output", async () => {
19
20
  return {
20
21
  outputContent: () => ({ value: "" }),
21
22
  outputInfo: () => {
23
+ },
24
+ outputWarn: () => {
22
25
  }
23
26
  };
24
27
  });
@@ -30,6 +33,16 @@ vi.mock("@shopify/cli-kit/node/ui", async () => {
30
33
  renderTasks: vi.fn()
31
34
  };
32
35
  });
36
+ vi.mock("@shopify/cli-kit/node/git", async () => {
37
+ return {
38
+ getLatestGitCommit: vi.fn()
39
+ };
40
+ });
41
+ vi.mock("@shopify/cli-kit/node/context/local", async () => {
42
+ return {
43
+ ciPlatform: () => ({ isCI: false })
44
+ };
45
+ });
33
46
  describe("deploy", () => {
34
47
  const ADMIN_SESSION = {
35
48
  token: "abc123",
@@ -66,6 +79,31 @@ describe("deploy", () => {
66
79
  namespace: "some-namespace",
67
80
  namespaceId: "1"
68
81
  };
82
+ const expectedConfig = {
83
+ assetsDir: "dist/client",
84
+ deploymentUrl: "https://oxygen.shopifyapps.com",
85
+ deploymentToken: mockToken,
86
+ verificationMaxDuration: 180,
87
+ metadata: {
88
+ url: deployParams.metadataUrl,
89
+ user: deployParams.metadataUser,
90
+ version: deployParams.metadataVersion
91
+ },
92
+ publicDeployment: deployParams.publicDeployment,
93
+ skipVerification: false,
94
+ rootPath: deployParams.path,
95
+ skipBuild: false,
96
+ workerOnly: false,
97
+ workerDir: "dist/worker"
98
+ };
99
+ const expectedHooks = {
100
+ buildFunction: expect.any(Function),
101
+ onVerificationComplete: expect.any(Function),
102
+ onUploadFilesStart: expect.any(Function),
103
+ onUploadFilesComplete: expect.any(Function),
104
+ onVerificationError: expect.any(Function),
105
+ onUploadFilesError: expect.any(Function)
106
+ };
69
107
  beforeEach(async () => {
70
108
  process.exit = vi.fn();
71
109
  vi.mocked(login).mockResolvedValue({
@@ -83,54 +121,68 @@ describe("deploy", () => {
83
121
  vi.mocked(createDeploy).mockResolvedValue(
84
122
  "https://a-lovely-deployment.com"
85
123
  );
86
- vi.mocked(getOxygenDeploymentToken).mockResolvedValue("some-encoded-token");
124
+ vi.mocked(getOxygenDeploymentData).mockResolvedValue({
125
+ oxygenDeploymentToken: "some-encoded-token",
126
+ environments: []
127
+ });
87
128
  vi.mocked(parseToken).mockReturnValue(mockToken);
88
129
  });
89
130
  afterEach(() => {
90
131
  vi.resetAllMocks();
91
132
  process.exit = originalExit;
92
133
  });
93
- it("calls getOxygenDeploymentToken with the correct parameters", async () => {
134
+ it("calls getOxygenDeploymentData with the correct parameters", async () => {
94
135
  await oxygenDeploy(deployParams);
95
- expect(getOxygenDeploymentToken).toHaveBeenCalledWith({
136
+ expect(getOxygenDeploymentData).toHaveBeenCalledWith({
96
137
  root: "./",
97
138
  flagShop: "snowdevil.myshopify.com"
98
139
  });
99
- expect(getOxygenDeploymentToken).toHaveBeenCalledTimes(1);
140
+ expect(getOxygenDeploymentData).toHaveBeenCalledTimes(1);
100
141
  });
101
142
  it("calls createDeploy with the correct parameters", async () => {
102
143
  await oxygenDeploy(deployParams);
103
- const expectedConfig = {
104
- assetsDir: "dist/client",
105
- deploymentUrl: "https://oxygen.shopifyapps.com",
106
- deploymentToken: mockToken,
107
- healthCheckMaxDuration: 180,
108
- metadata: {
109
- url: deployParams.metadataUrl,
110
- user: deployParams.metadataUser,
111
- version: deployParams.metadataVersion
112
- },
113
- publicDeployment: deployParams.publicDeployment,
114
- skipHealthCheck: false,
115
- rootPath: deployParams.path,
116
- skipBuild: false,
117
- workerOnly: false,
118
- workerDir: "dist/worker"
119
- };
120
144
  expect(vi.mocked(createDeploy)).toHaveBeenCalledWith({
121
145
  config: expectedConfig,
122
- hooks: {
123
- buildFunction: expect.any(Function),
124
- onHealthCheckComplete: expect.any(Function),
125
- onUploadFilesStart: expect.any(Function),
126
- onUploadFilesComplete: expect.any(Function),
127
- onHealthCheckError: expect.any(Function),
128
- onUploadFilesError: expect.any(Function)
129
- },
146
+ hooks: expectedHooks,
147
+ logger: deploymentLogger
148
+ });
149
+ expect(vi.mocked(renderSuccess)).toHaveBeenCalled;
150
+ });
151
+ it("calls createDeploy with the checked out branch name", async () => {
152
+ vi.mocked(getLatestGitCommit).mockResolvedValue({
153
+ hash: "123",
154
+ message: "test commit",
155
+ date: "2021-01-01",
156
+ author_name: "test author",
157
+ author_email: "test@author.com",
158
+ body: "test body",
159
+ refs: "HEAD -> main"
160
+ });
161
+ await oxygenDeploy(deployParams);
162
+ expect(vi.mocked(createDeploy)).toHaveBeenCalledWith({
163
+ config: { ...expectedConfig, environmentTag: "main" },
164
+ hooks: expectedHooks,
130
165
  logger: deploymentLogger
131
166
  });
132
167
  expect(vi.mocked(renderSuccess)).toHaveBeenCalled;
133
168
  });
169
+ it("calls renderSelectPrompt when there are multiple environments", async () => {
170
+ vi.mocked(getOxygenDeploymentData).mockResolvedValue({
171
+ oxygenDeploymentToken: "some-encoded-token",
172
+ environments: [
173
+ { name: "production", branch: "main" },
174
+ { name: "preview", branch: "staging" }
175
+ ]
176
+ });
177
+ await oxygenDeploy(deployParams);
178
+ expect(vi.mocked(renderSelectPrompt)).toHaveBeenCalledWith({
179
+ message: "Select an environment to deploy to",
180
+ choices: [
181
+ { label: "production", value: "main" },
182
+ { label: "preview", value: "staging" }
183
+ ]
184
+ });
185
+ });
134
186
  it("handles error during uploadFiles", async () => {
135
187
  const mockRenderFatalError = vi.fn();
136
188
  vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
@@ -156,14 +208,14 @@ describe("deploy", () => {
156
208
  }
157
209
  }
158
210
  });
159
- it("handles error during health check", async () => {
211
+ it("handles error during deployment verification", async () => {
160
212
  const mockRenderFatalError = vi.fn();
161
213
  vi.mocked(renderFatalError).mockImplementation(mockRenderFatalError);
162
214
  const error = new Error("Cloudflare is down!");
163
215
  vi.mocked(createDeploy).mockImplementation((options) => {
164
216
  options.hooks?.onUploadFilesStart?.();
165
217
  options.hooks?.onUploadFilesComplete?.();
166
- options.hooks?.onHealthCheckError?.(error);
218
+ options.hooks?.onVerificationError?.(error);
167
219
  return new Promise((_resolve, reject) => {
168
220
  reject(error);
169
221
  });
@@ -18,6 +18,7 @@ import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
18
18
  import { getConfig } from '../../lib/shopify-config.js';
19
19
  import { setupLiveReload } from '../../lib/live-reload.js';
20
20
  import { checkRemixVersions } from '../../lib/remix-version-check.js';
21
+ import { getGraphiQLUrl } from '../../lib/graphiql-url.js';
21
22
 
22
23
  const LOG_REBUILDING = "\u{1F9F1} Rebuilding...";
23
24
  const LOG_REBUILT = "\u{1F680} Rebuilt";
@@ -121,16 +122,19 @@ async function runDev({
121
122
  },
122
123
  workerRuntime
123
124
  );
124
- const graphiqlUrl = `${miniOxygen.listeningAt}/graphiql`;
125
125
  const debugNetworkUrl = `${miniOxygen.listeningAt}/debug-network`;
126
- enhanceH2Logs({ graphiqlUrl, ...remixConfig });
126
+ enhanceH2Logs({ host: miniOxygen.listeningAt, ...remixConfig });
127
127
  miniOxygen.showBanner({
128
128
  appName: storefront ? colors.cyan(storefront?.title) : void 0,
129
129
  headlinePrefix: initialBuildDurationMs > 0 ? `Initial build: ${initialBuildDurationMs}ms
130
130
  ` : "",
131
131
  extraLines: [
132
- colors.dim(`
133
- View GraphiQL API browser: ${graphiqlUrl}`),
132
+ colors.dim(
133
+ `
134
+ View GraphiQL API browser: ${getGraphiQLUrl({
135
+ host: miniOxygen.listeningAt
136
+ })}`
137
+ ),
134
138
  workerRuntime ? "" : colors.dim(
135
139
  `
136
140
  View server-side network requests: ${debugNetworkUrl}`
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * A side bar component with Overlay that works without JavaScript.
3
3
  * @example
4
- * ```ts
5
- * <Aside id="search-aside" heading="SEARCH">`
4
+ * ```jsx
5
+ * <Aside id="search-aside" heading="SEARCH">
6
6
  * <input type="search" />
7
7
  * ...
8
8
  * </Aside>
@@ -450,13 +450,11 @@ function usePredictiveSearch(): UseSearchReturn {
450
450
 
451
451
  /**
452
452
  * Converts a plural search type to a singular search type
453
- * @param type - The plural search type
454
- * @returns The singular search type
455
453
  *
456
454
  * @example
457
- * ```ts
458
- * pluralToSingularSearchType('articles') // => 'ARTICLE'
459
- * pluralToSingularSearchType(['articles', 'products']) // => 'ARTICLE,PRODUCT'
455
+ * ```js
456
+ * pluralToSingularSearchType('articles'); // => 'ARTICLE'
457
+ * pluralToSingularSearchType(['articles', 'products']); // => 'ARTICLE,PRODUCT'
460
458
  * ```
461
459
  */
462
460
  function pluralToSingularSearchType(
@@ -189,14 +189,13 @@ export function CatchBoundary() {
189
189
  * @see https://shopify.dev/docs/api/storefront/latest/objects/CustomerAccessToken
190
190
  *
191
191
  * @example
192
- * ```ts
193
- * //
192
+ * ```js
194
193
  * const {isLoggedIn, headers} = await validateCustomerAccessToken(
195
194
  * customerAccessToken,
196
195
  * session,
197
- * );
198
- * ```
199
- * */
196
+ * );
197
+ * ```
198
+ */
200
199
  async function validateCustomerAccessToken(
201
200
  session: HydrogenSession,
202
201
  customerAccessToken?: CustomerAccessToken,
@@ -40,7 +40,8 @@ function FeaturedCollection({
40
40
  }: {
41
41
  collection: FeaturedCollectionFragment;
42
42
  }) {
43
- const image = collection.image;
43
+ if (!collection) return null;
44
+ const image = collection?.image;
44
45
  return (
45
46
  <Link
46
47
  className="featured-collection"
@@ -108,8 +108,6 @@ async function fetchPredictiveSearchResults({
108
108
 
109
109
  /**
110
110
  * Normalize results and apply tracking qurery parameters to each result url
111
- * @param predictiveSearch
112
- * @param locale
113
111
  */
114
112
  export function normalizePredictiveSearchResults(
115
113
  predictiveSearch: PredictiveSearchQuery['predictiveSearch'],
@@ -3,21 +3,20 @@ import {redirect, type LoaderArgs} from '@shopify/remix-oxygen';
3
3
  /**
4
4
  * Automatically creates a new cart based on the URL and redirects straight to checkout.
5
5
  * Expected URL structure:
6
- * ```ts
6
+ * ```js
7
7
  * /cart/<variant_id>:<quantity>
8
8
  *
9
9
  * ```
10
+ *
10
11
  * More than one `<variant_id>:<quantity>` separated by a comma, can be supplied in the URL, for
11
12
  * carts with more than one product variant.
12
13
  *
13
- * @param `?discount` an optional discount code to apply to the cart
14
14
  * @example
15
- * Example path creating a cart with two product variants, different quantities, and a discount code:
16
- * ```ts
15
+ * Example path creating a cart with two product variants, different quantities, and a discount code in the querystring:
16
+ * ```js
17
17
  * /cart/41007289663544:1,41007289696312:2?discount=HYDROBOARD
18
18
  *
19
19
  * ```
20
- * @preserve
21
20
  */
22
21
  export async function loader({request, context, params}: LoaderArgs) {
23
22
  const {cart} = context;
@@ -66,7 +66,7 @@ function CollectionItem({
66
66
  to={`/collections/${collection.handle}`}
67
67
  prefetch="intent"
68
68
  >
69
- {collection.image && (
69
+ {collection?.image && (
70
70
  <Image
71
71
  alt={collection.image.altText || collection.title}
72
72
  aspectRatio="1/1"
@@ -3,14 +3,13 @@ import {redirect, type LoaderArgs} from '@shopify/remix-oxygen';
3
3
  /**
4
4
  * Automatically applies a discount found on the url
5
5
  * If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied
6
- * @param ?redirect an optional path to return to otherwise return to the home page
6
+ *
7
7
  * @example
8
- * Example path applying a discount and redirecting
9
- * ```ts
8
+ * Example path applying a discount and optional redirecting (defaults to the home page)
9
+ * ```js
10
10
  * /discount/FREESHIPPING?redirect=/products
11
11
  *
12
12
  * ```
13
- * @preserve
14
13
  */
15
14
  export async function loader({request, context, params}: LoaderArgs) {
16
15
  const {cart} = context;
@@ -15,9 +15,9 @@
15
15
  "dependencies": {
16
16
  "@remix-run/react": "1.19.1",
17
17
  "@shopify/cli": "3.49.2",
18
- "@shopify/cli-hydrogen": "^5.3.1",
19
- "@shopify/hydrogen": "^2023.7.8",
20
- "@shopify/remix-oxygen": "^1.1.4",
18
+ "@shopify/cli-hydrogen": "^5.4.1",
19
+ "@shopify/hydrogen": "^2023.7.9",
20
+ "@shopify/remix-oxygen": "^1.1.5",
21
21
  "graphql": "^16.6.0",
22
22
  "graphql-tag": "^2.12.6",
23
23
  "isbot": "^3.6.6",
@@ -30,7 +30,7 @@
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.2.20",
33
+ "@types/react": "^18.2.22",
34
34
  "@types/react-dom": "^18.2.7",
35
35
  "eslint": "^8.20.0",
36
36
  "eslint-plugin-hydrogen": "0.12.2",