@shopify/cli-hydrogen 7.1.2 → 8.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.
Files changed (145) hide show
  1. package/dist/commands/hydrogen/build-vite.js +19 -10
  2. package/dist/commands/hydrogen/build.js +10 -2
  3. package/dist/commands/hydrogen/check.js +1 -0
  4. package/dist/commands/hydrogen/codegen.js +1 -0
  5. package/dist/commands/hydrogen/customer-account/push.js +170 -0
  6. package/dist/commands/hydrogen/debug/cpu.js +10 -1
  7. package/dist/commands/hydrogen/deploy.js +110 -36
  8. package/dist/commands/hydrogen/dev-vite.js +128 -59
  9. package/dist/commands/hydrogen/dev.js +108 -51
  10. package/dist/commands/hydrogen/env/list.js +20 -28
  11. package/dist/commands/hydrogen/env/pull.js +29 -19
  12. package/dist/commands/hydrogen/env/{push__unstable.js → push.js} +34 -68
  13. package/dist/commands/hydrogen/generate/route.js +1 -0
  14. package/dist/commands/hydrogen/init.js +39 -21
  15. package/dist/commands/hydrogen/link.js +25 -6
  16. package/dist/commands/hydrogen/list.js +1 -0
  17. package/dist/commands/hydrogen/login.js +1 -0
  18. package/dist/commands/hydrogen/logout.js +1 -0
  19. package/dist/commands/hydrogen/preview.js +31 -16
  20. package/dist/commands/hydrogen/setup/css.js +8 -1
  21. package/dist/commands/hydrogen/setup/markets.js +1 -0
  22. package/dist/commands/hydrogen/setup/vite.js +244 -138
  23. package/dist/commands/hydrogen/setup.js +21 -23
  24. package/dist/commands/hydrogen/shortcut.js +10 -0
  25. package/dist/commands/hydrogen/unlink.js +1 -0
  26. package/dist/commands/hydrogen/upgrade.js +2 -1
  27. package/dist/generator-templates/assets/vite/package.json +3 -4
  28. package/dist/generator-templates/assets/vite/vite.config.js +15 -2
  29. package/dist/generator-templates/starter/CHANGELOG.md +129 -0
  30. package/dist/generator-templates/starter/README.md +3 -44
  31. package/dist/generator-templates/starter/app/components/Footer.tsx +1 -1
  32. package/dist/generator-templates/starter/app/components/Header.tsx +1 -1
  33. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +1 -0
  34. package/dist/generator-templates/starter/app/lib/fragments.ts +2 -0
  35. package/dist/generator-templates/starter/app/lib/root-data.ts +11 -0
  36. package/dist/generator-templates/starter/app/root.tsx +4 -20
  37. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +1 -1
  38. package/dist/generator-templates/starter/app/routes/account.tsx +1 -1
  39. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +3 -3
  40. package/dist/generator-templates/starter/app/routes/cart.tsx +1 -1
  41. package/dist/generator-templates/starter/app/routes/collections.all.tsx +160 -0
  42. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +1 -2
  43. package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +6 -3
  44. package/dist/generator-templates/starter/{remix.env.d.ts → env.d.ts} +8 -2
  45. package/dist/generator-templates/starter/package.json +14 -9
  46. package/dist/generator-templates/starter/server.ts +2 -1
  47. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +59 -3
  48. package/dist/generator-templates/starter/vite.config.ts +26 -0
  49. package/dist/{commands/hydrogen/init.d.ts → init.d.ts} +11 -4
  50. package/dist/lib/check-lockfile.js +12 -18
  51. package/dist/lib/codegen.js +37 -13
  52. package/dist/lib/common.js +50 -0
  53. package/dist/lib/cpu-profiler.js +4 -1
  54. package/dist/lib/dev-shared.js +99 -0
  55. package/dist/lib/environment-variables.js +51 -30
  56. package/dist/lib/file.js +8 -1
  57. package/dist/lib/flags.js +37 -26
  58. package/dist/lib/get-oxygen-deployment-data.js +10 -17
  59. package/dist/lib/graphql/admin/customer-application-update.js +29 -0
  60. package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
  61. package/dist/lib/graphql/admin/list-environments.js +1 -0
  62. package/dist/lib/graphql/admin/pull-variables.js +4 -4
  63. package/dist/lib/graphql/admin/test-helper.js +37 -0
  64. package/dist/lib/log.js +86 -13
  65. package/dist/lib/mini-oxygen/common.js +19 -33
  66. package/dist/lib/mini-oxygen/index.js +6 -2
  67. package/dist/lib/mini-oxygen/node.js +43 -31
  68. package/dist/lib/mini-oxygen/workerd.js +72 -165
  69. package/dist/lib/missing-routes.js +1 -1
  70. package/dist/lib/onboarding/common.js +85 -70
  71. package/dist/lib/onboarding/local.js +19 -9
  72. package/dist/lib/onboarding/remote.js +35 -30
  73. package/dist/lib/package-managers.js +24 -0
  74. package/dist/lib/remix-config.js +17 -1
  75. package/dist/lib/render-errors.js +17 -10
  76. package/dist/lib/request-events.js +6 -1
  77. package/dist/lib/setups/i18n/replacers.js +9 -6
  78. package/dist/lib/setups/routes/generate.js +1 -0
  79. package/dist/lib/shell.js +2 -1
  80. package/dist/lib/shopify-config.js +19 -1
  81. package/dist/lib/template-diff.js +36 -15
  82. package/dist/lib/template-downloader.js +35 -5
  83. package/dist/lib/transpile/morph/functions.js +26 -8
  84. package/dist/lib/transpile/morph/typedefs.js +6 -4
  85. package/dist/lib/transpile/project.js +8 -4
  86. package/dist/lib/tunneling.js +44 -0
  87. package/dist/lib/verify-linked-storefront.js +24 -0
  88. package/dist/lib/virtual-routes.js +1 -1
  89. package/dist/lib/vite-config.js +39 -9
  90. package/oclif.manifest.json +704 -508
  91. package/package.json +32 -24
  92. package/dist/commands/hydrogen/deploy.test.js +0 -553
  93. package/dist/commands/hydrogen/env/list.test.js +0 -148
  94. package/dist/commands/hydrogen/env/pull.test.js +0 -207
  95. package/dist/commands/hydrogen/env/push__unstable.test.js +0 -383
  96. package/dist/commands/hydrogen/generate/route.test.js +0 -43
  97. package/dist/commands/hydrogen/init.test.js +0 -641
  98. package/dist/commands/hydrogen/link.test.js +0 -187
  99. package/dist/commands/hydrogen/list.test.js +0 -111
  100. package/dist/commands/hydrogen/setup.test.js +0 -61
  101. package/dist/commands/hydrogen/shortcut.test.js +0 -30
  102. package/dist/commands/hydrogen/unlink.test.js +0 -36
  103. package/dist/commands/hydrogen/upgrade.test.js +0 -786
  104. package/dist/generator-templates/starter/remix.config.js +0 -24
  105. package/dist/lib/auth.test.js +0 -157
  106. package/dist/lib/check-lockfile.test.js +0 -81
  107. package/dist/lib/check-version.test.js +0 -86
  108. package/dist/lib/environment-variables.test.js +0 -149
  109. package/dist/lib/file.test.js +0 -68
  110. package/dist/lib/flags.test.js +0 -43
  111. package/dist/lib/get-oxygen-deployment-data.test.js +0 -120
  112. package/dist/lib/gid.test.js +0 -15
  113. package/dist/lib/graphql/admin/client.test.js +0 -76
  114. package/dist/lib/graphql/admin/create-storefront.test.js +0 -64
  115. package/dist/lib/graphql/admin/link-storefront.test.js +0 -38
  116. package/dist/lib/graphql/admin/list-environments.test.js +0 -44
  117. package/dist/lib/graphql/admin/list-storefronts.test.js +0 -44
  118. package/dist/lib/graphql/admin/pull-variables.test.js +0 -43
  119. package/dist/lib/graphql/business-platform/user-account.test.js +0 -80
  120. package/dist/lib/log.test.js +0 -92
  121. package/dist/lib/mini-oxygen/assets.js +0 -134
  122. package/dist/lib/mini-oxygen/mini-oxygen.test.js +0 -214
  123. package/dist/lib/mini-oxygen/workerd-inspector-logs.js +0 -227
  124. package/dist/lib/mini-oxygen/workerd-inspector-proxy.js +0 -200
  125. package/dist/lib/mini-oxygen/workerd-inspector.js +0 -219
  126. package/dist/lib/missing-routes.test.js +0 -45
  127. package/dist/lib/remix-version-check.test.js +0 -39
  128. package/dist/lib/remix-version-interop.test.js +0 -13
  129. package/dist/lib/setups/i18n/domains.test.js +0 -39
  130. package/dist/lib/setups/i18n/replacers.test.js +0 -261
  131. package/dist/lib/setups/i18n/subdomains.test.js +0 -39
  132. package/dist/lib/setups/i18n/subfolders.test.js +0 -39
  133. package/dist/lib/setups/routes/generate.test.js +0 -296
  134. package/dist/lib/shell.test.js +0 -111
  135. package/dist/lib/shopify-config.test.js +0 -199
  136. package/dist/lib/string.test.js +0 -16
  137. package/dist/lib/virtual-routes.test.js +0 -49
  138. package/dist/lib/vite/hydrogen-middleware.js +0 -82
  139. package/dist/lib/vite/mini-oxygen.js +0 -152
  140. package/dist/lib/vite/plugins.d.ts +0 -27
  141. package/dist/lib/vite/plugins.js +0 -139
  142. package/dist/lib/vite/shared.js +0 -10
  143. package/dist/lib/vite/utils.js +0 -55
  144. package/dist/lib/vite/worker-entry.js +0 -1518
  145. /package/dist/generator-templates/starter/{.eslintrc.js → .eslintrc.cjs} +0 -0
@@ -0,0 +1,24 @@
1
+ const packageManagers = [
2
+ {
3
+ name: "npm",
4
+ lockfile: "package-lock.json",
5
+ installCommand: "npm ci"
6
+ },
7
+ {
8
+ name: "yarn",
9
+ lockfile: "yarn.lock",
10
+ installCommand: "yarn install --frozen-lockfile"
11
+ },
12
+ {
13
+ name: "pnpm",
14
+ lockfile: "pnpm-lock.yaml",
15
+ installCommand: "pnpm install --frozen-lockfile"
16
+ },
17
+ {
18
+ name: "bun",
19
+ lockfile: "bun.lockb",
20
+ installCommand: "bun install --frozen-lockfile"
21
+ }
22
+ ];
23
+
24
+ export { packageManagers };
@@ -7,7 +7,13 @@ import { outputWarn } from '@shopify/cli-kit/node/output';
7
7
  import { fileExists } from '@shopify/cli-kit/node/fs';
8
8
  import { muteRemixLogs } from './log.js';
9
9
  import { getRequiredRemixVersion } from './remix-version-check.js';
10
+ import { findFileWithExtension } from './file.js';
11
+ import { getViteConfig } from './vite-config.js';
10
12
 
13
+ async function hasRemixConfigFile(root) {
14
+ const result = await findFileWithExtension(root, "remix.config");
15
+ return !!result.filepath;
16
+ }
11
17
  const BUILD_DIR = "dist";
12
18
  const CLIENT_SUBDIR = "client";
13
19
  const WORKER_SUBDIR = "worker";
@@ -33,7 +39,17 @@ function handleRemixImportFail() {
33
39
  `Please make sure you have \`@remix-run/dev@${remixVersion}\` installed and all the other Remix packages have the same version.`
34
40
  );
35
41
  }
42
+ function getRawRemixConfig(root) {
43
+ return findFileWithExtension(root, "remix.config").then(({ filepath }) => {
44
+ if (!filepath)
45
+ throw new AbortError("No remix.config.js file found.");
46
+ return createRequire(import.meta.url)(filepath);
47
+ });
48
+ }
36
49
  async function getRemixConfig(root, mode = process.env.NODE_ENV) {
50
+ if (!await hasRemixConfigFile(root)) {
51
+ return (await getViteConfig(root)).remixConfig;
52
+ }
37
53
  await muteRemixLogs();
38
54
  const { readConfig } = await import('@remix-run/dev/dist/config.js').catch(
39
55
  handleRemixImportFail
@@ -144,4 +160,4 @@ async function assertEntryFileExists(root, fileRelative) {
144
160
  }
145
161
  }
146
162
 
147
- export { assertOxygenChecks, getProjectPaths, getRemixConfig, handleRemixImportFail };
163
+ export { assertOxygenChecks, getProjectPaths, getRawRemixConfig, getRemixConfig, handleRemixImportFail, hasRemixConfigFile };
@@ -1,4 +1,4 @@
1
- import { renderFatalError } from '@shopify/cli-kit/node/ui';
1
+ import { renderFatalError, renderInfo } from '@shopify/cli-kit/node/ui';
2
2
  import { outputContent, outputToken } from '@shopify/cli-kit/node/output';
3
3
  import { hydrogenStorefrontsUrl } from './admin-urls.js';
4
4
  import { parseGid } from './gid.js';
@@ -25,15 +25,22 @@ function renderMissingStorefront({
25
25
  )}`.value
26
26
  });
27
27
  }
28
- function renderMissingLink({ session, cliCommand }) {
29
- renderFatalError({
30
- name: "NoLinkedStorefrontError",
31
- type: 0,
32
- message: `No linked Hydrogen storefront on ${session.storeFqdn}`,
33
- skipOclifErrorHandling: true,
34
- tryMessage: [
35
- "To pull environment variables or to deploy to Oxygen, link this project to a Hydrogen storefront. To select a storefront to link, run",
36
- { command: `${cliCommand} link` }
28
+ function renderMissingLink({ noStorefronts = false }) {
29
+ const headline = noStorefronts ? "You don't have a Hydrogen storefront to link to" : "You haven't linked your project to a storefront yet";
30
+ renderInfo({
31
+ headline,
32
+ body: [
33
+ "Link your local environment to a Hydrogen storefront. Enable automatic environment variable injection and access to",
34
+ { command: "env list" },
35
+ ",",
36
+ { command: "env pull" },
37
+ ",",
38
+ { command: "env push" },
39
+ ", and",
40
+ { command: "deploy" },
41
+ "commands. Use",
42
+ { command: `unlink` },
43
+ "to disconnect from the storefront."
37
44
  ]
38
45
  });
39
46
  }
@@ -9,7 +9,12 @@ let ResponseConstructor;
9
9
  function setConstructors(constructors) {
10
10
  ResponseConstructor = constructors.Response;
11
11
  }
12
- const DEV_ROUTES = /* @__PURE__ */ new Set(["/graphiql", "/subrequest-profiler"]);
12
+ const DEV_ROUTES = /* @__PURE__ */ new Set([
13
+ "/graphiql",
14
+ "/graphiql/customer-account.schema.json",
15
+ "/subrequest-profiler",
16
+ "/debug-network-server"
17
+ ]);
13
18
  const EVENT_MAP = {
14
19
  request: "Request",
15
20
  subrequest: "Sub request"
@@ -1,5 +1,5 @@
1
1
  import { AbortError } from '@shopify/cli-kit/node/error';
2
- import { joinPath } from '@shopify/cli-kit/node/path';
2
+ import { joinPath, resolvePath } from '@shopify/cli-kit/node/path';
3
3
  import { fileExists } from '@shopify/cli-kit/node/fs';
4
4
  import { replaceFileContent, findFileWithExtension } from '../../file.js';
5
5
  import { importLangAstGrep } from '../../ast.js';
@@ -147,9 +147,12 @@ ${localeExtractFn}
147
147
  });
148
148
  }
149
149
  async function replaceRemixEnv({ rootDirectory }, formatConfig, localeExtractImpl) {
150
- const remixEnvPath = joinPath(rootDirectory, "remix.env.d.ts");
151
- if (!await fileExists(remixEnvPath)) {
152
- return;
150
+ let envPath = joinPath(rootDirectory, "env.d.ts");
151
+ if (!await fileExists(envPath)) {
152
+ envPath = "remix.env.d.ts";
153
+ if (!await fileExists(envPath)) {
154
+ return;
155
+ }
153
156
  }
154
157
  const i18nType = localeExtractImpl.match(
155
158
  /^(export )?(type \w+ =\s+\{.*?\};)\n/ms
@@ -158,7 +161,7 @@ async function replaceRemixEnv({ rootDirectory }, formatConfig, localeExtractImp
158
161
  if (!i18nTypeName) {
159
162
  return;
160
163
  }
161
- await replaceFileContent(remixEnvPath, formatConfig, async (content) => {
164
+ await replaceFileContent(envPath, formatConfig, async (content) => {
162
165
  if (content.includes(`Storefront<`))
163
166
  return;
164
167
  const astGrep = await importLangAstGrep("ts");
@@ -237,7 +240,7 @@ async function findEntryFile({
237
240
  serverEntryPoint = "server"
238
241
  }) {
239
242
  const match = serverEntryPoint.match(/\.([jt]sx?)$/)?.[1];
240
- const { filepath, astType } = match ? { filepath: joinPath(rootDirectory, serverEntryPoint), astType: match } : await findFileWithExtension(rootDirectory, serverEntryPoint);
243
+ const { filepath, astType } = match ? { filepath: resolvePath(rootDirectory, serverEntryPoint), astType: match } : await findFileWithExtension(rootDirectory, serverEntryPoint);
241
244
  if (!filepath || !astType) {
242
245
  throw new AbortError(
243
246
  `Could not find a server entry point at ${serverEntryPoint}`
@@ -199,6 +199,7 @@ async function findRouteDependencies(routeFilePath, appDirectory) {
199
199
  for (let [, , match] of importMatches) {
200
200
  if (!match || !/^(\.|~)/.test(match))
201
201
  continue;
202
+ match = match.replace(/\?[a-z.]+$/, "");
202
203
  match = match.replace(
203
204
  "~",
204
205
  // import from '~/components/...'
package/dist/lib/shell.js CHANGED
@@ -113,7 +113,8 @@ async function createPlatformShortcut() {
113
113
  }
114
114
  const BASH_ZSH_COMMAND = `
115
115
  # Shopify Hydrogen alias to local projects
116
- alias ${ALIAS_NAME}='$(npm prefix -s)/node_modules/.bin/shopify hydrogen'`;
116
+ alias ${ALIAS_NAME}='$(npm prefix -s)/node_modules/.bin/shopify hydrogen'
117
+ `;
117
118
  const FISH_FUNCTION = `
118
119
  function ${ALIAS_NAME} --wraps='shopify hydrogen' --description 'Shortcut for the Hydrogen CLI'
119
120
  set npmPrefix (npm prefix -s)
@@ -87,5 +87,23 @@ async function ensureShopifyGitIgnore(root) {
87
87
  return false;
88
88
  }
89
89
  }
90
+ async function setCustomerAccountConfig(root, customerAccountConfig) {
91
+ try {
92
+ const filePath = resolvePath(root, SHOPIFY_DIR, SHOPIFY_DIR_PROJECT);
93
+ const existingConfig = JSON.parse(await readFile(filePath));
94
+ const config = {
95
+ ...existingConfig,
96
+ storefront: {
97
+ ...existingConfig.storefront,
98
+ customerAccountConfig
99
+ }
100
+ };
101
+ await writeFile(filePath, JSON.stringify(config));
102
+ await ensureShopifyGitIgnore(root);
103
+ return config;
104
+ } catch {
105
+ throw new AbortError("Project configuration could not be found.");
106
+ }
107
+ }
90
108
 
91
- export { SHOPIFY_DIR, SHOPIFY_DIR_PROJECT, ensureShopifyGitIgnore, getConfig, resetConfig, setStorefront, setUserAccount, unsetStorefront };
109
+ export { SHOPIFY_DIR, SHOPIFY_DIR_PROJECT, ensureShopifyGitIgnore, getConfig, resetConfig, setCustomerAccountConfig, setStorefront, setUserAccount, unsetStorefront };
@@ -1,7 +1,7 @@
1
1
  import { rmdirSync } from 'node:fs';
2
2
  import { temporaryDirectory } from 'tempy';
3
- import { createSymlink, copy, remove } from 'fs-extra/esm';
4
- import { copyFile, removeFile } from '@shopify/cli-kit/node/fs';
3
+ import { createSymlink, copy } from 'fs-extra/esm';
4
+ import { copyFile, removeFile, fileExists } from '@shopify/cli-kit/node/fs';
5
5
  import { joinPath, relativePath } from '@shopify/cli-kit/node/path';
6
6
  import { readAndParsePackageJson } from '@shopify/cli-kit/node/node-package-manager';
7
7
  import colors from '@shopify/cli-kit/node/colors';
@@ -66,9 +66,10 @@ ${colors.dim(
66
66
  return targetDirectory;
67
67
  }
68
68
  async function applyTemplateDiff(targetDirectory, diffDirectory, templateDir = getStarterDir()) {
69
- const pkgJson = await readAndParsePackageJson(
69
+ const diffPkgJson = await readAndParsePackageJson(
70
70
  joinPath(diffDirectory, "package.json")
71
71
  );
72
+ const diffOptions = diffPkgJson["h2:diff"] ?? {};
72
73
  const createFilter = (re, skipFiles) => (filepath) => {
73
74
  const filename = relativePath(templateDir, filepath);
74
75
  return !re.test(filename) && !skipFiles?.includes(filename);
@@ -76,7 +77,7 @@ async function applyTemplateDiff(targetDirectory, diffDirectory, templateDir = g
76
77
  await copy(templateDir, targetDirectory, {
77
78
  filter: createFilter(
78
79
  /(^|\/|\\)(dist|node_modules|\.cache|.turbo|CHANGELOG\.md)(\/|\\|$)/i,
79
- pkgJson["h2:diff"]?.["skip-files"] || []
80
+ diffOptions.skipFiles || []
80
81
  )
81
82
  });
82
83
  await copy(diffDirectory, targetDirectory, {
@@ -86,29 +87,49 @@ async function applyTemplateDiff(targetDirectory, diffDirectory, templateDir = g
86
87
  });
87
88
  await mergePackageJson(diffDirectory, targetDirectory, {
88
89
  ignoredKeys: ["h2:diff"],
89
- onResult: (pkgJson2) => {
90
+ onResult: (pkgJson) => {
90
91
  for (const key of ["build", "dev"]) {
91
- const scriptLine = pkgJson2.scripts?.[key];
92
- if (pkgJson2.scripts?.[key] && typeof scriptLine === "string") {
93
- pkgJson2.scripts[key] = scriptLine.replace(/\s+--diff/, "");
92
+ const scriptLine = pkgJson.scripts?.[key];
93
+ if (pkgJson.scripts?.[key] && typeof scriptLine === "string") {
94
+ pkgJson.scripts[key] = scriptLine.replace(/\s+--diff/, "");
94
95
  }
95
96
  }
96
- return pkgJson2;
97
+ if (diffOptions.skipDependencies && pkgJson.dependencies) {
98
+ for (const dep of diffOptions.skipDependencies) {
99
+ delete pkgJson.dependencies[dep];
100
+ }
101
+ }
102
+ if (diffOptions.skipDevDependencies && pkgJson.devDependencies) {
103
+ for (const devDep of diffOptions.skipDevDependencies) {
104
+ delete pkgJson.devDependencies[devDep];
105
+ }
106
+ }
107
+ return pkgJson;
97
108
  }
98
109
  });
99
110
  }
100
- async function copyDiffBuild(targetDirectory, diffDirectory) {
101
- const targetDist = joinPath(diffDirectory, "dist");
102
- await remove(targetDist);
111
+ async function copyDiffBuild(generatedDirectory, diffDirectory) {
112
+ const target = joinPath(diffDirectory, "dist");
113
+ await removeFile(target);
103
114
  await Promise.all([
104
- copy(joinPath(targetDirectory, "dist"), targetDist, {
115
+ copy(joinPath(generatedDirectory, "dist"), target, {
105
116
  overwrite: true
106
117
  }),
107
118
  copyFile(
108
- joinPath(targetDirectory, ".env"),
119
+ joinPath(generatedDirectory, ".env"),
109
120
  joinPath(diffDirectory, ".env")
110
121
  )
111
122
  ]);
112
123
  }
124
+ async function copyShopifyConfig(generatedDirectory, diffDirectory) {
125
+ const source = joinPath(generatedDirectory, ".shopify");
126
+ if (!await fileExists(source))
127
+ return;
128
+ const target = joinPath(diffDirectory, ".shopify");
129
+ await removeFile(target);
130
+ await copy(joinPath(generatedDirectory, ".shopify"), target, {
131
+ overwrite: true
132
+ });
133
+ }
113
134
 
114
- export { applyTemplateDiff, copyDiffBuild, prepareDiffDirectory };
135
+ export { applyTemplateDiff, copyDiffBuild, copyShopifyConfig, prepareDiffDirectory };
@@ -4,9 +4,12 @@ import { pipeline } from 'stream/promises';
4
4
  import gunzipMaybe from 'gunzip-maybe';
5
5
  import { extract } from 'tar-fs';
6
6
  import { fetch } from '@shopify/cli-kit/node/http';
7
- import { fileExists, mkdir } from '@shopify/cli-kit/node/fs';
7
+ import { parseGitHubRepositoryURL } from '@shopify/cli-kit/node/github';
8
+ import { fileExists, mkdir, rmdir } from '@shopify/cli-kit/node/fs';
8
9
  import { AbortError } from '@shopify/cli-kit/node/error';
9
10
  import { getSkeletonSourceDir } from './build.js';
11
+ import { joinPath } from '@shopify/cli-kit/node/path';
12
+ import { downloadGitRepository } from '@shopify/cli-kit/node/git';
10
13
 
11
14
  const REPO_RELEASES_URL = `https://api.github.com/repos/shopify/hydrogen/releases/latest`;
12
15
  const getTryMessage = (status) => status === 403 ? `If you are using a VPN, WARP, or similar service, consider disabling it momentarily.` : void 0;
@@ -25,7 +28,7 @@ async function getLatestReleaseDownloadUrl(signal) {
25
28
  url: release.tarball_url
26
29
  };
27
30
  }
28
- async function downloadTarball(url, storageDir, signal) {
31
+ async function downloadMonorepoTarball(url, storageDir, signal) {
29
32
  const response = await fetch(url, { signal });
30
33
  if (!response.ok || response.status >= 400) {
31
34
  throw new AbortError(
@@ -48,7 +51,7 @@ async function downloadTarball(url, storageDir, signal) {
48
51
  })
49
52
  );
50
53
  }
51
- async function getLatestTemplates({
54
+ async function downloadMonorepoTemplates({
52
55
  signal
53
56
  } = {}) {
54
57
  if (process.env.LOCAL_DEV) {
@@ -69,7 +72,7 @@ async function getLatestTemplates({
69
72
  }
70
73
  const templateStorageVersionPath = path.join(templateStoragePath, version);
71
74
  if (!await fileExists(templateStorageVersionPath)) {
72
- await downloadTarball(url, templateStorageVersionPath, signal);
75
+ await downloadMonorepoTarball(url, templateStorageVersionPath, signal);
73
76
  }
74
77
  return {
75
78
  version,
@@ -87,5 +90,32 @@ Please check your internet connection and the following error:
87
90
  );
88
91
  }
89
92
  }
93
+ async function downloadExternalRepo(appTemplate, signal) {
94
+ const parsed = parseGitHubRepositoryURL(appTemplate);
95
+ if (parsed.isErr()) {
96
+ throw new AbortError(parsed.error.message);
97
+ }
98
+ const externalTemplates = fileURLToPath(
99
+ new URL("../external-templates", import.meta.url)
100
+ );
101
+ if (!await fileExists(externalTemplates)) {
102
+ await mkdir(externalTemplates);
103
+ }
104
+ const result = parsed.value;
105
+ const templateDir = joinPath(
106
+ externalTemplates,
107
+ result.full.replace(/^https?:\/\//, "").replace(/[^\w]+/, "_")
108
+ );
109
+ if (await fileExists(templateDir)) {
110
+ await rmdir(templateDir, { force: true });
111
+ }
112
+ await downloadGitRepository({
113
+ repoUrl: result.full,
114
+ destination: templateDir,
115
+ shallow: true
116
+ });
117
+ await rmdir(joinPath(templateDir, ".git"), { force: true });
118
+ return { templateDir };
119
+ }
90
120
 
91
- export { getLatestTemplates };
121
+ export { downloadExternalRepo, downloadMonorepoTemplates };
@@ -6,18 +6,36 @@ function generateFunctionDocumentation(functionNode, variableStatement) {
6
6
  variableStatement
7
7
  );
8
8
  generateParameterDocumentation(functionNode, outputDocNode);
9
- const jsDocs = variableStatement?.getJsDocs()[0];
10
- if (jsDocs?.getTags().length === 0) {
9
+ if (variableStatement) {
11
10
  const declaration = variableStatement?.getDeclarations()[0];
12
- const type = declaration?.getType().getText();
13
- if (type && type !== "any" && type !== "unknown") {
14
- const tagName = type.startsWith("() =>") ? "return" : "type";
15
- const normalizedType = type.replace(/^\(\)\s+=>\s+/, "").replace(/typeof import\("[^"]+"\)\./, "typeof ");
16
- const text = normalizedType === "SerializeFrom<typeof loader>" ? `{LoaderReturnData}` : `{${normalizedType}}`;
17
- jsDocs.addTag({ tagName, text });
11
+ const jsDocs = variableStatement?.getJsDocs()[0];
12
+ if (jsDocs?.getTags().length === 0) {
13
+ const type = declaration?.getType().getText();
14
+ if (isAnnotableType(type)) {
15
+ jsDocs.addTag({
16
+ tagName: type.startsWith("() =>") ? "return" : "type",
17
+ text: normalizeType(type)
18
+ });
19
+ }
20
+ }
21
+ } else {
22
+ const declaration = functionNode;
23
+ const jsDocs = declaration.getJsDocs()[0];
24
+ if (jsDocs?.getTags().length === 0) {
25
+ const returnType = declaration.getReturnType().getText();
26
+ if (isAnnotableType(returnType)) {
27
+ jsDocs.addTag({ tagName: "return", text: normalizeType(returnType) });
28
+ }
18
29
  }
19
30
  }
20
31
  }
32
+ function isAnnotableType(type) {
33
+ return !!(type && type !== "any" && type !== "unknown" && type !== "{}" && type !== "boolean");
34
+ }
35
+ function normalizeType(type) {
36
+ const normalizedType = type.replace(/^\(\)\s+=>\s+/, "").replace(/typeof import\("[^"]+"\)\./, "typeof ");
37
+ return normalizedType === "SerializeFrom<any>" ? `{SerializeFrom<loader>}` : `{${normalizedType}}`;
38
+ }
21
39
  function generateParameterDocumentation(functionNode, docNode) {
22
40
  const params = functionNode.getParameters();
23
41
  const jsDoc = getJsDocOrCreate(docNode);
@@ -26,13 +26,15 @@ function generateTypeDefs(sourceFile, code) {
26
26
  }
27
27
  }
28
28
  typedefs.push("");
29
+ const knownGenerics = {
30
+ MetaFunction: "T",
31
+ SerializeFrom: "T"
32
+ };
29
33
  typedefsFromImports.forEach((typeElements, moduleSpecifier) => {
30
34
  for (const typeElement of typeElements) {
31
- if (typeElement === "SerializeFrom")
32
- continue;
33
- const hasGeneric = typeElement === "MetaFunction";
35
+ const hasGeneric = !!knownGenerics[typeElement];
34
36
  typedefs.push(
35
- `/** ${hasGeneric ? "@template T " : ""}@typedef {import('${moduleSpecifier}').${typeElement}${hasGeneric ? "<T>" : ""}} ${typeElement} */`
37
+ `/** ${hasGeneric ? `@template ${knownGenerics[typeElement]} ` : ""}@typedef {import('${moduleSpecifier}').${typeElement}${hasGeneric ? `<${knownGenerics[typeElement]}>` : ""}} ${typeElement} */`
36
38
  );
37
39
  }
38
40
  });
@@ -3,6 +3,7 @@ import { outputDebug } from '@shopify/cli-kit/node/output';
3
3
  import { joinPath } from '@shopify/cli-kit/node/path';
4
4
  import { getCodeFormatOptions, formatCode } from '../format-code.js';
5
5
  import { transpileFile } from './file.js';
6
+ import { findFileWithExtension } from '../file.js';
6
7
 
7
8
  const DEFAULT_JS_CONFIG = {
8
9
  checkJs: false,
@@ -120,10 +121,13 @@ async function transpileProject(projectDir, keepTypes = true) {
120
121
  );
121
122
  }
122
123
  try {
123
- const eslintrcPath = joinPath(projectDir, ".eslintrc.js");
124
- let eslintrc = await readFile(eslintrcPath);
125
- eslintrc = eslintrc.replace(/\/\*\*[\s*]+@type.+\s+\*\/\s?/gim, "").replace(/\s*,?\s*['"`]plugin:hydrogen\/typescript['"`]/gim, "").replace(/\s+['"`]@typescript-eslint\/.+,/gim, "");
126
- await writeFile(eslintrcPath, eslintrc);
124
+ const { filepath = joinPath(projectDir, ".eslintrc.cjs") } = await findFileWithExtension(projectDir, ".eslintrc", ["cjs", "js"]);
125
+ let eslintrc = await readFile(filepath);
126
+ if (!keepTypes) {
127
+ eslintrc = eslintrc.replace(/\/\*\*[\s*]+@type.+\s+\*\/\s?/gim, "");
128
+ }
129
+ eslintrc = eslintrc.replace(/\s*,?\s*['"`]plugin:hydrogen\/typescript['"`]/gim, "").replace(/\s+['"`]@typescript-eslint\/.+,/gim, "");
130
+ await writeFile(filepath, await formatCode(eslintrc, formatConfig));
127
131
  } catch (error) {
128
132
  outputDebug(
129
133
  "Could not remove TS rules from .eslintrc:\n" + error.stack
@@ -0,0 +1,44 @@
1
+ import { fanoutHooks } from '@shopify/cli-kit/node/plugins';
2
+ import { outputDebug } from '@shopify/cli-kit/node/output';
3
+ import { BugError, AbortError } from '@shopify/cli-kit/node/error';
4
+
5
+ async function startTunnelPlugin(config, port, provider) {
6
+ const hooks = await fanoutHooks(config, "tunnel_start", { port, provider });
7
+ const results = Object.values(hooks).filter(
8
+ (tunnelResponse) => !tunnelResponse?.isErr() || tunnelResponse.error.type !== "invalid-provider"
9
+ );
10
+ const first = results[0];
11
+ if (!first)
12
+ throw new BugError(`We couldn't find the ${provider} tunnel plugin`);
13
+ if (first.isErr())
14
+ throw new AbortError(
15
+ `${provider} failed to start the tunnel.
16
+ ${first.error.message}`
17
+ );
18
+ return first.value;
19
+ }
20
+ async function pollTunnelURL(tunnelClient) {
21
+ return new Promise((resolve, reject) => {
22
+ let retries = 0;
23
+ const pollTunnelStatus = async () => {
24
+ const result = tunnelClient.getTunnelStatus();
25
+ outputDebug(
26
+ `Polling tunnel status for ${tunnelClient.provider} (attempt ${retries}): ${result.status}`
27
+ );
28
+ if (result.status === "error")
29
+ return reject(new AbortError(result.message, result.tryMessage));
30
+ if (result.status === "connected") {
31
+ resolve(result.url);
32
+ } else {
33
+ retries += 1;
34
+ startPolling();
35
+ }
36
+ };
37
+ const startPolling = () => {
38
+ setTimeout(pollTunnelStatus, 500);
39
+ };
40
+ pollTunnelStatus();
41
+ });
42
+ }
43
+
44
+ export { pollTunnelURL, startTunnelPlugin };
@@ -0,0 +1,24 @@
1
+ import { getStorefronts } from './graphql/admin/link-storefront.js';
2
+ import { linkStorefront } from '../commands/hydrogen/link.js';
3
+ import { renderMissingLink } from './render-errors.js';
4
+
5
+ async function verifyLinkedStorefront({
6
+ root,
7
+ session,
8
+ config,
9
+ cliCommand
10
+ }) {
11
+ const storefronts = await getStorefronts(session);
12
+ let configuredStorefront = config.storefront?.id ? storefronts.find(({ id }) => id === config.storefront.id) : void 0;
13
+ if (configuredStorefront) {
14
+ return configuredStorefront;
15
+ }
16
+ renderMissingLink({ noStorefronts: !storefronts.length });
17
+ return await linkStorefront(root, session, config, {
18
+ force: true,
19
+ cliCommand,
20
+ storefronts
21
+ });
22
+ }
23
+
24
+ export { verifyLinkedStorefront };
@@ -5,8 +5,8 @@ import { joinPath, relativePath } from '@shopify/cli-kit/node/path';
5
5
  const VIRTUAL_ROUTES_DIR = "virtual-routes/routes";
6
6
  const VIRTUAL_ROOT = "virtual-routes/virtual-root";
7
7
  async function addVirtualRoutes(config) {
8
+ const distPath = process.env.SHOPIFY_UNIT_TEST ? fileURLToPath(new URL("../../../hydrogen/src/vite", import.meta.url)) : fileURLToPath(new URL("..", import.meta.url));
8
9
  const userRouteList = Object.values(config.routes);
9
- const distPath = fileURLToPath(new URL("..", import.meta.url));
10
10
  const virtualRoutesPath = joinPath(distPath, VIRTUAL_ROUTES_DIR);
11
11
  for (const absoluteFilePath of await glob(
12
12
  joinPath(virtualRoutesPath, "**", "*")
@@ -1,6 +1,11 @@
1
- import { joinPath } from '@shopify/cli-kit/node/path';
1
+ import { joinPath, resolvePath, dirname, basename } from '@shopify/cli-kit/node/path';
2
+ import { findFileWithExtension } from './file.js';
2
3
 
3
- async function getViteConfig(root) {
4
+ async function hasViteConfig(root) {
5
+ const result = await findFileWithExtension(root, "vite.config");
6
+ return !!result.filepath;
7
+ }
8
+ async function getViteConfig(root, ssrEntryFlag) {
4
9
  const vite = await import('vite');
5
10
  const command = "build";
6
11
  const mode = process.env.NODE_ENV || "production";
@@ -18,13 +23,19 @@ async function getViteConfig(root) {
18
23
  mode,
19
24
  mode
20
25
  );
26
+ const { appDirectory, serverBuildFile, routes } = getRemixConfigFromVite(resolvedViteConfig);
21
27
  const serverOutDir = resolvedViteConfig.build.outDir;
22
28
  const clientOutDir = serverOutDir.replace(/server$/, "client");
23
29
  const rollupOutput = resolvedViteConfig.build.rollupOptions.output;
24
30
  const { entryFileNames } = (Array.isArray(rollupOutput) ? rollupOutput[0] : rollupOutput) ?? {};
25
31
  const serverOutFile = joinPath(
26
32
  serverOutDir,
27
- typeof entryFileNames === "string" ? entryFileNames : "index.js"
33
+ typeof entryFileNames === "string" ? entryFileNames : serverBuildFile ?? "index.js"
34
+ );
35
+ const ssrEntry = ssrEntryFlag ?? resolvedViteConfig.build.ssr;
36
+ const resolvedSsrEntry = resolvePath(
37
+ resolvedViteConfig.root,
38
+ typeof ssrEntry === "string" ? ssrEntry : "server"
28
39
  );
29
40
  return {
30
41
  clientOutDir,
@@ -32,14 +43,33 @@ async function getViteConfig(root) {
32
43
  serverOutFile,
33
44
  resolvedViteConfig,
34
45
  userViteConfig: maybeConfig.config,
35
- remixConfig: getRemixConfigFromVite(resolvedViteConfig)
46
+ remixConfig: {
47
+ routes: routes ?? {},
48
+ appDirectory: appDirectory ?? joinPath(resolvedViteConfig.root, "app"),
49
+ rootDirectory: resolvedViteConfig.root,
50
+ serverEntryPoint: (await findFileWithExtension(
51
+ dirname(resolvedSsrEntry),
52
+ basename(resolvedSsrEntry)
53
+ )).filepath || resolvedSsrEntry
54
+ }
36
55
  };
37
56
  }
38
57
  function getRemixConfigFromVite(viteConfig) {
39
- const { remixConfig } = viteConfig.__remixPluginContext || {
40
- remixConfig: { appDirectory: joinPath(viteConfig.root, "app") }
41
- };
42
- return remixConfig;
58
+ const { remixConfig } = findHydrogenPlugin(viteConfig)?.api?.getPluginOptions() ?? {};
59
+ return remixConfig ? {
60
+ appDirectory: remixConfig.appDirectory,
61
+ serverBuildFile: remixConfig.serverBuildFile,
62
+ routes: remixConfig.routes
63
+ } : {};
64
+ }
65
+ function findPlugin(config, name) {
66
+ return config.plugins.find((plugin) => plugin.name === name);
67
+ }
68
+ function findHydrogenPlugin(config) {
69
+ return findPlugin(config, "hydrogen:main");
70
+ }
71
+ function findOxygenPlugin(config) {
72
+ return findPlugin(config, "oxygen:main");
43
73
  }
44
74
 
45
- export { getViteConfig };
75
+ export { findHydrogenPlugin, findOxygenPlugin, getViteConfig, hasViteConfig };