@shopify/cli-hydrogen 5.0.2 → 5.1.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 (243) hide show
  1. package/dist/commands/hydrogen/build.js +16 -2
  2. package/dist/commands/hydrogen/codegen-unstable.js +13 -24
  3. package/dist/commands/hydrogen/dev.js +45 -39
  4. package/dist/commands/hydrogen/env/list.js +25 -24
  5. package/dist/commands/hydrogen/env/list.test.js +46 -43
  6. package/dist/commands/hydrogen/env/pull.js +53 -25
  7. package/dist/commands/hydrogen/env/pull.test.js +123 -42
  8. package/dist/commands/hydrogen/generate/route.js +31 -132
  9. package/dist/commands/hydrogen/generate/route.test.js +34 -126
  10. package/dist/commands/hydrogen/init.js +46 -127
  11. package/dist/commands/hydrogen/init.test.js +352 -100
  12. package/dist/commands/hydrogen/link.js +70 -69
  13. package/dist/commands/hydrogen/link.test.js +72 -107
  14. package/dist/commands/hydrogen/list.js +22 -12
  15. package/dist/commands/hydrogen/list.test.js +51 -48
  16. package/dist/commands/hydrogen/login.js +31 -0
  17. package/dist/commands/hydrogen/logout.js +21 -0
  18. package/dist/commands/hydrogen/setup/css.js +79 -0
  19. package/dist/commands/hydrogen/setup/markets.js +53 -0
  20. package/dist/commands/hydrogen/setup.js +133 -0
  21. package/dist/commands/hydrogen/shortcut.js +2 -45
  22. package/dist/commands/hydrogen/shortcut.test.js +10 -37
  23. package/dist/generator-templates/assets/css-modules/package.json +6 -0
  24. package/dist/generator-templates/assets/postcss/package.json +10 -0
  25. package/dist/generator-templates/assets/postcss/postcss.config.js +8 -0
  26. package/dist/generator-templates/assets/tailwind/package.json +13 -0
  27. package/dist/generator-templates/assets/tailwind/postcss.config.js +10 -0
  28. package/dist/generator-templates/assets/tailwind/tailwind.config.js +8 -0
  29. package/dist/generator-templates/assets/tailwind/tailwind.css +3 -0
  30. package/dist/generator-templates/assets/vanilla-extract/package.json +9 -0
  31. package/dist/generator-templates/starter/.eslintignore +5 -0
  32. package/dist/generator-templates/starter/.eslintrc.js +18 -0
  33. package/dist/generator-templates/starter/.graphqlrc.yml +1 -0
  34. package/dist/generator-templates/starter/README.md +40 -0
  35. package/dist/generator-templates/starter/app/components/Aside.tsx +47 -0
  36. package/dist/generator-templates/starter/app/components/Cart.tsx +340 -0
  37. package/dist/generator-templates/starter/app/components/Footer.tsx +99 -0
  38. package/dist/generator-templates/starter/app/components/Header.tsx +178 -0
  39. package/dist/generator-templates/starter/app/components/Layout.tsx +95 -0
  40. package/dist/generator-templates/starter/app/components/Search.tsx +480 -0
  41. package/dist/generator-templates/starter/app/entry.client.tsx +12 -0
  42. package/dist/generator-templates/starter/app/entry.server.tsx +33 -0
  43. package/dist/generator-templates/starter/app/root.tsx +270 -0
  44. package/dist/generator-templates/starter/app/routes/$.tsx +7 -0
  45. package/dist/generator-templates/{routes → starter/app/routes}/[robots.txt].tsx +47 -69
  46. package/dist/generator-templates/starter/app/routes/[sitemap.xml].tsx +174 -0
  47. package/dist/generator-templates/starter/app/routes/_index.tsx +145 -0
  48. package/dist/generator-templates/starter/app/routes/account.$.tsx +9 -0
  49. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +563 -0
  50. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +309 -0
  51. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +196 -0
  52. package/dist/generator-templates/starter/app/routes/account.profile.tsx +289 -0
  53. package/dist/generator-templates/starter/app/routes/account.tsx +203 -0
  54. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +157 -0
  55. package/dist/generator-templates/starter/app/routes/account_.login.tsx +143 -0
  56. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +33 -0
  57. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +124 -0
  58. package/dist/generator-templates/starter/app/routes/account_.register.tsx +207 -0
  59. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +136 -0
  60. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +342 -0
  61. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +88 -0
  62. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +162 -0
  63. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +94 -0
  64. package/dist/generator-templates/starter/app/routes/cart.tsx +104 -0
  65. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +184 -0
  66. package/dist/generator-templates/starter/app/routes/collections._index.tsx +120 -0
  67. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +57 -0
  68. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +94 -0
  69. package/dist/generator-templates/starter/app/routes/policies._index.tsx +63 -0
  70. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +418 -0
  71. package/dist/generator-templates/starter/app/routes/search.tsx +168 -0
  72. package/dist/generator-templates/starter/app/styles/app.css +473 -0
  73. package/dist/generator-templates/starter/app/styles/reset.css +129 -0
  74. package/dist/generator-templates/starter/app/utils.ts +46 -0
  75. package/dist/generator-templates/starter/package.json +43 -0
  76. package/dist/generator-templates/starter/public/favicon.svg +28 -0
  77. package/dist/generator-templates/starter/remix.config.js +26 -0
  78. package/dist/generator-templates/starter/remix.env.d.ts +39 -0
  79. package/dist/generator-templates/starter/server.ts +253 -0
  80. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +1906 -0
  81. package/dist/generator-templates/starter/tsconfig.json +22 -0
  82. package/dist/lib/auth.js +123 -0
  83. package/dist/lib/auth.test.js +157 -0
  84. package/dist/lib/build.js +51 -0
  85. package/dist/lib/check-version.js +3 -3
  86. package/dist/lib/check-version.test.js +24 -0
  87. package/dist/lib/codegen.js +26 -17
  88. package/dist/lib/environment-variables.js +68 -0
  89. package/dist/lib/environment-variables.test.js +147 -0
  90. package/dist/lib/file.js +41 -0
  91. package/dist/lib/file.test.js +69 -0
  92. package/dist/lib/flags.js +39 -2
  93. package/dist/lib/format-code.js +26 -0
  94. package/dist/lib/gid.js +12 -0
  95. package/dist/lib/{graphql.test.js → gid.test.js} +1 -1
  96. package/dist/lib/graphql/admin/client.js +27 -0
  97. package/dist/lib/graphql/admin/client.test.js +51 -0
  98. package/dist/lib/graphql/admin/create-storefront.js +13 -15
  99. package/dist/lib/graphql/admin/create-storefront.test.js +64 -0
  100. package/dist/lib/graphql/admin/fetch-job.js +6 -15
  101. package/dist/lib/graphql/admin/link-storefront.js +7 -11
  102. package/dist/lib/graphql/admin/link-storefront.test.js +38 -0
  103. package/dist/lib/graphql/admin/list-environments.js +2 -2
  104. package/dist/lib/graphql/admin/list-environments.test.js +44 -0
  105. package/dist/lib/graphql/admin/list-storefronts.js +7 -11
  106. package/dist/lib/graphql/admin/list-storefronts.test.js +44 -0
  107. package/dist/lib/graphql/admin/pull-variables.js +3 -3
  108. package/dist/lib/graphql/admin/pull-variables.test.js +37 -0
  109. package/dist/lib/graphql/business-platform/user-account.js +83 -0
  110. package/dist/lib/graphql/business-platform/user-account.test.js +80 -0
  111. package/dist/lib/log.js +185 -9
  112. package/dist/lib/log.test.js +92 -0
  113. package/dist/lib/mini-oxygen.js +19 -9
  114. package/dist/lib/missing-routes.js +0 -2
  115. package/dist/lib/onboarding/common.js +456 -0
  116. package/dist/lib/onboarding/index.js +2 -0
  117. package/dist/lib/onboarding/local.js +229 -0
  118. package/dist/lib/onboarding/remote.js +89 -0
  119. package/dist/lib/remix-version-interop.js +5 -5
  120. package/dist/lib/remix-version-interop.test.js +11 -1
  121. package/dist/lib/render-errors.js +13 -11
  122. package/dist/lib/setups/css/assets.js +89 -0
  123. package/dist/lib/setups/css/css-modules.js +22 -0
  124. package/dist/lib/setups/css/index.js +44 -0
  125. package/dist/lib/setups/css/postcss.js +34 -0
  126. package/dist/lib/setups/css/replacers.js +137 -0
  127. package/dist/lib/setups/css/tailwind.js +54 -0
  128. package/dist/lib/setups/css/vanilla-extract.js +22 -0
  129. package/dist/lib/setups/i18n/domains.test.js +25 -0
  130. package/dist/lib/setups/i18n/index.js +46 -0
  131. package/dist/lib/setups/i18n/replacers.js +227 -0
  132. package/dist/lib/setups/i18n/subdomains.test.js +25 -0
  133. package/dist/lib/setups/i18n/subfolders.test.js +25 -0
  134. package/dist/lib/setups/i18n/templates/domains.js +14 -0
  135. package/dist/lib/setups/i18n/templates/domains.ts +25 -0
  136. package/dist/lib/setups/i18n/templates/subdomains.js +14 -0
  137. package/dist/lib/setups/i18n/templates/subdomains.ts +24 -0
  138. package/dist/lib/setups/i18n/templates/subfolders.js +14 -0
  139. package/dist/lib/setups/i18n/templates/subfolders.ts +28 -0
  140. package/dist/lib/setups/routes/generate.js +244 -0
  141. package/dist/lib/setups/routes/generate.test.js +313 -0
  142. package/dist/lib/shell.js +52 -5
  143. package/dist/lib/shell.test.js +42 -16
  144. package/dist/lib/shopify-config.js +23 -18
  145. package/dist/lib/shopify-config.test.js +63 -73
  146. package/dist/lib/template-downloader.js +9 -7
  147. package/dist/lib/transpile-ts.js +9 -29
  148. package/dist/virtual-routes/routes/index.jsx +40 -19
  149. package/oclif.manifest.json +710 -1
  150. package/package.json +16 -16
  151. package/dist/commands/hydrogen/build.d.ts +0 -23
  152. package/dist/commands/hydrogen/check.d.ts +0 -15
  153. package/dist/commands/hydrogen/codegen-unstable.d.ts +0 -15
  154. package/dist/commands/hydrogen/dev.d.ts +0 -21
  155. package/dist/commands/hydrogen/env/list.d.ts +0 -18
  156. package/dist/commands/hydrogen/env/pull.d.ts +0 -22
  157. package/dist/commands/hydrogen/g.d.ts +0 -10
  158. package/dist/commands/hydrogen/generate/route.d.ts +0 -32
  159. package/dist/commands/hydrogen/generate/route.test.d.ts +0 -1
  160. package/dist/commands/hydrogen/generate/routes.d.ts +0 -16
  161. package/dist/commands/hydrogen/init.d.ts +0 -24
  162. package/dist/commands/hydrogen/init.test.d.ts +0 -1
  163. package/dist/commands/hydrogen/link.d.ts +0 -23
  164. package/dist/commands/hydrogen/link.test.d.ts +0 -1
  165. package/dist/commands/hydrogen/list.d.ts +0 -21
  166. package/dist/commands/hydrogen/list.test.d.ts +0 -1
  167. package/dist/commands/hydrogen/preview.d.ts +0 -17
  168. package/dist/commands/hydrogen/shortcut.d.ts +0 -9
  169. package/dist/commands/hydrogen/shortcut.test.d.ts +0 -1
  170. package/dist/commands/hydrogen/unlink.d.ts +0 -16
  171. package/dist/commands/hydrogen/unlink.test.d.ts +0 -1
  172. package/dist/create-app.d.ts +0 -1
  173. package/dist/generator-templates/routes/[sitemap.xml].tsx +0 -235
  174. package/dist/generator-templates/routes/account/login.tsx +0 -103
  175. package/dist/generator-templates/routes/account/register.tsx +0 -103
  176. package/dist/generator-templates/routes/cart.tsx +0 -81
  177. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +0 -104
  178. package/dist/generator-templates/routes/collections/index.tsx +0 -102
  179. package/dist/generator-templates/routes/graphiql.tsx +0 -10
  180. package/dist/generator-templates/routes/index.tsx +0 -40
  181. package/dist/generator-templates/routes/pages/$pageHandle.tsx +0 -112
  182. package/dist/generator-templates/routes/policies/$policyHandle.tsx +0 -140
  183. package/dist/generator-templates/routes/policies/index.tsx +0 -117
  184. package/dist/generator-templates/routes/products/$productHandle.tsx +0 -92
  185. package/dist/hooks/init.d.ts +0 -5
  186. package/dist/lib/admin-session.d.ts +0 -6
  187. package/dist/lib/admin-session.js +0 -16
  188. package/dist/lib/admin-session.test.d.ts +0 -1
  189. package/dist/lib/admin-session.test.js +0 -27
  190. package/dist/lib/admin-urls.d.ts +0 -8
  191. package/dist/lib/check-lockfile.d.ts +0 -3
  192. package/dist/lib/check-lockfile.test.d.ts +0 -1
  193. package/dist/lib/check-version.d.ts +0 -16
  194. package/dist/lib/check-version.test.d.ts +0 -1
  195. package/dist/lib/codegen.d.ts +0 -26
  196. package/dist/lib/combined-environment-variables.d.ts +0 -8
  197. package/dist/lib/combined-environment-variables.js +0 -57
  198. package/dist/lib/combined-environment-variables.test.d.ts +0 -1
  199. package/dist/lib/combined-environment-variables.test.js +0 -111
  200. package/dist/lib/config.d.ts +0 -20
  201. package/dist/lib/flags.d.ts +0 -27
  202. package/dist/lib/flags.test.d.ts +0 -1
  203. package/dist/lib/graphql/admin/create-storefront.d.ts +0 -17
  204. package/dist/lib/graphql/admin/fetch-job.d.ts +0 -23
  205. package/dist/lib/graphql/admin/link-storefront.d.ts +0 -14
  206. package/dist/lib/graphql/admin/list-environments.d.ts +0 -21
  207. package/dist/lib/graphql/admin/list-storefronts.d.ts +0 -25
  208. package/dist/lib/graphql/admin/pull-variables.d.ts +0 -21
  209. package/dist/lib/graphql.d.ts +0 -21
  210. package/dist/lib/graphql.js +0 -18
  211. package/dist/lib/graphql.test.d.ts +0 -1
  212. package/dist/lib/log.d.ts +0 -6
  213. package/dist/lib/mini-oxygen.d.ts +0 -22
  214. package/dist/lib/missing-routes.d.ts +0 -8
  215. package/dist/lib/missing-routes.test.d.ts +0 -1
  216. package/dist/lib/missing-storefronts.d.ts +0 -5
  217. package/dist/lib/missing-storefronts.js +0 -18
  218. package/dist/lib/process.d.ts +0 -6
  219. package/dist/lib/pull-environment-variables.d.ts +0 -20
  220. package/dist/lib/pull-environment-variables.js +0 -57
  221. package/dist/lib/pull-environment-variables.test.d.ts +0 -1
  222. package/dist/lib/pull-environment-variables.test.js +0 -174
  223. package/dist/lib/remix-version-interop.d.ts +0 -11
  224. package/dist/lib/remix-version-interop.test.d.ts +0 -1
  225. package/dist/lib/render-errors.d.ts +0 -16
  226. package/dist/lib/shell.d.ts +0 -11
  227. package/dist/lib/shell.test.d.ts +0 -1
  228. package/dist/lib/shop.d.ts +0 -7
  229. package/dist/lib/shop.js +0 -32
  230. package/dist/lib/shop.test.d.ts +0 -1
  231. package/dist/lib/shop.test.js +0 -78
  232. package/dist/lib/shopify-config.d.ts +0 -35
  233. package/dist/lib/shopify-config.test.d.ts +0 -1
  234. package/dist/lib/string.d.ts +0 -3
  235. package/dist/lib/string.test.d.ts +0 -1
  236. package/dist/lib/template-downloader.d.ts +0 -6
  237. package/dist/lib/transpile-ts.d.ts +0 -16
  238. package/dist/lib/user-errors.d.ts +0 -9
  239. package/dist/lib/user-errors.js +0 -11
  240. package/dist/lib/virtual-routes.d.ts +0 -7
  241. package/dist/lib/virtual-routes.test.d.ts +0 -1
  242. /package/dist/{commands/hydrogen/env/list.test.d.ts → lib/setups/css/common.js} +0 -0
  243. /package/dist/{commands/hydrogen/env/pull.test.d.ts → lib/setups/i18n/mock-i18n-types.js} +0 -0
@@ -3,10 +3,11 @@ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
3
3
  import { inTemporaryDirectory, fileExists, readFile, writeFile } from '@shopify/cli-kit/node/fs';
4
4
  import { joinPath } from '@shopify/cli-kit/node/path';
5
5
  import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
6
- import { getAdminSession } from '../../../lib/admin-session.js';
7
- import { pullRemoteEnvironmentVariables } from '../../../lib/pull-environment-variables.js';
8
- import { getConfig } from '../../../lib/shopify-config.js';
9
- import { pullVariables } from './pull.js';
6
+ import { login } from '../../../lib/auth.js';
7
+ import { getStorefrontEnvVariables } from '../../../lib/graphql/admin/pull-variables.js';
8
+ import { runEnvPull } from './pull.js';
9
+ import { renderMissingLink, renderMissingStorefront } from '../../../lib/render-errors.js';
10
+ import { linkStorefront } from '../link.js';
10
11
 
11
12
  vi.mock("@shopify/cli-kit/node/ui", async () => {
12
13
  const original = await vi.importActual("@shopify/cli-kit/node/ui");
@@ -16,58 +17,65 @@ vi.mock("@shopify/cli-kit/node/ui", async () => {
16
17
  };
17
18
  });
18
19
  vi.mock("../link.js");
19
- vi.mock("../../../lib/admin-session.js");
20
- vi.mock("../../../lib/shopify-config.js");
21
- vi.mock("../../../lib/pull-environment-variables.js");
22
- vi.mock("../../../lib/shop.js", () => ({
23
- getHydrogenShop: () => "my-shop"
24
- }));
20
+ vi.mock("../../../lib/auth.js");
21
+ vi.mock("../../../lib/render-errors.js");
22
+ vi.mock("../../../lib/graphql/admin/pull-variables.js");
25
23
  describe("pullVariables", () => {
26
24
  const ADMIN_SESSION = {
27
25
  token: "abc123",
28
26
  storeFqdn: "my-shop"
29
27
  };
28
+ const SHOPIFY_CONFIG = {
29
+ shop: "my-shop",
30
+ shopName: "My Shop",
31
+ email: "email",
32
+ storefront: {
33
+ id: "gid://shopify/HydrogenStorefront/2",
34
+ title: "Existing Link"
35
+ }
36
+ };
30
37
  beforeEach(async () => {
31
- vi.mocked(getAdminSession).mockResolvedValue(ADMIN_SESSION);
32
- vi.mocked(getConfig).mockResolvedValue({
33
- storefront: {
34
- id: "gid://shopify/HydrogenStorefront/2",
35
- title: "Existing Link"
36
- }
37
- });
38
- vi.mocked(pullRemoteEnvironmentVariables).mockResolvedValue([
39
- {
40
- id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
41
- key: "PUBLIC_API_TOKEN",
42
- value: "abc123",
43
- isSecret: false
44
- },
45
- {
46
- id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/2",
47
- key: "PRIVATE_API_TOKEN",
48
- value: "",
49
- isSecret: true
50
- }
51
- ]);
38
+ vi.mocked(login).mockResolvedValue({
39
+ session: ADMIN_SESSION,
40
+ config: SHOPIFY_CONFIG
41
+ });
42
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
43
+ id: SHOPIFY_CONFIG.storefront.id,
44
+ environmentVariables: [
45
+ {
46
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
47
+ key: "PUBLIC_API_TOKEN",
48
+ value: "abc123",
49
+ isSecret: false
50
+ },
51
+ {
52
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/2",
53
+ key: "PRIVATE_API_TOKEN",
54
+ value: "",
55
+ isSecret: true
56
+ }
57
+ ]
58
+ });
52
59
  });
53
60
  afterEach(() => {
54
61
  vi.resetAllMocks();
55
62
  mockAndCaptureOutput().clear();
56
63
  });
57
- it("calls pullRemoteEnvironmentVariables", async () => {
64
+ it("calls getStorefrontEnvVariables", async () => {
58
65
  await inTemporaryDirectory(async (tmpDir) => {
59
- await pullVariables({ path: tmpDir, envBranch: "staging" });
60
- expect(pullRemoteEnvironmentVariables).toHaveBeenCalledWith({
61
- root: tmpDir,
62
- envBranch: "staging"
63
- });
66
+ await runEnvPull({ path: tmpDir, envBranch: "staging" });
67
+ expect(getStorefrontEnvVariables).toHaveBeenCalledWith(
68
+ ADMIN_SESSION,
69
+ SHOPIFY_CONFIG.storefront.id,
70
+ "staging"
71
+ );
64
72
  });
65
73
  });
66
74
  it("writes environment variables to a .env file", async () => {
67
75
  await inTemporaryDirectory(async (tmpDir) => {
68
76
  const filePath = joinPath(tmpDir, ".env");
69
77
  expect(await fileExists(filePath)).toBeFalsy();
70
- await pullVariables({ path: tmpDir });
78
+ await runEnvPull({ path: tmpDir });
71
79
  expect(await readFile(filePath)).toStrictEqual(
72
80
  'PUBLIC_API_TOKEN=abc123\nPRIVATE_API_TOKEN=""'
73
81
  );
@@ -76,7 +84,7 @@ describe("pullVariables", () => {
76
84
  it("warns about secret environment variables", async () => {
77
85
  await inTemporaryDirectory(async (tmpDir) => {
78
86
  const outputMock = mockAndCaptureOutput();
79
- await pullVariables({ path: tmpDir });
87
+ await runEnvPull({ path: tmpDir });
80
88
  expect(outputMock.warn()).toMatch(
81
89
  /Existing Link contains environment variables marked as secret, so their/
82
90
  );
@@ -86,12 +94,85 @@ describe("pullVariables", () => {
86
94
  it("renders a success message", async () => {
87
95
  await inTemporaryDirectory(async (tmpDir) => {
88
96
  const outputMock = mockAndCaptureOutput();
89
- await pullVariables({ path: tmpDir });
97
+ await runEnvPull({ path: tmpDir });
90
98
  expect(outputMock.info()).toMatch(
91
99
  /Changes have been made to your \.env file/
92
100
  );
93
101
  });
94
102
  });
103
+ describe("when environment variables are empty", () => {
104
+ beforeEach(() => {
105
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
106
+ id: "gid://shopify/HydrogenStorefront/1",
107
+ environmentVariables: []
108
+ });
109
+ });
110
+ it("renders a message", async () => {
111
+ await inTemporaryDirectory(async (tmpDir) => {
112
+ const outputMock = mockAndCaptureOutput();
113
+ await runEnvPull({ path: tmpDir });
114
+ expect(outputMock.info()).toMatch(/No environment variables found\./);
115
+ });
116
+ });
117
+ });
118
+ describe("when there is no linked storefront", () => {
119
+ beforeEach(async () => {
120
+ vi.mocked(login).mockResolvedValue({
121
+ session: ADMIN_SESSION,
122
+ config: {
123
+ ...SHOPIFY_CONFIG,
124
+ storefront: void 0
125
+ }
126
+ });
127
+ });
128
+ it("calls renderMissingLink", async () => {
129
+ await inTemporaryDirectory(async (tmpDir) => {
130
+ await runEnvPull({ path: tmpDir });
131
+ expect(renderMissingLink).toHaveBeenCalledOnce();
132
+ });
133
+ });
134
+ it("prompts the user to create a link", async () => {
135
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
136
+ await inTemporaryDirectory(async (tmpDir) => {
137
+ await runEnvPull({ path: tmpDir });
138
+ expect(renderConfirmationPrompt).toHaveBeenCalledWith({
139
+ message: expect.stringMatching(/Run .* link.*\?/i)
140
+ });
141
+ expect(linkStorefront).toHaveBeenCalledWith(
142
+ tmpDir,
143
+ ADMIN_SESSION,
144
+ { ...SHOPIFY_CONFIG, storefront: void 0 },
145
+ expect.anything()
146
+ );
147
+ });
148
+ });
149
+ it("ends without requesting variables", async () => {
150
+ await inTemporaryDirectory(async (tmpDir) => {
151
+ await runEnvPull({ path: tmpDir });
152
+ expect(getStorefrontEnvVariables).not.toHaveBeenCalled();
153
+ });
154
+ });
155
+ describe("and the user does not create a new link", () => {
156
+ it("ends without requesting variables", async () => {
157
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(false);
158
+ await inTemporaryDirectory(async (tmpDir) => {
159
+ await runEnvPull({ path: tmpDir });
160
+ expect(getStorefrontEnvVariables).not.toHaveBeenCalled();
161
+ });
162
+ });
163
+ });
164
+ });
165
+ describe("when there is no matching storefront in the shop", () => {
166
+ beforeEach(() => {
167
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue(null);
168
+ });
169
+ it("renders missing storefronts message and ends", async () => {
170
+ await inTemporaryDirectory(async (tmpDir) => {
171
+ await runEnvPull({ path: tmpDir });
172
+ expect(renderMissingStorefront).toHaveBeenCalledOnce();
173
+ });
174
+ });
175
+ });
95
176
  describe("when a .env file already exists", () => {
96
177
  beforeEach(() => {
97
178
  vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
@@ -100,7 +181,7 @@ describe("pullVariables", () => {
100
181
  await inTemporaryDirectory(async (tmpDir) => {
101
182
  const filePath = joinPath(tmpDir, ".env");
102
183
  await writeFile(filePath, "EXISTING_TOKEN=1");
103
- await pullVariables({ path: tmpDir });
184
+ await runEnvPull({ path: tmpDir });
104
185
  expect(renderConfirmationPrompt).toHaveBeenCalledWith({
105
186
  confirmationMessage: `Yes, confirm changes`,
106
187
  cancellationMessage: `No, make changes later`,
@@ -115,7 +196,7 @@ describe("pullVariables", () => {
115
196
  await inTemporaryDirectory(async (tmpDir) => {
116
197
  const filePath = joinPath(tmpDir, ".env");
117
198
  await writeFile(filePath, "EXISTING_TOKEN=1");
118
- await pullVariables({ path: tmpDir, force: true });
199
+ await runEnvPull({ path: tmpDir, force: true });
119
200
  expect(renderConfirmationPrompt).not.toHaveBeenCalled();
120
201
  });
121
202
  });
@@ -1,27 +1,11 @@
1
- import { fileURLToPath } from 'url';
2
1
  import Command from '@shopify/cli-kit/node/base-command';
3
- import { fileExists, readFile, mkdir, writeFile } from '@shopify/cli-kit/node/fs';
4
- import { resolvePath, joinPath, relativePath, relativizePath, dirname } from '@shopify/cli-kit/node/path';
5
- import { AbortError } from '@shopify/cli-kit/node/error';
6
- import { renderSuccess, renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
2
+ import { resolvePath } from '@shopify/cli-kit/node/path';
3
+ import { renderSuccess } from '@shopify/cli-kit/node/ui';
4
+ import colors from '@shopify/cli-kit/node/colors';
7
5
  import { commonFlags } from '../../../lib/flags.js';
8
6
  import { Flags, Args } from '@oclif/core';
9
- import { transpileFile, format, resolveFormatConfig } from '../../../lib/transpile-ts.js';
10
- import { getV2Flags, convertRouteToV2, convertTemplateToRemixVersion } from '../../../lib/remix-version-interop.js';
7
+ import { ALL_ROUTE_CHOICES, generateRoutes } from '../../../lib/setups/routes/generate.js';
11
8
 
12
- const GENERATOR_TEMPLATES_DIR = "generator-templates";
13
- const ROUTE_MAP = {
14
- home: "/index",
15
- page: "/pages/$pageHandle",
16
- cart: "/cart",
17
- products: "/products/$productHandle",
18
- collections: "/collections/$collectionHandle",
19
- policies: ["/policies/index", "/policies/$policyHandle"],
20
- robots: "/[robots.txt]",
21
- sitemap: "/[sitemap.xml]",
22
- account: ["/account/login", "/account/register"]
23
- };
24
- const ROUTES = [...Object.keys(ROUTE_MAP), "all"];
25
9
  class GenerateRoute extends Command {
26
10
  static description = "Generates a standard Shopify route.";
27
11
  static flags = {
@@ -38,131 +22,46 @@ class GenerateRoute extends Command {
38
22
  };
39
23
  static hidden;
40
24
  static args = {
41
- route: Args.string({
42
- name: "route",
43
- description: `The route to generate. One of ${ROUTES.join()}.`,
25
+ routeName: Args.string({
26
+ name: "routeName",
27
+ description: `The route to generate. One of ${ALL_ROUTE_CHOICES.join()}.`,
44
28
  required: true,
45
- options: ROUTES,
29
+ options: ALL_ROUTE_CHOICES,
46
30
  env: "SHOPIFY_HYDROGEN_ARG_ROUTE"
47
31
  })
48
32
  };
49
33
  async run() {
50
- const result = /* @__PURE__ */ new Map();
51
34
  const {
52
35
  flags,
53
- args: { route }
36
+ args: { routeName }
54
37
  } = await this.parse(GenerateRoute);
55
- const routePath = route === "all" ? Object.values(ROUTE_MAP).flat() : ROUTE_MAP[route];
56
- if (!routePath) {
57
- throw new AbortError(
58
- `No route found for ${route}. Try one of ${ROUTES.join()}.`
59
- );
60
- }
61
38
  const directory = flags.path ? resolvePath(flags.path) : process.cwd();
62
- const isTypescript = flags.typescript || await fileExists(joinPath(directory, "tsconfig.json"));
63
- const routesArray = Array.isArray(routePath) ? routePath : [routePath];
64
- try {
65
- const { isV2RouteConvention, ...v2Flags } = await getV2Flags(directory);
66
- for (const item of routesArray) {
67
- const routeFrom = item;
68
- const routeTo = isV2RouteConvention ? convertRouteToV2(item) : item;
69
- result.set(
70
- routeTo,
71
- await runGenerate(routeFrom, routeTo, {
72
- directory,
73
- typescript: isTypescript,
74
- force: flags.force,
75
- adapter: flags.adapter,
76
- v2Flags
77
- })
78
- );
79
- }
80
- } catch (err) {
81
- throw new AbortError(err.message);
82
- }
83
- const extension = isTypescript ? ".tsx" : ".jsx";
84
- const success = Array.from(result.values()).filter(
85
- (result2) => result2.operation !== "skipped"
86
- );
87
- renderSuccess({
88
- headline: `${success.length} of ${result.size} route${result.size > 1 ? "s" : ""} generated`,
89
- body: {
90
- list: {
91
- items: Array.from(result.entries()).map(
92
- ([path, { operation }]) => `[${operation}] app/routes${path}${extension}`
93
- )
94
- }
95
- }
39
+ await runGenerate({
40
+ ...flags,
41
+ directory,
42
+ routeName
96
43
  });
97
44
  }
98
45
  }
99
- async function runGenerate(routeFrom, routeTo, {
100
- directory,
101
- typescript,
102
- force,
103
- adapter,
104
- templatesRoot = fileURLToPath(new URL("../../../", import.meta.url)),
105
- v2Flags = {}
106
- }) {
107
- let operation;
108
- const extension = typescript ? ".tsx" : ".jsx";
109
- const templatePath = joinPath(
110
- templatesRoot,
111
- GENERATOR_TEMPLATES_DIR,
112
- "routes",
113
- `${routeFrom}.tsx`
46
+ async function runGenerate(options) {
47
+ const { routes } = await generateRoutes(options);
48
+ const padEnd = 3 + routes.reduce(
49
+ (acc, route) => Math.max(acc, route.destinationRoute.length),
50
+ 0
114
51
  );
115
- const destinationPath = joinPath(
116
- directory,
117
- "app",
118
- "routes",
119
- `${routeTo}${extension}`
120
- );
121
- const relativeDestinationPath = relativePath(directory, destinationPath);
122
- if (!force && await fileExists(destinationPath)) {
123
- const shouldOverwrite = await renderConfirmationPrompt({
124
- message: `The file ${relativizePath(
125
- relativeDestinationPath
126
- )} already exists. Do you want to overwrite it?`,
127
- defaultValue: false
128
- });
129
- operation = shouldOverwrite ? "overwritten" : "skipped";
130
- if (operation === "skipped") {
131
- return { operation };
52
+ const successfulGenerationCount = routes.filter(
53
+ ({ operation }) => operation !== "skipped"
54
+ ).length;
55
+ renderSuccess({
56
+ headline: `${successfulGenerationCount} of ${routes.length} route${routes.length > 1 ? "s" : ""} generated`,
57
+ body: {
58
+ list: {
59
+ items: routes.map(
60
+ ({ operation, destinationRoute }) => destinationRoute.padEnd(padEnd) + colors.dim(`[${operation}]`)
61
+ )
62
+ }
132
63
  }
133
- } else {
134
- operation = "generated";
135
- }
136
- let templateContent = await readFile(templatePath);
137
- templateContent = convertTemplateToRemixVersion(templateContent, v2Flags);
138
- if (!typescript) {
139
- const jsConfigPath = joinPath(directory, "jsconfig.json");
140
- const config = await fileExists(jsConfigPath) ? JSON.parse(
141
- (await readFile(jsConfigPath, { encoding: "utf8" })).replace(
142
- /^\s*\/\/.*$/gm,
143
- ""
144
- )
145
- ) : void 0;
146
- templateContent = transpileFile(templateContent, config?.compilerOptions);
147
- }
148
- if (adapter) {
149
- templateContent = templateContent.replace(
150
- /@shopify\/remix-oxygen/g,
151
- adapter
152
- );
153
- }
154
- templateContent = format(
155
- templateContent,
156
- await resolveFormatConfig(destinationPath),
157
- destinationPath
158
- );
159
- if (!await fileExists(dirname(destinationPath))) {
160
- await mkdir(dirname(destinationPath));
161
- }
162
- await writeFile(destinationPath, templateContent);
163
- return {
164
- operation
165
- };
64
+ });
166
65
  }
167
66
 
168
- export { GENERATOR_TEMPLATES_DIR, GenerateRoute as default, runGenerate };
67
+ export { GenerateRoute as default, runGenerate };
@@ -1,134 +1,42 @@
1
- import { describe, beforeEach, vi, it, expect } from 'vitest';
2
- import { temporaryDirectoryTask } from 'tempy';
3
- import { runGenerate, GENERATOR_TEMPLATES_DIR } from './route.js';
4
- import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
5
- import { readFile, mkdir, writeFile } from '@shopify/cli-kit/node/fs';
6
- import { joinPath, dirname } from '@shopify/cli-kit/node/path';
7
- import { convertRouteToV2 } from '../../../lib/remix-version-interop.js';
1
+ import { describe, beforeEach, vi, afterEach, it, expect } from 'vitest';
2
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
3
+ import { runGenerate } from './route.js';
4
+ import { generateRoutes } from '../../../lib/setups/routes/generate.js';
8
5
 
9
- describe("generate/route", () => {
6
+ describe("runGenerate", () => {
7
+ const outputMock = mockAndCaptureOutput();
10
8
  beforeEach(() => {
11
9
  vi.resetAllMocks();
12
- vi.mock("@shopify/cli-kit/node/output");
13
- vi.mock("@shopify/cli-kit/node/ui");
10
+ vi.mock("../../../lib/setups/routes/generate.js");
14
11
  });
15
- it("generates a route file", async () => {
16
- await temporaryDirectoryTask(async (tmpDir) => {
17
- const route = "pages/$pageHandle";
18
- const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
19
- files: [],
20
- templates: [[route, `const str = "hello world"`]]
21
- });
22
- await runGenerate(route, route, {
23
- directory: appRoot,
24
- templatesRoot
25
- });
26
- expect(
27
- await readFile(joinPath(appRoot, "app/routes", `${route}.jsx`))
28
- ).toContain(`const str = 'hello world'`);
29
- });
30
- });
31
- it("generates a route file for Remix v2", async () => {
32
- await temporaryDirectoryTask(async (tmpDir) => {
33
- const route = "custom/path/$handle/index";
34
- const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
35
- files: [],
36
- templates: [[route, `const str = "hello world"`]]
37
- });
38
- await runGenerate(route, convertRouteToV2(route), {
39
- directory: appRoot,
40
- templatesRoot
41
- });
42
- expect(
43
- await readFile(
44
- joinPath(appRoot, "app/routes", `custom.path.$handle._index.jsx`)
45
- )
46
- ).toContain(`const str = 'hello world'`);
47
- });
48
- });
49
- it("produces a typescript file when typescript argument is true", async () => {
50
- await temporaryDirectoryTask(async (tmpDir) => {
51
- const route = "pages/$pageHandle";
52
- const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
53
- files: [],
54
- templates: [[route, 'const str = "hello typescript"']]
55
- });
56
- await runGenerate(route, route, {
57
- directory: appRoot,
58
- templatesRoot,
59
- typescript: true
60
- });
61
- expect(
62
- await readFile(joinPath(appRoot, "app/routes", `${route}.tsx`))
63
- ).toContain(`const str = 'hello typescript'`);
64
- });
65
- });
66
- it("prompts the user if there the file already exists", async () => {
67
- await temporaryDirectoryTask(async (tmpDir) => {
68
- vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
69
- async () => true
70
- );
71
- const route = "page/$pageHandle";
72
- const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
73
- files: [[`app/routes/${route}.jsx`, 'const str = "I exist"']],
74
- templates: [[route, 'const str = "hello world"']]
75
- });
76
- await runGenerate(route, route, {
77
- directory: appRoot,
78
- templatesRoot
79
- });
80
- expect(renderConfirmationPrompt).toHaveBeenCalledWith(
81
- expect.objectContaining({
82
- message: expect.stringContaining("already exists")
83
- })
84
- );
85
- });
12
+ afterEach(() => {
13
+ vi.resetAllMocks();
14
+ outputMock.clear();
86
15
  });
87
- it("does not prompt the user if the force property is true", async () => {
88
- await temporaryDirectoryTask(async (tmpDir) => {
89
- vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
90
- async () => true
91
- );
92
- const route = "page/$pageHandle";
93
- const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
94
- files: [[`app/routes/${route}.jsx`, 'const str = "I exist"']],
95
- templates: [[route, 'const str = "hello world"']]
96
- });
97
- await runGenerate(route, route, {
98
- directory: appRoot,
99
- templatesRoot,
100
- force: true
101
- });
102
- expect(renderConfirmationPrompt).not.toHaveBeenCalled();
16
+ it("calls route generation and renders the result", async () => {
17
+ vi.mocked(generateRoutes).mockResolvedValue({
18
+ isTypescript: true,
19
+ transpilerOptions: {},
20
+ formatOptions: {},
21
+ v2Flags: {},
22
+ routeGroups: {},
23
+ routes: [
24
+ { sourceRoute: "", destinationRoute: "/cart", operation: "created" },
25
+ { sourceRoute: "", destinationRoute: "/about", operation: "skipped" },
26
+ {
27
+ sourceRoute: "",
28
+ destinationRoute: "/collections",
29
+ operation: "created"
30
+ }
31
+ ]
103
32
  });
33
+ const options = {
34
+ routeName: "all",
35
+ directory: "there",
36
+ typescript: true
37
+ };
38
+ await runGenerate(options);
39
+ expect(generateRoutes).toHaveBeenCalledWith(options);
40
+ expect(outputMock.info()).toMatch(/2 of 3 routes/i);
104
41
  });
105
42
  });
106
- async function createHydrogen(directory, {
107
- files,
108
- templates
109
- } = {
110
- files: [],
111
- templates: []
112
- }) {
113
- for (const item of files) {
114
- const [filePath, fileContent] = item;
115
- const fullFilePath = joinPath(directory, "app", filePath);
116
- await mkdir(dirname(fullFilePath));
117
- await writeFile(fullFilePath, fileContent);
118
- }
119
- for (const item of templates) {
120
- const [filePath, fileContent] = item;
121
- const fullFilePath = joinPath(
122
- directory,
123
- GENERATOR_TEMPLATES_DIR,
124
- "routes",
125
- `${filePath}.tsx`
126
- );
127
- await mkdir(dirname(fullFilePath));
128
- await writeFile(fullFilePath, fileContent);
129
- }
130
- return {
131
- appRoot: joinPath(directory, "app"),
132
- templatesRoot: directory
133
- };
134
- }