@shopify/cli-hydrogen 5.0.2 → 5.1.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 (249) hide show
  1. package/dist/commands/hydrogen/build.js +21 -6
  2. package/dist/commands/hydrogen/check.js +2 -2
  3. package/dist/commands/hydrogen/codegen-unstable.js +14 -25
  4. package/dist/commands/hydrogen/dev.js +55 -43
  5. package/dist/commands/hydrogen/env/list.js +25 -24
  6. package/dist/commands/hydrogen/env/list.test.js +46 -43
  7. package/dist/commands/hydrogen/env/pull.js +53 -25
  8. package/dist/commands/hydrogen/env/pull.test.js +123 -42
  9. package/dist/commands/hydrogen/generate/route.js +31 -132
  10. package/dist/commands/hydrogen/generate/route.test.js +34 -126
  11. package/dist/commands/hydrogen/init.js +46 -127
  12. package/dist/commands/hydrogen/init.test.js +352 -100
  13. package/dist/commands/hydrogen/link.js +70 -69
  14. package/dist/commands/hydrogen/link.test.js +72 -107
  15. package/dist/commands/hydrogen/list.js +22 -12
  16. package/dist/commands/hydrogen/list.test.js +51 -48
  17. package/dist/commands/hydrogen/login.js +31 -0
  18. package/dist/commands/hydrogen/logout.js +21 -0
  19. package/dist/commands/hydrogen/preview.js +1 -1
  20. package/dist/commands/hydrogen/setup/css.js +79 -0
  21. package/dist/commands/hydrogen/setup/markets.js +53 -0
  22. package/dist/commands/hydrogen/setup.js +133 -0
  23. package/dist/commands/hydrogen/shortcut.js +2 -45
  24. package/dist/commands/hydrogen/shortcut.test.js +10 -37
  25. package/dist/generator-templates/assets/css-modules/package.json +6 -0
  26. package/dist/generator-templates/assets/postcss/package.json +10 -0
  27. package/dist/generator-templates/assets/postcss/postcss.config.js +8 -0
  28. package/dist/generator-templates/assets/tailwind/package.json +13 -0
  29. package/dist/generator-templates/assets/tailwind/postcss.config.js +10 -0
  30. package/dist/generator-templates/assets/tailwind/tailwind.config.js +8 -0
  31. package/dist/generator-templates/assets/tailwind/tailwind.css +3 -0
  32. package/dist/generator-templates/assets/vanilla-extract/package.json +9 -0
  33. package/dist/generator-templates/starter/.eslintignore +5 -0
  34. package/dist/generator-templates/starter/.eslintrc.js +18 -0
  35. package/dist/generator-templates/starter/.graphqlrc.yml +1 -0
  36. package/dist/generator-templates/starter/README.md +40 -0
  37. package/dist/generator-templates/starter/app/components/Aside.tsx +47 -0
  38. package/dist/generator-templates/starter/app/components/Cart.tsx +340 -0
  39. package/dist/generator-templates/starter/app/components/Footer.tsx +99 -0
  40. package/dist/generator-templates/starter/app/components/Header.tsx +178 -0
  41. package/dist/generator-templates/starter/app/components/Layout.tsx +95 -0
  42. package/dist/generator-templates/starter/app/components/Search.tsx +480 -0
  43. package/dist/generator-templates/starter/app/entry.client.tsx +12 -0
  44. package/dist/generator-templates/starter/app/entry.server.tsx +33 -0
  45. package/dist/generator-templates/starter/app/root.tsx +264 -0
  46. package/dist/generator-templates/starter/app/routes/$.tsx +7 -0
  47. package/dist/generator-templates/{routes → starter/app/routes}/[robots.txt].tsx +47 -69
  48. package/dist/generator-templates/starter/app/routes/[sitemap.xml].tsx +174 -0
  49. package/dist/generator-templates/starter/app/routes/_index.tsx +145 -0
  50. package/dist/generator-templates/starter/app/routes/account.$.tsx +9 -0
  51. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +563 -0
  52. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +309 -0
  53. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +196 -0
  54. package/dist/generator-templates/starter/app/routes/account.profile.tsx +289 -0
  55. package/dist/generator-templates/starter/app/routes/account.tsx +203 -0
  56. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +157 -0
  57. package/dist/generator-templates/starter/app/routes/account_.login.tsx +143 -0
  58. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +33 -0
  59. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +124 -0
  60. package/dist/generator-templates/starter/app/routes/account_.register.tsx +207 -0
  61. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +136 -0
  62. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +342 -0
  63. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +88 -0
  64. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +162 -0
  65. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +94 -0
  66. package/dist/generator-templates/starter/app/routes/cart.tsx +104 -0
  67. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +184 -0
  68. package/dist/generator-templates/starter/app/routes/collections._index.tsx +120 -0
  69. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +57 -0
  70. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +94 -0
  71. package/dist/generator-templates/starter/app/routes/policies._index.tsx +63 -0
  72. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +418 -0
  73. package/dist/generator-templates/starter/app/routes/search.tsx +168 -0
  74. package/dist/generator-templates/starter/app/styles/app.css +473 -0
  75. package/dist/generator-templates/starter/app/styles/reset.css +129 -0
  76. package/dist/generator-templates/starter/app/utils.ts +46 -0
  77. package/dist/generator-templates/starter/package.json +43 -0
  78. package/dist/generator-templates/starter/public/favicon.svg +28 -0
  79. package/dist/generator-templates/starter/remix.config.js +26 -0
  80. package/dist/generator-templates/starter/remix.env.d.ts +39 -0
  81. package/dist/generator-templates/starter/server.ts +253 -0
  82. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +1906 -0
  83. package/dist/generator-templates/starter/tsconfig.json +22 -0
  84. package/dist/lib/auth.js +123 -0
  85. package/dist/lib/auth.test.js +157 -0
  86. package/dist/lib/build.js +51 -0
  87. package/dist/lib/check-version.js +3 -3
  88. package/dist/lib/check-version.test.js +24 -0
  89. package/dist/lib/codegen.js +26 -17
  90. package/dist/lib/environment-variables.js +68 -0
  91. package/dist/lib/environment-variables.test.js +147 -0
  92. package/dist/lib/file.js +41 -0
  93. package/dist/lib/file.test.js +69 -0
  94. package/dist/lib/flags.js +39 -2
  95. package/dist/lib/format-code.js +26 -0
  96. package/dist/lib/gid.js +12 -0
  97. package/dist/lib/{graphql.test.js → gid.test.js} +1 -1
  98. package/dist/lib/graphql/admin/client.js +27 -0
  99. package/dist/lib/graphql/admin/client.test.js +51 -0
  100. package/dist/lib/graphql/admin/create-storefront.js +13 -15
  101. package/dist/lib/graphql/admin/create-storefront.test.js +64 -0
  102. package/dist/lib/graphql/admin/fetch-job.js +6 -15
  103. package/dist/lib/graphql/admin/link-storefront.js +7 -11
  104. package/dist/lib/graphql/admin/link-storefront.test.js +38 -0
  105. package/dist/lib/graphql/admin/list-environments.js +2 -2
  106. package/dist/lib/graphql/admin/list-environments.test.js +44 -0
  107. package/dist/lib/graphql/admin/list-storefronts.js +7 -11
  108. package/dist/lib/graphql/admin/list-storefronts.test.js +44 -0
  109. package/dist/lib/graphql/admin/pull-variables.js +3 -3
  110. package/dist/lib/graphql/admin/pull-variables.test.js +37 -0
  111. package/dist/lib/graphql/business-platform/user-account.js +83 -0
  112. package/dist/lib/graphql/business-platform/user-account.test.js +80 -0
  113. package/dist/lib/log.js +216 -9
  114. package/dist/lib/log.test.js +92 -0
  115. package/dist/lib/mini-oxygen.js +19 -9
  116. package/dist/lib/missing-routes.js +0 -2
  117. package/dist/lib/onboarding/common.js +456 -0
  118. package/dist/lib/onboarding/index.js +2 -0
  119. package/dist/lib/onboarding/local.js +229 -0
  120. package/dist/lib/onboarding/remote.js +89 -0
  121. package/dist/lib/remix-config.js +135 -0
  122. package/dist/lib/remix-version-check.js +51 -0
  123. package/dist/lib/remix-version-check.test.js +38 -0
  124. package/dist/lib/remix-version-interop.js +6 -6
  125. package/dist/lib/remix-version-interop.test.js +12 -2
  126. package/dist/lib/render-errors.js +13 -11
  127. package/dist/lib/setups/css/assets.js +89 -0
  128. package/dist/lib/setups/css/css-modules.js +22 -0
  129. package/dist/lib/setups/css/index.js +44 -0
  130. package/dist/lib/setups/css/postcss.js +34 -0
  131. package/dist/lib/setups/css/replacers.js +137 -0
  132. package/dist/lib/setups/css/tailwind.js +54 -0
  133. package/dist/lib/setups/css/vanilla-extract.js +22 -0
  134. package/dist/lib/setups/i18n/domains.test.js +25 -0
  135. package/dist/lib/setups/i18n/index.js +46 -0
  136. package/dist/lib/setups/i18n/replacers.js +227 -0
  137. package/dist/lib/setups/i18n/subdomains.test.js +25 -0
  138. package/dist/lib/setups/i18n/subfolders.test.js +25 -0
  139. package/dist/lib/setups/i18n/templates/domains.js +14 -0
  140. package/dist/lib/setups/i18n/templates/domains.ts +25 -0
  141. package/dist/lib/setups/i18n/templates/subdomains.js +14 -0
  142. package/dist/lib/setups/i18n/templates/subdomains.ts +24 -0
  143. package/dist/lib/setups/i18n/templates/subfolders.js +14 -0
  144. package/dist/lib/setups/i18n/templates/subfolders.ts +28 -0
  145. package/dist/lib/setups/routes/generate.js +244 -0
  146. package/dist/lib/setups/routes/generate.test.js +313 -0
  147. package/dist/lib/shell.js +52 -5
  148. package/dist/lib/shell.test.js +42 -16
  149. package/dist/lib/shopify-config.js +23 -18
  150. package/dist/lib/shopify-config.test.js +63 -73
  151. package/dist/lib/template-downloader.js +9 -7
  152. package/dist/lib/transpile-ts.js +9 -29
  153. package/dist/virtual-routes/routes/index.jsx +40 -19
  154. package/oclif.manifest.json +710 -1
  155. package/package.json +20 -21
  156. package/dist/commands/hydrogen/build.d.ts +0 -23
  157. package/dist/commands/hydrogen/check.d.ts +0 -15
  158. package/dist/commands/hydrogen/codegen-unstable.d.ts +0 -15
  159. package/dist/commands/hydrogen/dev.d.ts +0 -21
  160. package/dist/commands/hydrogen/env/list.d.ts +0 -18
  161. package/dist/commands/hydrogen/env/pull.d.ts +0 -22
  162. package/dist/commands/hydrogen/g.d.ts +0 -10
  163. package/dist/commands/hydrogen/generate/route.d.ts +0 -32
  164. package/dist/commands/hydrogen/generate/route.test.d.ts +0 -1
  165. package/dist/commands/hydrogen/generate/routes.d.ts +0 -16
  166. package/dist/commands/hydrogen/init.d.ts +0 -24
  167. package/dist/commands/hydrogen/init.test.d.ts +0 -1
  168. package/dist/commands/hydrogen/link.d.ts +0 -23
  169. package/dist/commands/hydrogen/link.test.d.ts +0 -1
  170. package/dist/commands/hydrogen/list.d.ts +0 -21
  171. package/dist/commands/hydrogen/list.test.d.ts +0 -1
  172. package/dist/commands/hydrogen/preview.d.ts +0 -17
  173. package/dist/commands/hydrogen/shortcut.d.ts +0 -9
  174. package/dist/commands/hydrogen/shortcut.test.d.ts +0 -1
  175. package/dist/commands/hydrogen/unlink.d.ts +0 -16
  176. package/dist/commands/hydrogen/unlink.test.d.ts +0 -1
  177. package/dist/create-app.d.ts +0 -1
  178. package/dist/generator-templates/routes/[sitemap.xml].tsx +0 -235
  179. package/dist/generator-templates/routes/account/login.tsx +0 -103
  180. package/dist/generator-templates/routes/account/register.tsx +0 -103
  181. package/dist/generator-templates/routes/cart.tsx +0 -81
  182. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +0 -104
  183. package/dist/generator-templates/routes/collections/index.tsx +0 -102
  184. package/dist/generator-templates/routes/graphiql.tsx +0 -10
  185. package/dist/generator-templates/routes/index.tsx +0 -40
  186. package/dist/generator-templates/routes/pages/$pageHandle.tsx +0 -112
  187. package/dist/generator-templates/routes/policies/$policyHandle.tsx +0 -140
  188. package/dist/generator-templates/routes/policies/index.tsx +0 -117
  189. package/dist/generator-templates/routes/products/$productHandle.tsx +0 -92
  190. package/dist/hooks/init.d.ts +0 -5
  191. package/dist/lib/admin-session.d.ts +0 -6
  192. package/dist/lib/admin-session.js +0 -16
  193. package/dist/lib/admin-session.test.d.ts +0 -1
  194. package/dist/lib/admin-session.test.js +0 -27
  195. package/dist/lib/admin-urls.d.ts +0 -8
  196. package/dist/lib/check-lockfile.d.ts +0 -3
  197. package/dist/lib/check-lockfile.test.d.ts +0 -1
  198. package/dist/lib/check-version.d.ts +0 -16
  199. package/dist/lib/check-version.test.d.ts +0 -1
  200. package/dist/lib/codegen.d.ts +0 -26
  201. package/dist/lib/combined-environment-variables.d.ts +0 -8
  202. package/dist/lib/combined-environment-variables.js +0 -57
  203. package/dist/lib/combined-environment-variables.test.d.ts +0 -1
  204. package/dist/lib/combined-environment-variables.test.js +0 -111
  205. package/dist/lib/config.d.ts +0 -20
  206. package/dist/lib/config.js +0 -141
  207. package/dist/lib/flags.d.ts +0 -27
  208. package/dist/lib/flags.test.d.ts +0 -1
  209. package/dist/lib/graphql/admin/create-storefront.d.ts +0 -17
  210. package/dist/lib/graphql/admin/fetch-job.d.ts +0 -23
  211. package/dist/lib/graphql/admin/link-storefront.d.ts +0 -14
  212. package/dist/lib/graphql/admin/list-environments.d.ts +0 -21
  213. package/dist/lib/graphql/admin/list-storefronts.d.ts +0 -25
  214. package/dist/lib/graphql/admin/pull-variables.d.ts +0 -21
  215. package/dist/lib/graphql.d.ts +0 -21
  216. package/dist/lib/graphql.js +0 -18
  217. package/dist/lib/graphql.test.d.ts +0 -1
  218. package/dist/lib/log.d.ts +0 -6
  219. package/dist/lib/mini-oxygen.d.ts +0 -22
  220. package/dist/lib/missing-routes.d.ts +0 -8
  221. package/dist/lib/missing-routes.test.d.ts +0 -1
  222. package/dist/lib/missing-storefronts.d.ts +0 -5
  223. package/dist/lib/missing-storefronts.js +0 -18
  224. package/dist/lib/process.d.ts +0 -6
  225. package/dist/lib/pull-environment-variables.d.ts +0 -20
  226. package/dist/lib/pull-environment-variables.js +0 -57
  227. package/dist/lib/pull-environment-variables.test.d.ts +0 -1
  228. package/dist/lib/pull-environment-variables.test.js +0 -174
  229. package/dist/lib/remix-version-interop.d.ts +0 -11
  230. package/dist/lib/remix-version-interop.test.d.ts +0 -1
  231. package/dist/lib/render-errors.d.ts +0 -16
  232. package/dist/lib/shell.d.ts +0 -11
  233. package/dist/lib/shell.test.d.ts +0 -1
  234. package/dist/lib/shop.d.ts +0 -7
  235. package/dist/lib/shop.js +0 -32
  236. package/dist/lib/shop.test.d.ts +0 -1
  237. package/dist/lib/shop.test.js +0 -78
  238. package/dist/lib/shopify-config.d.ts +0 -35
  239. package/dist/lib/shopify-config.test.d.ts +0 -1
  240. package/dist/lib/string.d.ts +0 -3
  241. package/dist/lib/string.test.d.ts +0 -1
  242. package/dist/lib/template-downloader.d.ts +0 -6
  243. package/dist/lib/transpile-ts.d.ts +0 -16
  244. package/dist/lib/user-errors.d.ts +0 -9
  245. package/dist/lib/user-errors.js +0 -11
  246. package/dist/lib/virtual-routes.d.ts +0 -7
  247. package/dist/lib/virtual-routes.test.d.ts +0 -1
  248. /package/dist/{commands/hydrogen/env/list.test.d.ts → lib/setups/css/common.js} +0 -0
  249. /package/dist/{commands/hydrogen/env/pull.test.d.ts → lib/setups/i18n/mock-i18n-types.js} +0 -0
@@ -0,0 +1,313 @@
1
+ import { describe, beforeEach, vi, it, expect } from 'vitest';
2
+ import { temporaryDirectoryTask } from 'tempy';
3
+ import { getResolvedRoutes, generateRoutes, generateProjectFile } from './generate.js';
4
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
5
+ import { fileExists, readFile, mkdir, writeFile } from '@shopify/cli-kit/node/fs';
6
+ import { joinPath, dirname } from '@shopify/cli-kit/node/path';
7
+ import { getTemplateAppFile } from '../../../lib/build.js';
8
+ import { getRemixConfig } from '../../remix-config.js';
9
+
10
+ const readProjectFile = (dirs, fileBasename, ext = "tsx") => readFile(joinPath(dirs.appDirectory, `${fileBasename}.${ext}`));
11
+ describe("generate/route", () => {
12
+ beforeEach(() => {
13
+ vi.resetAllMocks();
14
+ vi.mock("@shopify/cli-kit/node/output");
15
+ vi.mock("@shopify/cli-kit/node/ui");
16
+ vi.mock("../../remix-config.js", async () => ({ getRemixConfig: vi.fn() }));
17
+ });
18
+ describe("generateRoutes", () => {
19
+ it("generates all routes with correct configuration", async () => {
20
+ const { resolvedRouteFiles } = await getResolvedRoutes();
21
+ expect(
22
+ resolvedRouteFiles.find((item) => /account_?\.login/.test(item))
23
+ ).toBeTruthy();
24
+ await temporaryDirectoryTask(async (tmpDir) => {
25
+ const directories = await createHydrogenFixture(tmpDir, {
26
+ files: [
27
+ ["jsconfig.json", JSON.stringify({ compilerOptions: { test: "js" } })],
28
+ [".prettierrc.json", JSON.stringify({ singleQuote: false })]
29
+ ],
30
+ templates: resolvedRouteFiles.map(
31
+ (filepath) => ["routes/" + filepath + ".tsx", ""]
32
+ )
33
+ });
34
+ vi.mocked(getRemixConfig).mockResolvedValue(directories);
35
+ const result = await generateRoutes({
36
+ routeName: "all",
37
+ directory: directories.rootDirectory,
38
+ templatesRoot: directories.templatesRoot
39
+ });
40
+ expect(result).toMatchObject(
41
+ expect.objectContaining({
42
+ isTypescript: false,
43
+ transpilerOptions: { test: "js" },
44
+ formatOptions: { singleQuote: false },
45
+ routes: expect.any(Array)
46
+ })
47
+ );
48
+ expect(result.routes).toHaveLength(
49
+ Object.values(resolvedRouteFiles).length
50
+ );
51
+ });
52
+ });
53
+ it("figures out the locale if a home route already exists", async () => {
54
+ await temporaryDirectoryTask(async (tmpDir) => {
55
+ const route = "routes/pages.$handle";
56
+ const directories = await createHydrogenFixture(tmpDir, {
57
+ files: [
58
+ ["tsconfig.json", JSON.stringify({ compilerOptions: { test: "ts" } })],
59
+ ["app/routes/($locale)._index.tsx", "export const test = true;"]
60
+ ],
61
+ templates: [[route + ".tsx", `const str = "hello world"`]]
62
+ });
63
+ vi.mocked(getRemixConfig).mockResolvedValue({
64
+ ...directories,
65
+ tsconfigPath: "somewhere",
66
+ future: {
67
+ v2_routeConvention: true
68
+ }
69
+ });
70
+ const result = await generateRoutes({
71
+ routeName: ["page"],
72
+ directory: directories.rootDirectory,
73
+ templatesRoot: directories.templatesRoot
74
+ });
75
+ expect(result).toMatchObject(
76
+ expect.objectContaining({
77
+ isTypescript: true,
78
+ transpilerOptions: void 0,
79
+ routes: expect.any(Array),
80
+ formatOptions: expect.any(Object)
81
+ })
82
+ );
83
+ expect(result.routes).toHaveLength(1);
84
+ expect(result.routes[0]).toMatchObject({
85
+ destinationRoute: expect.stringContaining("($locale).pages.$handle")
86
+ });
87
+ });
88
+ });
89
+ });
90
+ describe("generateProjectFile", () => {
91
+ it("generates a route file for Remix v1", async () => {
92
+ await temporaryDirectoryTask(async (tmpDir) => {
93
+ const route = "routes/pages.$handle";
94
+ const directories = await createHydrogenFixture(tmpDir, {
95
+ files: [],
96
+ templates: [[route + ".tsx", `const str = "hello world"`]]
97
+ });
98
+ await generateProjectFile(route, {
99
+ ...directories,
100
+ v2Flags: {
101
+ isV2RouteConvention: false
102
+ }
103
+ });
104
+ expect(
105
+ await readProjectFile(directories, route.replace(".", "/"), "jsx")
106
+ ).toContain(`const str = 'hello world'`);
107
+ });
108
+ });
109
+ it("generates a route file for Remix v2", async () => {
110
+ await temporaryDirectoryTask(async (tmpDir) => {
111
+ const route = "routes/custom.path.$handle._index";
112
+ const directories = await createHydrogenFixture(tmpDir, {
113
+ files: [],
114
+ templates: [[route + ".tsx", `const str = "hello world"`]]
115
+ });
116
+ await generateProjectFile(route, {
117
+ ...directories,
118
+ v2Flags: { isV2RouteConvention: true }
119
+ });
120
+ expect(await readProjectFile(directories, route, "jsx")).toContain(
121
+ `const str = 'hello world'`
122
+ );
123
+ });
124
+ });
125
+ it("generates route files with locale prefix", async () => {
126
+ await temporaryDirectoryTask(async (tmpDir) => {
127
+ const routeCode = `const str = 'hello world'`;
128
+ const directories = await createHydrogenFixture(tmpDir, {
129
+ files: [],
130
+ templates: [
131
+ ["routes/_index.tsx", routeCode],
132
+ ["routes/pages.$handle.tsx", routeCode],
133
+ ["routes/[robots.txt].tsx", routeCode],
134
+ ["routes/[sitemap.xml].tsx", routeCode]
135
+ ]
136
+ });
137
+ const localePrefix = "locale";
138
+ await generateProjectFile("routes/_index", {
139
+ ...directories,
140
+ v2Flags: { isV2RouteConvention: true },
141
+ localePrefix,
142
+ typescript: true
143
+ });
144
+ await generateProjectFile("routes/pages.$handle", {
145
+ ...directories,
146
+ v2Flags: { isV2RouteConvention: false },
147
+ localePrefix,
148
+ typescript: true
149
+ });
150
+ await generateProjectFile("routes/[sitemap.xml]", {
151
+ ...directories,
152
+ v2Flags: { isV2RouteConvention: true },
153
+ localePrefix,
154
+ typescript: true
155
+ });
156
+ await generateProjectFile("routes/[robots.txt]", {
157
+ ...directories,
158
+ v2Flags: { isV2RouteConvention: true },
159
+ localePrefix,
160
+ typescript: true
161
+ });
162
+ await expect(
163
+ readProjectFile(directories, `routes/($locale)._index`)
164
+ ).resolves.toContain(routeCode);
165
+ await expect(
166
+ readProjectFile(directories, `routes/($locale).[sitemap.xml]`)
167
+ ).resolves.toContain(routeCode);
168
+ await expect(
169
+ readProjectFile(directories, `routes/[robots.txt]`)
170
+ ).resolves.toContain(routeCode);
171
+ await expect(
172
+ readProjectFile(directories, `routes/($locale)/pages/$handle`)
173
+ ).resolves.toContain(routeCode);
174
+ });
175
+ });
176
+ it("produces a typescript file when typescript argument is true", async () => {
177
+ await temporaryDirectoryTask(async (tmpDir) => {
178
+ const route = "routes/pages.$handle";
179
+ const directories = await createHydrogenFixture(tmpDir, {
180
+ files: [],
181
+ templates: [[route + ".tsx", 'const str = "hello typescript"']]
182
+ });
183
+ await generateProjectFile(route, {
184
+ ...directories,
185
+ typescript: true,
186
+ v2Flags: { isV2RouteConvention: true }
187
+ });
188
+ expect(await readProjectFile(directories, route)).toContain(
189
+ `const str = 'hello typescript'`
190
+ );
191
+ });
192
+ });
193
+ it("prompts the user if there the file already exists", async () => {
194
+ await temporaryDirectoryTask(async (tmpDir) => {
195
+ vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
196
+ async () => true
197
+ );
198
+ const route = "routes/page.$handle";
199
+ const directories = await createHydrogenFixture(tmpDir, {
200
+ files: [[`app/${route}.jsx`, 'const str = "I exist"']],
201
+ templates: [[route + ".tsx", 'const str = "hello world"']]
202
+ });
203
+ await generateProjectFile(route, {
204
+ ...directories,
205
+ v2Flags: { isV2RouteConvention: true }
206
+ });
207
+ expect(renderConfirmationPrompt).toHaveBeenCalledWith(
208
+ expect.objectContaining({
209
+ message: expect.stringContaining("already exists")
210
+ })
211
+ );
212
+ });
213
+ });
214
+ it("does not prompt the user if the force property is true", async () => {
215
+ await temporaryDirectoryTask(async (tmpDir) => {
216
+ vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
217
+ async () => true
218
+ );
219
+ const route = "routes/page.$pageHandle";
220
+ const directories = await createHydrogenFixture(tmpDir, {
221
+ files: [[`app/${route}.jsx`, 'const str = "I exist"']],
222
+ templates: [[route + ".tsx", 'const str = "hello world"']]
223
+ });
224
+ await generateProjectFile(route, {
225
+ ...directories,
226
+ force: true
227
+ });
228
+ expect(renderConfirmationPrompt).not.toHaveBeenCalled();
229
+ });
230
+ });
231
+ it("generates all the route dependencies", async () => {
232
+ await temporaryDirectoryTask(async (tmpDir) => {
233
+ const templates = [
234
+ [
235
+ "routes/pages.$pageHandle.tsx",
236
+ `import Dep from 'some-node-dep';
237
+ import AnotherRoute from './AnotherRoute';
238
+ import Form from '~/components/Form';
239
+ import {
240
+
241
+
242
+ Button} from '../components/Button';
243
+ import {stuff} from '../utils';
244
+ import {serverOnly} from '../something.server';
245
+ import styles from '../styles/app.css';
246
+ export {Dep, AnotherRoute, Form, Button, stuff, serverOnly, styles};
247
+ `
248
+ ],
249
+ [
250
+ "components/Form.tsx",
251
+ `import {Button} from './Button';
252
+ import {Text} from './Text';
253
+ export {Button, Text};
254
+ `
255
+ ],
256
+ ["components/Button.tsx", `export const Button = '';
257
+ `],
258
+ ["components/Text.tsx", `export const Text = '';
259
+ `],
260
+ ["utils/index.ts", `export {stuff} from './stuff';
261
+ `],
262
+ ["utils/stuff.ts", `export const stuff = '';
263
+ `],
264
+ ["something.server.ts", `export const serverOnly = '';
265
+ `],
266
+ ["styles/app.css", `.red{color:red;}`]
267
+ ];
268
+ const directories = await createHydrogenFixture(tmpDir, { templates });
269
+ vi.mocked(getRemixConfig).mockResolvedValue(directories);
270
+ await generateProjectFile("routes/pages.$pageHandle", {
271
+ ...directories,
272
+ v2Flags: { isV2RouteConvention: true },
273
+ force: true
274
+ });
275
+ await Promise.all(
276
+ templates.map(async ([file, content]) => {
277
+ const actualFile = joinPath(
278
+ directories.appDirectory,
279
+ file.replace(".ts", ".js")
280
+ );
281
+ await expect(fileExists(actualFile)).resolves.toBeTruthy();
282
+ await expect(readFile(actualFile)).resolves.toEqual(
283
+ content.replace(/\{\n+/, "{")
284
+ );
285
+ })
286
+ );
287
+ });
288
+ });
289
+ });
290
+ });
291
+ async function createHydrogenFixture(directory, {
292
+ files = [],
293
+ templates = []
294
+ }) {
295
+ const projectDir = "project";
296
+ for (const item of files) {
297
+ const [filePath, fileContent] = item;
298
+ const fullFilePath = joinPath(directory, projectDir, filePath);
299
+ await mkdir(dirname(fullFilePath));
300
+ await writeFile(fullFilePath, fileContent);
301
+ }
302
+ for (const item of templates) {
303
+ const [filePath, fileContent] = item;
304
+ const fullFilePath = getTemplateAppFile(filePath, directory);
305
+ await mkdir(dirname(fullFilePath));
306
+ await writeFile(fullFilePath, fileContent);
307
+ }
308
+ return {
309
+ rootDirectory: joinPath(directory, projectDir),
310
+ appDirectory: joinPath(directory, projectDir, "app"),
311
+ templatesRoot: directory
312
+ };
313
+ }
package/dist/lib/shell.js CHANGED
@@ -6,7 +6,7 @@ import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
6
6
  import { execAsync } from './process.js';
7
7
 
8
8
  const ALIAS_NAME = "h2";
9
- const isWindows = () => process.platform === "win32";
9
+ const isWindows = () => os.platform() === "win32";
10
10
  const isGitBash = () => !!process.env.MINGW_PREFIX;
11
11
  function resolveFromHome(filepath) {
12
12
  if (filepath[0] === "~") {
@@ -107,15 +107,62 @@ async function hasCliAlias() {
107
107
  return false;
108
108
  }
109
109
  }
110
- async function getCliCommand() {
111
- if (await hasCliAlias()) {
110
+ async function createPlatformShortcut() {
111
+ const shortcuts = isWindows() && !isGitBash() ? await createShortcutsForWindows() : await createShortcutsForUnix();
112
+ return shortcuts;
113
+ }
114
+ const BASH_ZSH_COMMAND = `
115
+ # Shopify Hydrogen alias to local projects
116
+ alias ${ALIAS_NAME}='$(npm prefix -s)/node_modules/.bin/shopify hydrogen'`;
117
+ const FISH_FUNCTION = `
118
+ function ${ALIAS_NAME} --wraps='shopify hydrogen' --description 'Shortcut for the Hydrogen CLI'
119
+ set npmPrefix (npm prefix -s)
120
+ $npmPrefix/node_modules/.bin/shopify hydrogen $argv
121
+ end
122
+ `;
123
+ async function createShortcutsForUnix() {
124
+ const shells = [];
125
+ if (await shellWriteAlias("zsh", ALIAS_NAME, BASH_ZSH_COMMAND)) {
126
+ shells.push("zsh");
127
+ }
128
+ if (await shellWriteAlias("bash", ALIAS_NAME, BASH_ZSH_COMMAND)) {
129
+ shells.push("bash");
130
+ }
131
+ if (await shellWriteAlias("fish", ALIAS_NAME, FISH_FUNCTION)) {
132
+ shells.push("fish");
133
+ }
134
+ return shells;
135
+ }
136
+ const PS_FUNCTION = `function Invoke-Local-H2 {$npmPrefix = npm prefix -s; Invoke-Expression "$npmPrefix\\node_modules\\.bin\\shopify.ps1 hydrogen $Args"}; Set-Alias -Name ${ALIAS_NAME} -Value Invoke-Local-H2`;
137
+ const PS_APPEND_PROFILE_COMMAND = `
138
+ if (!(Test-Path -Path $PROFILE)) {
139
+ New-Item -ItemType File -Path $PROFILE -Force
140
+ }
141
+
142
+ $profileContent = Get-Content -Path $PROFILE
143
+ if (!$profileContent -or $profileContent -NotLike '*Invoke-Local-H2*') {
144
+ Add-Content -Path $PROFILE -Value '${PS_FUNCTION}'
145
+ }
146
+ `;
147
+ async function createShortcutsForWindows() {
148
+ const shells = [];
149
+ if (await shellRunScript(PS_APPEND_PROFILE_COMMAND, "powershell.exe")) {
150
+ shells.push("PowerShell");
151
+ }
152
+ if (await shellRunScript(PS_APPEND_PROFILE_COMMAND, "pwsh.exe")) {
153
+ shells.push("PowerShell 7+");
154
+ }
155
+ return shells;
156
+ }
157
+ async function getCliCommand(directory = process.cwd(), forcePkgManager) {
158
+ if (!forcePkgManager && await hasCliAlias()) {
112
159
  return ALIAS_NAME;
113
160
  }
114
161
  let cli = "npx";
115
- const pkgManager = await getPackageManager(process.cwd()).catch(() => null);
162
+ const pkgManager = forcePkgManager ?? await getPackageManager(directory).catch(() => null);
116
163
  if (pkgManager === "pnpm" || pkgManager === "yarn")
117
164
  cli = pkgManager;
118
165
  return `${cli} shopify hydrogen`;
119
166
  }
120
167
 
121
- export { ALIAS_NAME, getCliCommand, isGitBash, isWindows, shellRunScript, shellWriteAlias };
168
+ export { ALIAS_NAME, createPlatformShortcut, getCliCommand, isGitBash, isWindows, shellRunScript, shellWriteAlias };
@@ -1,26 +1,31 @@
1
- import { describe, beforeEach, vi, it, expect } from 'vitest';
1
+ import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import { platform, userInfo } from 'node:os';
2
3
  import { fileExists } from '@shopify/cli-kit/node/fs';
3
4
  import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
4
- import { shellWriteAlias, getCliCommand } from './shell.js';
5
+ import { shellWriteAlias, createPlatformShortcut, getCliCommand } from './shell.js';
5
6
  import { execAsync } from './process.js';
6
7
 
8
+ vi.mock("node:os");
9
+ vi.mock("node:child_process");
10
+ vi.mock("@shopify/cli-kit/node/fs");
11
+ vi.mock("@shopify/cli-kit/node/node-package-manager");
12
+ vi.mock("./process.js", async () => {
13
+ const original = await vi.importActual(
14
+ "./process.js"
15
+ );
16
+ return {
17
+ ...original,
18
+ execAsync: vi.fn()
19
+ };
20
+ });
21
+ vi.mocked(fileExists).mockResolvedValue(false);
22
+ vi.mocked(getPackageManager).mockResolvedValue("npm");
7
23
  describe("shell", () => {
8
24
  beforeEach(() => {
9
25
  vi.resetAllMocks();
10
- vi.mock("node:child_process");
11
- vi.mock("@shopify/cli-kit/node/fs");
12
- vi.mock("@shopify/cli-kit/node/node-package-manager");
13
- vi.mock("./process.js", async () => {
14
- const original = await vi.importActual(
15
- "./process.js"
16
- );
17
- return {
18
- ...original,
19
- execAsync: vi.fn()
20
- };
21
- });
22
- vi.mocked(fileExists).mockResolvedValue(false);
23
- vi.mocked(getPackageManager).mockResolvedValue("npm");
26
+ });
27
+ afterEach(() => {
28
+ delete process.env.MINGW_PREFIX;
24
29
  });
25
30
  describe("shellWriteAlias", () => {
26
31
  ["bash", "zsh", "fish"].forEach((shell) => {
@@ -63,8 +68,29 @@ describe("shell", () => {
63
68
  });
64
69
  });
65
70
  });
71
+ describe("createPlatformShortcut", () => {
72
+ it("creates aliases for Unix", async () => {
73
+ vi.mocked(platform).mockReturnValue("darwin");
74
+ const result = await createPlatformShortcut();
75
+ expect(result).toEqual(expect.arrayContaining(["zsh", "bash", "fish"]));
76
+ });
77
+ it("creates aliases for Windows", async () => {
78
+ vi.mocked(platform).mockReturnValue("win32");
79
+ const result = await createPlatformShortcut();
80
+ expect(result).toEqual(
81
+ expect.arrayContaining(["PowerShell", "PowerShell 7+"])
82
+ );
83
+ });
84
+ it("creates aliases for Windows in Git Bash", async () => {
85
+ process.env.MINGW_PREFIX = "something";
86
+ vi.mocked(platform).mockReturnValue("win32");
87
+ const result = await createPlatformShortcut();
88
+ expect(result).toEqual(expect.arrayContaining(["bash"]));
89
+ });
90
+ });
66
91
  describe("getCliCommand", () => {
67
92
  it("returns the shortcut alias if available", async () => {
93
+ vi.mocked(userInfo).mockReturnValue({ shell: "/bin/bash" });
68
94
  vi.mocked(execAsync).mockImplementation(
69
95
  (shellCommand) => shellCommand.startsWith("grep") ? Promise.resolve({ stdout: "stuff", stderr: "" }) : Promise.reject(null)
70
96
  );
@@ -1,10 +1,16 @@
1
1
  import { resolvePath, dirname } from '@shopify/cli-kit/node/path';
2
- import { fileExists, readFile, mkdir, writeFile } from '@shopify/cli-kit/node/fs';
2
+ import { fileExists, writeFile, readFile, mkdir } from '@shopify/cli-kit/node/fs';
3
3
  import { AbortError } from '@shopify/cli-kit/node/error';
4
- import { outputInfo } from '@shopify/cli-kit/node/output';
5
4
 
6
5
  const SHOPIFY_DIR = ".shopify";
7
6
  const SHOPIFY_DIR_PROJECT = "project.json";
7
+ async function resetConfig(root) {
8
+ const filePath = resolvePath(root, SHOPIFY_DIR, SHOPIFY_DIR_PROJECT);
9
+ if (!await fileExists(filePath)) {
10
+ return;
11
+ }
12
+ await writeFile(filePath, JSON.stringify({}));
13
+ }
8
14
  async function getConfig(root) {
9
15
  const filePath = resolvePath(root, SHOPIFY_DIR, SHOPIFY_DIR_PROJECT);
10
16
  if (!await fileExists(filePath)) {
@@ -12,25 +18,23 @@ async function getConfig(root) {
12
18
  }
13
19
  return JSON.parse(await readFile(filePath));
14
20
  }
15
- async function setShop(root, shop) {
21
+ async function setUserAccount(root, { shop, shopName, email }) {
16
22
  const filePath = resolvePath(root, SHOPIFY_DIR, SHOPIFY_DIR_PROJECT);
17
- if (!await fileExists(filePath)) {
23
+ let existingConfig = {};
24
+ if (await fileExists(filePath)) {
25
+ existingConfig = JSON.parse(await readFile(filePath));
26
+ } else {
18
27
  await mkdir(dirname(filePath));
19
- const newConfig = {
20
- shop
21
- };
22
- await writeFile(filePath, JSON.stringify(newConfig));
23
- await ensureShopifyGitIgnore(root);
24
- return newConfig;
25
28
  }
26
- const existingConfig = JSON.parse(await readFile(filePath));
27
- const config = {
29
+ const newConfig = {
28
30
  ...existingConfig,
29
- shop
31
+ shop,
32
+ shopName,
33
+ email
30
34
  };
31
- await writeFile(filePath, JSON.stringify(config));
35
+ await writeFile(filePath, JSON.stringify(newConfig));
32
36
  await ensureShopifyGitIgnore(root);
33
- return config;
37
+ return newConfig;
34
38
  }
35
39
  async function setStorefront(root, { id, title }) {
36
40
  try {
@@ -50,7 +54,9 @@ async function setStorefront(root, { id, title }) {
50
54
  async function unsetStorefront(root) {
51
55
  try {
52
56
  const filePath = resolvePath(root, SHOPIFY_DIR, SHOPIFY_DIR_PROJECT);
53
- const existingConfig = JSON.parse(await readFile(filePath));
57
+ const existingConfig = JSON.parse(
58
+ await readFile(filePath)
59
+ );
54
60
  const config = {
55
61
  ...existingConfig,
56
62
  storefront: void 0
@@ -75,7 +81,6 @@ async function ensureShopifyGitIgnore(root) {
75
81
  }
76
82
  gitIgnoreContents += `${SHOPIFY_DIR}\r
77
83
  `;
78
- outputInfo("Adding .shopify to .gitignore...");
79
84
  await writeFile(gitIgnoreFilePath, gitIgnoreContents);
80
85
  return true;
81
86
  } catch {
@@ -83,4 +88,4 @@ async function ensureShopifyGitIgnore(root) {
83
88
  }
84
89
  }
85
90
 
86
- export { SHOPIFY_DIR, SHOPIFY_DIR_PROJECT, ensureShopifyGitIgnore, getConfig, setShop, setStorefront, unsetStorefront };
91
+ export { SHOPIFY_DIR, SHOPIFY_DIR_PROJECT, ensureShopifyGitIgnore, getConfig, resetConfig, setStorefront, setUserAccount, unsetStorefront };