@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
@@ -0,0 +1,147 @@
1
+ import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import { inTemporaryDirectory, writeFile } from '@shopify/cli-kit/node/fs';
3
+ import { joinPath } from '@shopify/cli-kit/node/path';
4
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
5
+ import { getAllEnvironmentVariables } from './environment-variables.js';
6
+ import { getStorefrontEnvVariables } from './graphql/admin/pull-variables.js';
7
+ import { login } from './auth.js';
8
+
9
+ vi.mock("./auth.js");
10
+ vi.mock("./graphql/admin/pull-variables.js");
11
+ describe("getAllEnvironmentVariables()", () => {
12
+ const ADMIN_SESSION = {
13
+ token: "abc123",
14
+ storeFqdn: "my-shop"
15
+ };
16
+ const SHOPIFY_CONFIG = {
17
+ shop: "my-shop",
18
+ shopName: "My Shop",
19
+ email: "email",
20
+ storefront: {
21
+ id: "gid://shopify/HydrogenStorefront/1",
22
+ title: "Hydrogen"
23
+ }
24
+ };
25
+ beforeEach(() => {
26
+ vi.mocked(login).mockResolvedValue({
27
+ session: ADMIN_SESSION,
28
+ config: SHOPIFY_CONFIG
29
+ });
30
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
31
+ id: SHOPIFY_CONFIG.storefront.id,
32
+ environmentVariables: [
33
+ {
34
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
35
+ key: "PUBLIC_API_TOKEN",
36
+ value: "abc123",
37
+ isSecret: false
38
+ }
39
+ ]
40
+ });
41
+ });
42
+ afterEach(() => {
43
+ vi.resetAllMocks();
44
+ mockAndCaptureOutput().clear();
45
+ });
46
+ it("calls pullRemoteEnvironmentVariables", async () => {
47
+ await inTemporaryDirectory(async (tmpDir) => {
48
+ await getAllEnvironmentVariables({
49
+ envBranch: "main",
50
+ root: tmpDir
51
+ });
52
+ expect(getStorefrontEnvVariables).toHaveBeenCalledWith(
53
+ ADMIN_SESSION,
54
+ SHOPIFY_CONFIG.storefront.id,
55
+ "main"
56
+ );
57
+ });
58
+ });
59
+ it("does not call pullRemoteEnvironmentVariables when indicated", async () => {
60
+ await inTemporaryDirectory(async (tmpDir) => {
61
+ await getAllEnvironmentVariables({
62
+ envBranch: "main",
63
+ root: tmpDir,
64
+ fetchRemote: false
65
+ });
66
+ expect(getStorefrontEnvVariables).not.toHaveBeenCalled();
67
+ });
68
+ });
69
+ it("renders a message about injection", async () => {
70
+ await inTemporaryDirectory(async (tmpDir) => {
71
+ const outputMock = mockAndCaptureOutput();
72
+ await getAllEnvironmentVariables({ root: tmpDir });
73
+ expect(outputMock.info()).toMatch(
74
+ /Environment variables injected into MiniOxygen:/
75
+ );
76
+ });
77
+ });
78
+ it("lists all of the variables being used", async () => {
79
+ await inTemporaryDirectory(async (tmpDir) => {
80
+ const outputMock = mockAndCaptureOutput();
81
+ await getAllEnvironmentVariables({ root: tmpDir });
82
+ expect(outputMock.info()).toMatch(/PUBLIC_API_TOKEN\s+from Oxygen/);
83
+ });
84
+ });
85
+ it("doest not fail on network errors", async () => {
86
+ await inTemporaryDirectory(async (tmpDir) => {
87
+ vi.mocked(getStorefrontEnvVariables).mockRejectedValue(
88
+ new Error("Network error")
89
+ );
90
+ const outputMock = mockAndCaptureOutput();
91
+ await getAllEnvironmentVariables({ root: tmpDir });
92
+ expect(outputMock.info()).not.toMatch(/PUBLIC_API_TOKEN\s+from Oxygen/);
93
+ expect(outputMock.warn()).toMatch(/failed to load/i);
94
+ });
95
+ });
96
+ describe("when one of the variables is a secret", () => {
97
+ beforeEach(() => {
98
+ vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
99
+ id: SHOPIFY_CONFIG.storefront.id,
100
+ environmentVariables: [
101
+ {
102
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
103
+ key: "PUBLIC_API_TOKEN",
104
+ value: "",
105
+ isSecret: true
106
+ }
107
+ ]
108
+ });
109
+ });
110
+ it("uses special messaging to alert the user", async () => {
111
+ await inTemporaryDirectory(async (tmpDir) => {
112
+ const outputMock = mockAndCaptureOutput();
113
+ await getAllEnvironmentVariables({ root: tmpDir });
114
+ expect(outputMock.info()).toMatch(
115
+ /PUBLIC_API_TOKEN\s+from Oxygen \(Marked as secret\)/
116
+ );
117
+ });
118
+ });
119
+ });
120
+ describe("when there are local variables", () => {
121
+ it("includes local variables in the list", async () => {
122
+ await inTemporaryDirectory(async (tmpDir) => {
123
+ const filePath = joinPath(tmpDir, ".env");
124
+ await writeFile(filePath, "LOCAL_TOKEN=1");
125
+ const outputMock = mockAndCaptureOutput();
126
+ await getAllEnvironmentVariables({ root: tmpDir });
127
+ expect(outputMock.info()).toMatch(/LOCAL_TOKEN\s+from local \.env/);
128
+ });
129
+ });
130
+ describe("and they overwrite remote variables", () => {
131
+ it("uses special messaging to alert the user", async () => {
132
+ await inTemporaryDirectory(async (tmpDir) => {
133
+ const filePath = joinPath(tmpDir, ".env");
134
+ await writeFile(filePath, "PUBLIC_API_TOKEN=abc");
135
+ const outputMock = mockAndCaptureOutput();
136
+ await getAllEnvironmentVariables({ root: tmpDir });
137
+ expect(outputMock.info()).not.toMatch(
138
+ /PUBLIC_API_TOKEN\s+from Oxygen/
139
+ );
140
+ expect(outputMock.info()).toMatch(
141
+ /PUBLIC_API_TOKEN\s+from local \.env/
142
+ );
143
+ });
144
+ });
145
+ });
146
+ });
147
+ });
@@ -0,0 +1,41 @@
1
+ import { resolvePath } from '@shopify/cli-kit/node/path';
2
+ import { readFile, writeFile, isDirectory, fileExists } from '@shopify/cli-kit/node/fs';
3
+ import { readdir } from 'fs/promises';
4
+ import { formatCode } from './format-code.js';
5
+
6
+ async function replaceFileContent(filepath, formatConfig, replacer) {
7
+ let content = await replacer(await readFile(filepath));
8
+ if (typeof content !== "string")
9
+ return;
10
+ if (formatConfig) {
11
+ content = formatCode(content, formatConfig, filepath);
12
+ }
13
+ return writeFile(filepath, content);
14
+ }
15
+ const DEFAULT_EXTENSIONS = ["tsx", "ts", "jsx", "js", "mjs", "cjs"];
16
+ async function findFileWithExtension(directory, fileBase, extensions = DEFAULT_EXTENSIONS) {
17
+ const dirFiles = await readdir(directory);
18
+ if (dirFiles.includes(fileBase)) {
19
+ const filepath = resolvePath(directory, fileBase);
20
+ if (!await isDirectory(filepath)) {
21
+ return { filepath };
22
+ }
23
+ for (const extension of ["ts", "js"]) {
24
+ const filepath2 = resolvePath(directory, `${fileBase}/index.${extension}`);
25
+ if (await fileExists(resolvePath(directory, filepath2))) {
26
+ return { filepath: filepath2, extension, astType: extension };
27
+ }
28
+ }
29
+ } else {
30
+ for (const extension of extensions) {
31
+ const filename = `${fileBase}.${extension}`;
32
+ if (dirFiles.includes(filename)) {
33
+ const astType = extension === "mjs" || extension === "cjs" ? "js" : extension;
34
+ return { filepath: resolvePath(directory, filename), extension, astType };
35
+ }
36
+ }
37
+ }
38
+ return {};
39
+ }
40
+
41
+ export { findFileWithExtension, replaceFileContent };
@@ -0,0 +1,69 @@
1
+ import { temporaryDirectoryTask } from 'tempy';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { replaceFileContent, findFileWithExtension } from './file.js';
4
+ import { resolvePath } from '@shopify/cli-kit/node/path';
5
+ import { writeFile, readFile, mkdir } from '@shopify/cli-kit/node/fs';
6
+
7
+ describe("File utils", () => {
8
+ describe("replaceFileContent", () => {
9
+ it("replaces the content of a file and formats it", async () => {
10
+ await temporaryDirectoryTask(async (tmpDir) => {
11
+ const filepath = resolvePath(tmpDir, "index.js");
12
+ await writeFile(
13
+ filepath,
14
+ 'function foo() { console.log("foo"); return null}'
15
+ );
16
+ await replaceFileContent(filepath, {}, async (content) => {
17
+ return content.replaceAll("foo", "bar");
18
+ });
19
+ expect(await readFile(filepath)).toBe(
20
+ 'function bar() {\n console.log("bar");\n return null;\n}\n'
21
+ );
22
+ });
23
+ });
24
+ });
25
+ describe("findFileWithExtension", () => {
26
+ it("ignores missing files", async () => {
27
+ await temporaryDirectoryTask(async (tmpDir) => {
28
+ expect(findFileWithExtension(tmpDir, "nope")).resolves.toEqual({
29
+ filepath: void 0,
30
+ extension: void 0,
31
+ astType: void 0
32
+ });
33
+ });
34
+ });
35
+ it("finds the file with its corresponding extension and astType", async () => {
36
+ await temporaryDirectoryTask(async (tmpDir) => {
37
+ await writeFile(resolvePath(tmpDir, "first.js"), "content");
38
+ await writeFile(resolvePath(tmpDir, "second.tsx"), "content");
39
+ await writeFile(resolvePath(tmpDir, "third.mjs"), "content");
40
+ await writeFile(resolvePath(tmpDir, "fourth"), "content");
41
+ await mkdir(resolvePath(tmpDir, "fifth"));
42
+ await writeFile(resolvePath(tmpDir, "fifth", "index.ts"), "content");
43
+ expect(findFileWithExtension(tmpDir, "first")).resolves.toEqual({
44
+ filepath: expect.stringMatching(/first\.js$/),
45
+ extension: "js",
46
+ astType: "js"
47
+ });
48
+ expect(findFileWithExtension(tmpDir, "second")).resolves.toEqual({
49
+ filepath: expect.stringMatching(/second\.tsx$/),
50
+ extension: "tsx",
51
+ astType: "tsx"
52
+ });
53
+ expect(findFileWithExtension(tmpDir, "third")).resolves.toEqual({
54
+ filepath: expect.stringMatching(/third\.mjs$/),
55
+ extension: "mjs",
56
+ astType: "js"
57
+ });
58
+ expect(findFileWithExtension(tmpDir, "fourth")).resolves.toEqual({
59
+ filepath: expect.stringMatching(/fourth$/)
60
+ });
61
+ expect(findFileWithExtension(tmpDir, "fifth")).resolves.toEqual({
62
+ filepath: expect.stringMatching(/fifth\/index\.ts$/),
63
+ extension: "ts",
64
+ astType: "ts"
65
+ });
66
+ });
67
+ });
68
+ });
69
+ });
package/dist/lib/flags.js CHANGED
@@ -3,6 +3,8 @@ import { camelize } from '@shopify/cli-kit/common/string';
3
3
  import { renderInfo } from '@shopify/cli-kit/node/ui';
4
4
  import { normalizeStoreFqdn } from '@shopify/cli-kit/node/context/fqdn';
5
5
  import colors from '@shopify/cli-kit/node/colors';
6
+ import { SETUP_CSS_STRATEGIES } from './setups/css/index.js';
7
+ import { I18N_CHOICES } from './setups/i18n/index.js';
6
8
 
7
9
  const commonFlags = {
8
10
  path: Flags.string({
@@ -25,7 +27,12 @@ const commonFlags = {
25
27
  env: "SHOPIFY_SHOP",
26
28
  parse: async (input) => normalizeStoreFqdn(input)
27
29
  }),
28
- ["env-branch"]: Flags.string({
30
+ installDeps: Flags.boolean({
31
+ description: "Auto install dependencies using the active package manager",
32
+ env: "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS",
33
+ allowNo: true
34
+ }),
35
+ envBranch: Flags.string({
29
36
  description: "Specify an environment's branch name when using remote environment variables.",
30
37
  env: "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH",
31
38
  char: "e"
@@ -35,6 +42,30 @@ const commonFlags = {
35
42
  env: "SHOPIFY_HYDROGEN_FLAG_SOURCEMAP",
36
43
  default: true,
37
44
  allowNo: true
45
+ }),
46
+ codegenConfigPath: Flags.string({
47
+ description: "Specify a path to a codegen configuration file. Defaults to `<root>/codegen.ts` if it exists.",
48
+ required: false,
49
+ dependsOn: ["codegen-unstable"]
50
+ }),
51
+ styling: Flags.string({
52
+ description: `Sets the styling strategy to use. One of ${SETUP_CSS_STRATEGIES.map(
53
+ (item) => `\`${item}\``
54
+ ).join(", ")}.`,
55
+ choices: SETUP_CSS_STRATEGIES,
56
+ env: "SHOPIFY_HYDROGEN_FLAG_STYLING"
57
+ }),
58
+ markets: Flags.string({
59
+ description: `Sets the URL structure to support multiple markets. One of ${I18N_CHOICES.map(
60
+ (item) => `\`${item}\``
61
+ ).join(", ")}.`,
62
+ choices: I18N_CHOICES,
63
+ env: "SHOPIFY_HYDROGEN_FLAG_I18N"
64
+ }),
65
+ shortcut: Flags.boolean({
66
+ description: "Create a shortcut to the Shopify Hydrogen CLI.",
67
+ env: "SHOPIFY_HYDROGEN_FLAG_SHORTCUT",
68
+ allowNo: true
38
69
  })
39
70
  };
40
71
  function flagsToCamelObject(obj) {
@@ -74,5 +105,11 @@ function deprecated(name) {
74
105
  hidden: true
75
106
  });
76
107
  }
108
+ function overrideFlag(flag, extra) {
109
+ return {
110
+ ...flag,
111
+ ...extra
112
+ };
113
+ }
77
114
 
78
- export { commonFlags, deprecated, flagsToCamelObject, parseProcessFlags };
115
+ export { commonFlags, deprecated, flagsToCamelObject, overrideFlag, parseProcessFlags };
@@ -0,0 +1,26 @@
1
+ import prettier from 'prettier';
2
+ import { extname } from '@shopify/cli-kit/node/path';
3
+
4
+ const DEFAULT_PRETTIER_CONFIG = {
5
+ arrowParens: "always",
6
+ singleQuote: true,
7
+ bracketSpacing: false,
8
+ trailingComma: "all"
9
+ };
10
+ async function getCodeFormatOptions(filePath = process.cwd()) {
11
+ try {
12
+ return await prettier.resolveConfig(filePath) || DEFAULT_PRETTIER_CONFIG;
13
+ } catch {
14
+ return DEFAULT_PRETTIER_CONFIG;
15
+ }
16
+ }
17
+ function formatCode(content, config = DEFAULT_PRETTIER_CONFIG, filePath = "") {
18
+ const ext = extname(filePath);
19
+ const formattedContent = prettier.format(content, {
20
+ parser: ext === ".tsx" || ext === ".ts" ? "typescript" : "babel",
21
+ ...config
22
+ });
23
+ return formattedContent;
24
+ }
25
+
26
+ export { formatCode, getCodeFormatOptions };
@@ -0,0 +1,12 @@
1
+ import { AbortError } from '@shopify/cli-kit/node/error';
2
+
3
+ const GID_REGEXP = /gid:\/\/shopify\/\w*\/(\d+)/;
4
+ function parseGid(gid) {
5
+ const matches = GID_REGEXP.exec(gid);
6
+ if (matches && matches[1] !== void 0) {
7
+ return matches[1];
8
+ }
9
+ throw new AbortError(`Invalid Global ID: ${gid}`);
10
+ }
11
+
12
+ export { parseGid };
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { AbortError } from '@shopify/cli-kit/node/error';
3
- import { parseGid } from './graphql.js';
3
+ import { parseGid } from './gid.js';
4
4
 
5
5
  describe("parseGid", () => {
6
6
  it("returns an ID", () => {
@@ -0,0 +1,27 @@
1
+ import { AbortError } from '@shopify/cli-kit/node/error';
2
+ import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
3
+
4
+ async function adminRequest(query, session, variables) {
5
+ const api = "Admin";
6
+ const url = `https://${session.storeFqdn}/admin/api/unstable/graphql.json`;
7
+ try {
8
+ return await graphqlRequest({
9
+ query,
10
+ api,
11
+ url,
12
+ token: session.token,
13
+ variables
14
+ });
15
+ } catch (error) {
16
+ const errors = error.errors;
17
+ if (errors?.some?.((error2) => error2.message.includes("app is not installed"))) {
18
+ throw new AbortError(
19
+ "Hydrogen sales channel isn't installed",
20
+ "Install the Hydrogen sales channel on your store to start creating and linking Hydrogen storefronts: https://apps.shopify.com/hydrogen"
21
+ );
22
+ }
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ export { adminRequest };
@@ -0,0 +1,51 @@
1
+ import { AbortError } from '@shopify/cli-kit/node/error';
2
+ import { vi, describe, it, expect } from 'vitest';
3
+ import { adminRequest } from './client.js';
4
+ import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
5
+
6
+ vi.mock("@shopify/cli-kit/node/api/graphql");
7
+ describe("adminRequest", () => {
8
+ it("sends a query to the Admin API and returns the successful response", async () => {
9
+ const fakeResponse = {
10
+ test: "test"
11
+ };
12
+ vi.mocked(graphqlRequest).mockResolvedValue(fakeResponse);
13
+ const response = await adminRequest("", {
14
+ token: "",
15
+ storeFqdn: ""
16
+ });
17
+ expect(response).toContain(fakeResponse);
18
+ });
19
+ describe("error response", () => {
20
+ it("sends a query to the Admin API and returns an unknown error response", async () => {
21
+ const fakeGraphqlError = {
22
+ errors: [
23
+ {
24
+ message: "test error"
25
+ }
26
+ ]
27
+ };
28
+ vi.mocked(graphqlRequest).mockRejectedValue(fakeGraphqlError);
29
+ const response = adminRequest("", {
30
+ token: "",
31
+ storeFqdn: ""
32
+ });
33
+ await expect(response).rejects.toContain(fakeGraphqlError);
34
+ });
35
+ it("sends a query to the Admin API and returns an error where app isn't installed", async () => {
36
+ const fakeGraphqlError = {
37
+ errors: [
38
+ {
39
+ message: "app is not installed"
40
+ }
41
+ ]
42
+ };
43
+ vi.mocked(graphqlRequest).mockRejectedValue(fakeGraphqlError);
44
+ const response = adminRequest("", {
45
+ token: "",
46
+ storeFqdn: ""
47
+ });
48
+ await expect(response).rejects.toThrowError(AbortError);
49
+ });
50
+ });
51
+ });
@@ -1,5 +1,5 @@
1
- import { adminRequest } from '../../graphql.js';
2
- import { getAdminSession } from '../../admin-session.js';
1
+ import { AbortError } from '@shopify/cli-kit/node/error';
2
+ import { adminRequest } from './client.js';
3
3
 
4
4
  const CreateStorefrontMutation = `#graphql
5
5
  mutation CreateStorefront($title: String!) {
@@ -18,21 +18,19 @@ const CreateStorefrontMutation = `#graphql
18
18
  }
19
19
  }
20
20
  `;
21
- async function createStorefront(shop, title) {
22
- const adminSession = await getAdminSession(shop);
23
- const { hydrogenStorefrontCreate } = await adminRequest(
21
+ async function createStorefront(adminSession, title) {
22
+ const {
23
+ hydrogenStorefrontCreate: { hydrogenStorefront, userErrors, jobId }
24
+ } = await adminRequest(
24
25
  CreateStorefrontMutation,
25
26
  adminSession,
26
- {
27
- title
28
- }
27
+ { title }
29
28
  );
30
- return {
31
- adminSession,
32
- storefront: hydrogenStorefrontCreate.hydrogenStorefront,
33
- userErrors: hydrogenStorefrontCreate.userErrors,
34
- jobId: hydrogenStorefrontCreate.jobId
35
- };
29
+ if (!hydrogenStorefront || !jobId || userErrors.length > 0) {
30
+ const errorMessages = userErrors.map(({ message }) => message).join(", ");
31
+ throw new AbortError("Could not create storefront. " + errorMessages);
32
+ }
33
+ return { jobId, storefront: hydrogenStorefront };
36
34
  }
37
35
 
38
- export { CreateStorefrontMutation, createStorefront };
36
+ export { createStorefront };
@@ -0,0 +1,64 @@
1
+ import { vi, describe, afterEach, it, expect } from 'vitest';
2
+ import { adminRequest } from './client.js';
3
+ import { createStorefront } from './create-storefront.js';
4
+
5
+ vi.mock("./client.js");
6
+ describe("createStorefront", () => {
7
+ const ADMIN_SESSION = {
8
+ token: "abc123",
9
+ storeFqdn: "my-shop.myshopify.com"
10
+ };
11
+ afterEach(() => {
12
+ vi.resetAllMocks();
13
+ });
14
+ it("sends a mutation to create a new storefront and returns it", async () => {
15
+ vi.mocked(adminRequest).mockImplementation(
16
+ (_, __, variables) => Promise.resolve({
17
+ hydrogenStorefrontCreate: {
18
+ hydrogenStorefront: {
19
+ id: "gid://shopify/HydrogenStorefront/123",
20
+ title: variables?.title,
21
+ productionUrl: "https://..."
22
+ },
23
+ userErrors: [],
24
+ jobId: "123"
25
+ }
26
+ })
27
+ );
28
+ const TITLE = "title";
29
+ await expect(createStorefront(ADMIN_SESSION, TITLE)).resolves.toStrictEqual(
30
+ {
31
+ jobId: "123",
32
+ storefront: {
33
+ id: "gid://shopify/HydrogenStorefront/123",
34
+ title: TITLE,
35
+ productionUrl: "https://..."
36
+ }
37
+ }
38
+ );
39
+ expect(adminRequest).toHaveBeenCalledWith(
40
+ expect.stringMatching(/^#graphql.+mutation.+hydrogenStorefrontCreate\(/s),
41
+ ADMIN_SESSION,
42
+ { title: TITLE }
43
+ );
44
+ });
45
+ it("throws formatted GraphQL errors", async () => {
46
+ const error = "Title is invalid";
47
+ vi.mocked(adminRequest).mockResolvedValue({
48
+ hydrogenStorefrontCreate: {
49
+ jobId: void 0,
50
+ hydrogenStorefront: void 0,
51
+ userErrors: [
52
+ {
53
+ code: "INVALID",
54
+ field: ["title"],
55
+ message: error
56
+ }
57
+ ]
58
+ }
59
+ });
60
+ await expect(createStorefront(ADMIN_SESSION, "title")).rejects.toThrow(
61
+ error
62
+ );
63
+ });
64
+ });
@@ -1,5 +1,4 @@
1
- import { adminRequest } from '../../graphql.js';
2
- import { getAdminSession } from '../../admin-session.js';
1
+ import { adminRequest } from './client.js';
3
2
 
4
3
  const FetchJobQuery = `#graphql
5
4
  query FetchJob($id: ID!) {
@@ -13,26 +12,18 @@ const FetchJobQuery = `#graphql
13
12
  }
14
13
  }
15
14
  `;
16
- async function fetchJob(shop, jobId) {
17
- const adminSession = await getAdminSession(shop);
15
+ async function fetchJob(adminSession, jobId) {
18
16
  const { hydrogenStorefrontJob } = await adminRequest(
19
17
  FetchJobQuery,
20
18
  adminSession,
21
- {
22
- id: jobId
23
- }
19
+ { id: jobId }
24
20
  );
25
- return {
26
- adminSession,
27
- id: hydrogenStorefrontJob.id,
28
- done: hydrogenStorefrontJob.done,
29
- errors: hydrogenStorefrontJob.errors
30
- };
21
+ return hydrogenStorefrontJob;
31
22
  }
32
- function waitForJob(shop, jobId) {
23
+ function waitForJob(adminSession, jobId) {
33
24
  return new Promise((resolve, reject) => {
34
25
  const interval = setInterval(async () => {
35
- const job = await fetchJob(shop, jobId);
26
+ const job = await fetchJob(adminSession, jobId);
36
27
  if (job.errors.length > 0) {
37
28
  clearInterval(interval);
38
29
  return reject();
@@ -1,5 +1,5 @@
1
- import { adminRequest, parseGid } from '../../graphql.js';
2
- import { getAdminSession } from '../../admin-session.js';
1
+ import { adminRequest } from './client.js';
2
+ import { parseGid } from '../../gid.js';
3
3
 
4
4
  const LinkStorefrontQuery = `#graphql
5
5
  query LinkStorefront {
@@ -10,19 +10,15 @@ const LinkStorefrontQuery = `#graphql
10
10
  }
11
11
  }
12
12
  `;
13
- async function getStorefronts(shop) {
14
- const adminSession = await getAdminSession(shop);
13
+ async function getStorefronts(adminSession) {
15
14
  const { hydrogenStorefronts } = await adminRequest(
16
15
  LinkStorefrontQuery,
17
16
  adminSession
18
17
  );
19
- return {
20
- adminSession,
21
- storefronts: hydrogenStorefronts.map((storefront) => ({
22
- ...storefront,
23
- parsedId: parseGid(storefront.id)
24
- }))
25
- };
18
+ return hydrogenStorefronts.map((storefront) => ({
19
+ ...storefront,
20
+ parsedId: parseGid(storefront.id)
21
+ }));
26
22
  }
27
23
 
28
24
  export { LinkStorefrontQuery, getStorefronts };
@@ -0,0 +1,38 @@
1
+ import { vi, describe, afterEach, it, expect } from 'vitest';
2
+ import { adminRequest } from './client.js';
3
+ import { getStorefronts } from './link-storefront.js';
4
+
5
+ vi.mock("./client.js");
6
+ describe("getStorefrontsWithDeployment", () => {
7
+ const ADMIN_SESSION = {
8
+ token: "abc123",
9
+ storeFqdn: "my-shop.myshopify.com"
10
+ };
11
+ afterEach(() => {
12
+ vi.resetAllMocks();
13
+ });
14
+ it("calls the graphql client and returns Hydrogen storefronts", async () => {
15
+ const mockedResponse = {
16
+ hydrogenStorefronts: [
17
+ {
18
+ id: "gid://shopify/HydrogenStorefront/123",
19
+ title: "title",
20
+ productionUrl: "https://..."
21
+ }
22
+ ]
23
+ };
24
+ vi.mocked(adminRequest).mockResolvedValue(
25
+ mockedResponse
26
+ );
27
+ await expect(getStorefronts(ADMIN_SESSION)).resolves.toStrictEqual([
28
+ {
29
+ ...mockedResponse.hydrogenStorefronts[0],
30
+ parsedId: "123"
31
+ }
32
+ ]);
33
+ expect(adminRequest).toHaveBeenCalledWith(
34
+ expect.stringMatching(/^#graphql.+query.+hydrogenStorefronts\s*{/s),
35
+ ADMIN_SESSION
36
+ );
37
+ });
38
+ });