@shopify/cli-hydrogen 5.0.1 → 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 (240) hide show
  1. package/dist/commands/hydrogen/build.js +38 -8
  2. package/dist/commands/hydrogen/codegen-unstable.js +13 -24
  3. package/dist/commands/hydrogen/dev.js +61 -41
  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 +101 -43
  13. package/dist/commands/hydrogen/link.test.js +108 -74
  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/preview.js +2 -1
  19. package/dist/commands/hydrogen/setup/css.js +79 -0
  20. package/dist/commands/hydrogen/setup/markets.js +53 -0
  21. package/dist/commands/hydrogen/setup.js +133 -0
  22. package/dist/commands/hydrogen/shortcut.js +2 -45
  23. package/dist/commands/hydrogen/shortcut.test.js +10 -37
  24. package/dist/generator-templates/assets/css-modules/package.json +6 -0
  25. package/dist/generator-templates/assets/postcss/package.json +10 -0
  26. package/dist/generator-templates/assets/postcss/postcss.config.js +8 -0
  27. package/dist/generator-templates/assets/tailwind/package.json +13 -0
  28. package/dist/generator-templates/assets/tailwind/postcss.config.js +10 -0
  29. package/dist/generator-templates/assets/tailwind/tailwind.config.js +8 -0
  30. package/dist/generator-templates/assets/tailwind/tailwind.css +3 -0
  31. package/dist/generator-templates/assets/vanilla-extract/package.json +9 -0
  32. package/dist/generator-templates/starter/.eslintignore +5 -0
  33. package/dist/generator-templates/starter/.eslintrc.js +18 -0
  34. package/dist/generator-templates/starter/.graphqlrc.yml +1 -0
  35. package/dist/generator-templates/starter/README.md +40 -0
  36. package/dist/generator-templates/starter/app/components/Aside.tsx +47 -0
  37. package/dist/generator-templates/starter/app/components/Cart.tsx +340 -0
  38. package/dist/generator-templates/starter/app/components/Footer.tsx +99 -0
  39. package/dist/generator-templates/starter/app/components/Header.tsx +178 -0
  40. package/dist/generator-templates/starter/app/components/Layout.tsx +95 -0
  41. package/dist/generator-templates/starter/app/components/Search.tsx +480 -0
  42. package/dist/generator-templates/starter/app/entry.client.tsx +12 -0
  43. package/dist/generator-templates/starter/app/entry.server.tsx +33 -0
  44. package/dist/generator-templates/starter/app/root.tsx +270 -0
  45. package/dist/generator-templates/starter/app/routes/$.tsx +7 -0
  46. package/dist/generator-templates/{routes → starter/app/routes}/[robots.txt].tsx +47 -69
  47. package/dist/generator-templates/starter/app/routes/[sitemap.xml].tsx +174 -0
  48. package/dist/generator-templates/starter/app/routes/_index.tsx +145 -0
  49. package/dist/generator-templates/starter/app/routes/account.$.tsx +9 -0
  50. package/dist/generator-templates/starter/app/routes/account.addresses.tsx +563 -0
  51. package/dist/generator-templates/starter/app/routes/account.orders.$id.tsx +309 -0
  52. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +196 -0
  53. package/dist/generator-templates/starter/app/routes/account.profile.tsx +289 -0
  54. package/dist/generator-templates/starter/app/routes/account.tsx +203 -0
  55. package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +157 -0
  56. package/dist/generator-templates/starter/app/routes/account_.login.tsx +143 -0
  57. package/dist/generator-templates/starter/app/routes/account_.logout.tsx +33 -0
  58. package/dist/generator-templates/starter/app/routes/account_.recover.tsx +124 -0
  59. package/dist/generator-templates/starter/app/routes/account_.register.tsx +207 -0
  60. package/dist/generator-templates/starter/app/routes/account_.reset.$id.$resetToken.tsx +136 -0
  61. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +342 -0
  62. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +88 -0
  63. package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +162 -0
  64. package/dist/generator-templates/starter/app/routes/blogs._index.tsx +94 -0
  65. package/dist/generator-templates/starter/app/routes/cart.tsx +104 -0
  66. package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +184 -0
  67. package/dist/generator-templates/starter/app/routes/collections._index.tsx +120 -0
  68. package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +57 -0
  69. package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +94 -0
  70. package/dist/generator-templates/starter/app/routes/policies._index.tsx +63 -0
  71. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +418 -0
  72. package/dist/generator-templates/starter/app/routes/search.tsx +168 -0
  73. package/dist/generator-templates/starter/app/styles/app.css +473 -0
  74. package/dist/generator-templates/starter/app/styles/reset.css +129 -0
  75. package/dist/generator-templates/starter/app/utils.ts +46 -0
  76. package/dist/generator-templates/starter/package.json +43 -0
  77. package/dist/generator-templates/starter/public/favicon.svg +28 -0
  78. package/dist/generator-templates/starter/remix.config.js +26 -0
  79. package/dist/generator-templates/starter/remix.env.d.ts +39 -0
  80. package/dist/generator-templates/starter/server.ts +253 -0
  81. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +1906 -0
  82. package/dist/generator-templates/starter/tsconfig.json +22 -0
  83. package/dist/lib/auth.js +123 -0
  84. package/dist/lib/auth.test.js +157 -0
  85. package/dist/lib/build.js +51 -0
  86. package/dist/lib/check-version.js +3 -3
  87. package/dist/lib/check-version.test.js +24 -0
  88. package/dist/lib/codegen.js +26 -17
  89. package/dist/lib/environment-variables.js +68 -0
  90. package/dist/lib/environment-variables.test.js +147 -0
  91. package/dist/lib/file.js +41 -0
  92. package/dist/lib/file.test.js +69 -0
  93. package/dist/lib/flags.js +39 -2
  94. package/dist/lib/format-code.js +26 -0
  95. package/dist/lib/gid.js +12 -0
  96. package/dist/lib/{graphql.test.js → gid.test.js} +1 -1
  97. package/dist/lib/graphql/admin/client.js +27 -0
  98. package/dist/lib/graphql/admin/client.test.js +51 -0
  99. package/dist/lib/graphql/admin/create-storefront.js +36 -0
  100. package/dist/lib/graphql/admin/create-storefront.test.js +64 -0
  101. package/dist/lib/graphql/admin/fetch-job.js +39 -0
  102. package/dist/lib/graphql/admin/link-storefront.js +7 -11
  103. package/dist/lib/graphql/admin/link-storefront.test.js +38 -0
  104. package/dist/lib/graphql/admin/list-environments.js +2 -2
  105. package/dist/lib/graphql/admin/list-environments.test.js +44 -0
  106. package/dist/lib/graphql/admin/list-storefronts.js +7 -11
  107. package/dist/lib/graphql/admin/list-storefronts.test.js +44 -0
  108. package/dist/lib/graphql/admin/pull-variables.js +3 -3
  109. package/dist/lib/graphql/admin/pull-variables.test.js +37 -0
  110. package/dist/lib/graphql/business-platform/user-account.js +83 -0
  111. package/dist/lib/graphql/business-platform/user-account.test.js +80 -0
  112. package/dist/lib/log.js +185 -9
  113. package/dist/lib/log.test.js +92 -0
  114. package/dist/lib/mini-oxygen.js +32 -14
  115. package/dist/lib/missing-routes.js +0 -2
  116. package/dist/lib/onboarding/common.js +456 -0
  117. package/dist/lib/onboarding/index.js +2 -0
  118. package/dist/lib/onboarding/local.js +229 -0
  119. package/dist/lib/onboarding/remote.js +89 -0
  120. package/dist/lib/remix-version-interop.js +5 -5
  121. package/dist/lib/remix-version-interop.test.js +11 -1
  122. package/dist/lib/render-errors.js +13 -11
  123. package/dist/lib/setups/css/assets.js +89 -0
  124. package/dist/lib/setups/css/css-modules.js +22 -0
  125. package/dist/lib/setups/css/index.js +44 -0
  126. package/dist/lib/setups/css/postcss.js +34 -0
  127. package/dist/lib/setups/css/replacers.js +137 -0
  128. package/dist/lib/setups/css/tailwind.js +54 -0
  129. package/dist/lib/setups/css/vanilla-extract.js +22 -0
  130. package/dist/lib/setups/i18n/domains.test.js +25 -0
  131. package/dist/lib/setups/i18n/index.js +46 -0
  132. package/dist/lib/setups/i18n/replacers.js +227 -0
  133. package/dist/lib/setups/i18n/subdomains.test.js +25 -0
  134. package/dist/lib/setups/i18n/subfolders.test.js +25 -0
  135. package/dist/lib/setups/i18n/templates/domains.js +14 -0
  136. package/dist/lib/setups/i18n/templates/domains.ts +25 -0
  137. package/dist/lib/setups/i18n/templates/subdomains.js +14 -0
  138. package/dist/lib/setups/i18n/templates/subdomains.ts +24 -0
  139. package/dist/lib/setups/i18n/templates/subfolders.js +14 -0
  140. package/dist/lib/setups/i18n/templates/subfolders.ts +28 -0
  141. package/dist/lib/setups/routes/generate.js +244 -0
  142. package/dist/lib/setups/routes/generate.test.js +313 -0
  143. package/dist/lib/shell.js +52 -5
  144. package/dist/lib/shell.test.js +42 -16
  145. package/dist/lib/shopify-config.js +23 -18
  146. package/dist/lib/shopify-config.test.js +63 -73
  147. package/dist/lib/string.js +7 -0
  148. package/dist/lib/string.test.js +16 -0
  149. package/dist/lib/template-downloader.js +9 -7
  150. package/dist/lib/transpile-ts.js +9 -29
  151. package/dist/virtual-routes/routes/index.jsx +40 -19
  152. package/oclif.manifest.json +710 -1
  153. package/package.json +16 -16
  154. package/dist/commands/hydrogen/build.d.ts +0 -23
  155. package/dist/commands/hydrogen/check.d.ts +0 -15
  156. package/dist/commands/hydrogen/codegen-unstable.d.ts +0 -15
  157. package/dist/commands/hydrogen/dev.d.ts +0 -21
  158. package/dist/commands/hydrogen/env/list.d.ts +0 -18
  159. package/dist/commands/hydrogen/env/pull.d.ts +0 -22
  160. package/dist/commands/hydrogen/g.d.ts +0 -10
  161. package/dist/commands/hydrogen/generate/route.d.ts +0 -32
  162. package/dist/commands/hydrogen/generate/route.test.d.ts +0 -1
  163. package/dist/commands/hydrogen/generate/routes.d.ts +0 -16
  164. package/dist/commands/hydrogen/init.d.ts +0 -24
  165. package/dist/commands/hydrogen/init.test.d.ts +0 -1
  166. package/dist/commands/hydrogen/link.d.ts +0 -23
  167. package/dist/commands/hydrogen/link.test.d.ts +0 -1
  168. package/dist/commands/hydrogen/list.d.ts +0 -21
  169. package/dist/commands/hydrogen/list.test.d.ts +0 -1
  170. package/dist/commands/hydrogen/preview.d.ts +0 -17
  171. package/dist/commands/hydrogen/shortcut.d.ts +0 -9
  172. package/dist/commands/hydrogen/shortcut.test.d.ts +0 -1
  173. package/dist/commands/hydrogen/unlink.d.ts +0 -16
  174. package/dist/commands/hydrogen/unlink.test.d.ts +0 -1
  175. package/dist/create-app.d.ts +0 -1
  176. package/dist/generator-templates/routes/[sitemap.xml].tsx +0 -235
  177. package/dist/generator-templates/routes/account/login.tsx +0 -103
  178. package/dist/generator-templates/routes/account/register.tsx +0 -103
  179. package/dist/generator-templates/routes/cart.tsx +0 -81
  180. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +0 -104
  181. package/dist/generator-templates/routes/collections/index.tsx +0 -102
  182. package/dist/generator-templates/routes/graphiql.tsx +0 -10
  183. package/dist/generator-templates/routes/index.tsx +0 -40
  184. package/dist/generator-templates/routes/pages/$pageHandle.tsx +0 -112
  185. package/dist/generator-templates/routes/policies/$policyHandle.tsx +0 -140
  186. package/dist/generator-templates/routes/policies/index.tsx +0 -117
  187. package/dist/generator-templates/routes/products/$productHandle.tsx +0 -92
  188. package/dist/hooks/init.d.ts +0 -5
  189. package/dist/lib/admin-session.d.ts +0 -6
  190. package/dist/lib/admin-session.js +0 -16
  191. package/dist/lib/admin-session.test.d.ts +0 -1
  192. package/dist/lib/admin-session.test.js +0 -27
  193. package/dist/lib/admin-urls.d.ts +0 -8
  194. package/dist/lib/check-lockfile.d.ts +0 -3
  195. package/dist/lib/check-lockfile.test.d.ts +0 -1
  196. package/dist/lib/check-version.d.ts +0 -16
  197. package/dist/lib/check-version.test.d.ts +0 -1
  198. package/dist/lib/codegen.d.ts +0 -26
  199. package/dist/lib/combined-environment-variables.d.ts +0 -8
  200. package/dist/lib/combined-environment-variables.js +0 -57
  201. package/dist/lib/combined-environment-variables.test.d.ts +0 -1
  202. package/dist/lib/combined-environment-variables.test.js +0 -111
  203. package/dist/lib/config.d.ts +0 -20
  204. package/dist/lib/flags.d.ts +0 -27
  205. package/dist/lib/flags.test.d.ts +0 -1
  206. package/dist/lib/graphql/admin/link-storefront.d.ts +0 -14
  207. package/dist/lib/graphql/admin/list-environments.d.ts +0 -21
  208. package/dist/lib/graphql/admin/list-storefronts.d.ts +0 -25
  209. package/dist/lib/graphql/admin/pull-variables.d.ts +0 -21
  210. package/dist/lib/graphql.d.ts +0 -21
  211. package/dist/lib/graphql.js +0 -18
  212. package/dist/lib/graphql.test.d.ts +0 -1
  213. package/dist/lib/log.d.ts +0 -6
  214. package/dist/lib/mini-oxygen.d.ts +0 -14
  215. package/dist/lib/missing-routes.d.ts +0 -8
  216. package/dist/lib/missing-routes.test.d.ts +0 -1
  217. package/dist/lib/missing-storefronts.d.ts +0 -5
  218. package/dist/lib/missing-storefronts.js +0 -18
  219. package/dist/lib/process.d.ts +0 -6
  220. package/dist/lib/pull-environment-variables.d.ts +0 -20
  221. package/dist/lib/pull-environment-variables.js +0 -57
  222. package/dist/lib/pull-environment-variables.test.d.ts +0 -1
  223. package/dist/lib/pull-environment-variables.test.js +0 -174
  224. package/dist/lib/remix-version-interop.d.ts +0 -11
  225. package/dist/lib/remix-version-interop.test.d.ts +0 -1
  226. package/dist/lib/render-errors.d.ts +0 -16
  227. package/dist/lib/shell.d.ts +0 -11
  228. package/dist/lib/shell.test.d.ts +0 -1
  229. package/dist/lib/shop.d.ts +0 -7
  230. package/dist/lib/shop.js +0 -32
  231. package/dist/lib/shop.test.d.ts +0 -1
  232. package/dist/lib/shop.test.js +0 -78
  233. package/dist/lib/shopify-config.d.ts +0 -35
  234. package/dist/lib/shopify-config.test.d.ts +0 -1
  235. package/dist/lib/template-downloader.d.ts +0 -6
  236. package/dist/lib/transpile-ts.d.ts +0 -16
  237. package/dist/lib/virtual-routes.d.ts +0 -7
  238. package/dist/lib/virtual-routes.test.d.ts +0 -1
  239. /package/dist/{commands/hydrogen/env/list.test.d.ts → lib/setups/css/common.js} +0 -0
  240. /package/dist/{commands/hydrogen/env/pull.test.d.ts → lib/setups/i18n/mock-i18n-types.js} +0 -0
@@ -1,126 +1,378 @@
1
- import { describe, beforeEach, vi, it, expect } from 'vitest';
1
+ import { fileURLToPath } from 'node:url';
2
+ import { vi, describe, beforeEach, it, expect } from 'vitest';
2
3
  import { temporaryDirectoryTask } from 'tempy';
3
4
  import { runInit } from './init.js';
4
- import { renderSelectPrompt, renderConfirmationPrompt, renderTextPrompt, renderInfo } from '@shopify/cli-kit/node/ui';
5
- import { outputContent } from '@shopify/cli-kit/node/output';
6
- import { installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
5
+ import { exec } from '@shopify/cli-kit/node/system';
6
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
7
+ import { readFile, writeFile, isDirectory } from '@shopify/cli-kit/node/fs';
8
+ import { basename, joinPath } from '@shopify/cli-kit/node/path';
9
+ import { checkHydrogenVersion } from '../../lib/check-version.js';
10
+ import { handleProjectLocation } from '../../lib/onboarding/common.js';
11
+ import glob from 'fast-glob';
12
+ import { getSkeletonSourceDir } from '../../lib/build.js';
13
+ import { execAsync } from '../../lib/process.js';
14
+ import { rmdir, symlink } from 'fs-extra';
7
15
 
16
+ vi.mock("../../lib/template-downloader.js", async () => ({
17
+ getLatestTemplates: () => Promise.resolve({})
18
+ }));
19
+ vi.mock(
20
+ "@shopify/cli-kit/node/node-package-manager",
21
+ async (importOriginal) => {
22
+ const original = await importOriginal();
23
+ return {
24
+ ...original,
25
+ installNodeModules: vi.fn(),
26
+ getPackageManager: () => Promise.resolve("npm"),
27
+ packageManagerUsedForCreating: () => Promise.resolve("npm")
28
+ };
29
+ }
30
+ );
31
+ vi.mock("../../lib/check-version.js");
32
+ const { renderTasksHook } = vi.hoisted(() => {
33
+ return {
34
+ renderTasksHook: vi.fn()
35
+ };
36
+ });
37
+ vi.mock("@shopify/cli-kit/node/ui", async () => {
38
+ const original = await vi.importActual("@shopify/cli-kit/node/ui");
39
+ return {
40
+ ...original,
41
+ renderConfirmationPrompt: vi.fn(),
42
+ renderSelectPrompt: vi.fn(),
43
+ renderTextPrompt: vi.fn(),
44
+ renderInfo: vi.fn(),
45
+ renderTasks: vi.fn(async (args) => {
46
+ await original.renderTasks(args);
47
+ renderTasksHook();
48
+ })
49
+ };
50
+ });
51
+ vi.mock("../../lib/onboarding/common.js", async (importOriginal) => {
52
+ const original = await importOriginal();
53
+ return Object.keys(original).reduce((acc, item) => {
54
+ const key = item;
55
+ const value = original[key];
56
+ if (typeof value === "function") {
57
+ acc[key] = vi.fn(value);
58
+ } else {
59
+ acc[key] = value;
60
+ }
61
+ return acc;
62
+ }, {});
63
+ });
8
64
  describe("init", () => {
65
+ const outputMock = mockAndCaptureOutput();
9
66
  beforeEach(() => {
10
- vi.resetAllMocks();
11
- vi.mock("@shopify/cli-kit/node/output");
12
- vi.mock("../../lib/transpile-ts.js");
13
- vi.mock("../../lib/template-downloader.js", async () => ({
14
- getLatestTemplates: () => Promise.resolve({})
15
- }));
16
- vi.mock("@shopify/cli-kit/node/node-package-manager");
17
- vi.mocked(outputContent).mockImplementation(() => ({
18
- value: ""
19
- }));
20
- vi.mock("@shopify/cli-kit/node/ui");
21
- vi.mock("@shopify/cli-kit/node/fs");
67
+ vi.clearAllMocks();
68
+ outputMock.clear();
22
69
  });
23
- const defaultOptions = (stubs) => ({
24
- template: "hello-world",
25
- language: "js",
26
- path: "path/to/project",
27
- ...stubs
70
+ it("checks Hydrogen version", async () => {
71
+ await temporaryDirectoryTask(async (tmpDir) => {
72
+ const showUpgradeMock = vi.fn((param) => ({
73
+ currentVersion: "1.0.0",
74
+ newVersion: "1.0.1"
75
+ }));
76
+ vi.mocked(checkHydrogenVersion).mockResolvedValueOnce(showUpgradeMock);
77
+ vi.mocked(handleProjectLocation).mockResolvedValueOnce(void 0);
78
+ const project = await runInit({ path: tmpDir, git: false });
79
+ expect(project).toBeFalsy();
80
+ expect(checkHydrogenVersion).toHaveBeenCalledOnce();
81
+ expect(showUpgradeMock).toHaveBeenCalledWith(
82
+ expect.stringContaining("npm create @shopify/hydrogen@latest")
83
+ );
84
+ });
28
85
  });
29
- describe.each([
30
- {
31
- flag: "template",
32
- value: "hello-world",
33
- condition: { fn: renderSelectPrompt, match: /template/i }
34
- },
35
- {
36
- flag: "installDeps",
37
- value: true,
38
- condition: { fn: renderConfirmationPrompt, match: /install dependencies/i }
39
- },
40
- {
41
- flag: "language",
42
- value: "ts",
43
- condition: { fn: renderSelectPrompt, match: /language/i }
44
- },
45
- {
46
- flag: "path",
47
- value: "./my-app",
48
- condition: { fn: renderTextPrompt, match: /where/i }
49
- }
50
- ])("flag $flag", ({ flag, value, condition }) => {
51
- it(`does not prompt the user for ${flag} when a value is passed in options`, async () => {
86
+ describe("local templates", () => {
87
+ it("creates basic projects", async () => {
52
88
  await temporaryDirectoryTask(async (tmpDir) => {
53
- const options = defaultOptions({
89
+ await runInit({
54
90
  path: tmpDir,
55
- [flag]: value
91
+ git: false,
92
+ language: "ts",
93
+ mockShop: true
94
+ });
95
+ const skeletonFiles = await glob("**/*", {
96
+ cwd: getSkeletonSourceDir(),
97
+ ignore: ["**/node_modules/**", "**/dist/**"]
56
98
  });
57
- await runInit(options);
58
- expect(condition.fn).not.toHaveBeenCalledWith(
59
- expect.objectContaining({
60
- message: expect.stringMatching(condition.match)
61
- })
99
+ const projectFiles = await glob("**/*", { cwd: tmpDir });
100
+ const nonAppFiles = skeletonFiles.filter(
101
+ (item) => !item.startsWith("app/")
102
+ );
103
+ expect(projectFiles).toEqual(expect.arrayContaining(nonAppFiles));
104
+ expect(projectFiles).toContain("app/root.tsx");
105
+ expect(projectFiles).toContain("app/entry.client.tsx");
106
+ expect(projectFiles).toContain("app/entry.server.tsx");
107
+ expect(projectFiles).toContain("app/components/Layout.tsx");
108
+ expect(projectFiles).not.toContain("app/routes/_index.tsx");
109
+ await expect(readFile(`${tmpDir}/server.ts`)).resolves.toEqual(
110
+ await readFile(`${getSkeletonSourceDir()}/server.ts`)
111
+ );
112
+ await expect(readFile(`${tmpDir}/package.json`)).resolves.toMatch(
113
+ `"name": "${basename(tmpDir)}"`
114
+ );
115
+ await expect(readFile(`${tmpDir}/.env`)).resolves.toMatch(
116
+ `PUBLIC_STORE_DOMAIN="mock.shop"`
117
+ );
118
+ const output = outputMock.info();
119
+ expect(output).toMatch("success");
120
+ expect(output).not.toMatch("warning");
121
+ expect(output).toMatch(basename(tmpDir));
122
+ expect(output).not.toMatch("Routes");
123
+ expect(output).toMatch(/Language:\s*TypeScript/);
124
+ expect(output).toMatch("Help");
125
+ expect(output).toMatch("Next steps");
126
+ expect(output).toMatch(
127
+ /Run `cd .*? &&[^\w]*?npm[^\w]*?install[^\w]*?&&[^\w]*?npm[^\w]*?run[^\w]*?dev`/ims
62
128
  );
63
129
  });
64
130
  });
65
- it(`prompts the user for ${flag} when no value is passed in options`, async () => {
131
+ it("creates projects with route files", async () => {
66
132
  await temporaryDirectoryTask(async (tmpDir) => {
67
- const options = defaultOptions({
68
- path: tmpDir,
69
- [flag]: void 0
133
+ await runInit({ path: tmpDir, git: false, routes: true, language: "ts" });
134
+ const skeletonFiles = await glob("**/*", {
135
+ cwd: getSkeletonSourceDir(),
136
+ ignore: ["**/node_modules/**", "**/dist/**"]
70
137
  });
71
- await runInit(options);
72
- expect(condition.fn).toHaveBeenCalledWith(
73
- expect.objectContaining({
74
- message: expect.stringMatching(condition.match)
75
- })
138
+ const projectFiles = await glob("**/*", { cwd: tmpDir });
139
+ expect(projectFiles).toEqual(expect.arrayContaining(skeletonFiles));
140
+ expect(projectFiles).toContain("app/routes/_index.tsx");
141
+ await expect(readFile(`${tmpDir}/server.ts`)).resolves.toEqual(
142
+ await readFile(`${getSkeletonSourceDir()}/server.ts`)
76
143
  );
144
+ const output = outputMock.info();
145
+ expect(output).toMatch("success");
146
+ expect(output).not.toMatch("warning");
147
+ expect(output).toMatch(basename(tmpDir));
148
+ expect(output).toMatch(/Language:\s*TypeScript/);
149
+ expect(output).toMatch("Routes");
150
+ expect(output).toMatch("Home (/ & /:catchAll)");
151
+ expect(output).toMatch("Account (/account/*)");
77
152
  });
78
153
  });
79
- });
80
- it("installs dependencies when installDeps is true", async () => {
81
- await temporaryDirectoryTask(async (tmpDir) => {
82
- const options = defaultOptions({ installDeps: true, path: tmpDir });
83
- await runInit(options);
84
- expect(installNodeModules).toHaveBeenCalled();
154
+ it("transpiles projects to JS", async () => {
155
+ await temporaryDirectoryTask(async (tmpDir) => {
156
+ await runInit({ path: tmpDir, git: false, routes: true, language: "js" });
157
+ const skeletonFiles = await glob("**/*", {
158
+ cwd: getSkeletonSourceDir(),
159
+ ignore: ["**/node_modules/**", "**/dist/**"]
160
+ });
161
+ const projectFiles = await glob("**/*", { cwd: tmpDir });
162
+ expect(projectFiles).toEqual(
163
+ expect.arrayContaining(
164
+ skeletonFiles.filter((item) => !item.endsWith(".d.ts")).map(
165
+ (item) => item.replace(/\.ts(x)?$/, ".js$1").replace(/tsconfig\.json$/, "jsconfig.json")
166
+ )
167
+ )
168
+ );
169
+ expect(projectFiles).toContain("app/routes/_index.jsx");
170
+ await expect(readFile(`${tmpDir}/server.js`)).resolves.toMatch(
171
+ /export default {\n\s+async fetch\(\s*request,\s*env,\s*executionContext,?\s*\)/
172
+ );
173
+ const output = outputMock.info();
174
+ expect(output).toMatch("success");
175
+ expect(output).not.toMatch("warning");
176
+ expect(output).toMatch(basename(tmpDir));
177
+ expect(output).toMatch(/Language:\s*JavaScript/);
178
+ expect(output).toMatch("Routes");
179
+ expect(output).toMatch("Home (/ & /:catchAll)");
180
+ expect(output).toMatch("Account (/account/*)");
181
+ });
85
182
  });
86
- });
87
- it("does not install dependencies when installDeps is false", async () => {
88
- await temporaryDirectoryTask(async (tmpDir) => {
89
- const options = defaultOptions({ installDeps: false, path: tmpDir });
90
- await runInit(options);
91
- expect(installNodeModules).not.toHaveBeenCalled();
183
+ describe("styling libraries", () => {
184
+ it("scaffolds Tailwind CSS", async () => {
185
+ await temporaryDirectoryTask(async (tmpDir) => {
186
+ await runInit({
187
+ path: tmpDir,
188
+ git: false,
189
+ language: "ts",
190
+ styling: "tailwind"
191
+ });
192
+ await expect(readFile(`${tmpDir}/remix.config.js`)).resolves.toMatch(
193
+ /tailwind: true,\n\s*postcss: true,\n\s*future:/
194
+ );
195
+ await expect(
196
+ readFile(`${tmpDir}/app/styles/tailwind.css`)
197
+ ).resolves.toMatch(/@tailwind base;/);
198
+ const rootFile = await readFile(`${tmpDir}/app/root.tsx`);
199
+ await expect(rootFile).toMatch(/import tailwindCss from/);
200
+ await expect(rootFile).toMatch(
201
+ /export function links\(\) \{.*?return \[.*\{rel: 'stylesheet', href: tailwindCss\}/ims
202
+ );
203
+ const output = outputMock.info();
204
+ expect(output).toMatch("success");
205
+ expect(output).not.toMatch("warning");
206
+ expect(output).toMatch(/Styling:\s*Tailwind/);
207
+ });
208
+ });
209
+ it("scaffolds CSS Modules", async () => {
210
+ await temporaryDirectoryTask(async (tmpDir) => {
211
+ await runInit({
212
+ path: tmpDir,
213
+ git: false,
214
+ language: "ts",
215
+ styling: "css-modules"
216
+ });
217
+ await expect(readFile(`${tmpDir}/package.json`)).resolves.toMatch(
218
+ `"@remix-run/css-bundle": "`
219
+ );
220
+ const rootFile = await readFile(`${tmpDir}/app/root.tsx`);
221
+ await expect(rootFile).toMatch(/import {cssBundleHref} from/);
222
+ await expect(rootFile).toMatch(
223
+ /export function links\(\) \{.*?return \[.*\{rel: 'stylesheet', href: cssBundleHref\}/ims
224
+ );
225
+ const output = outputMock.info();
226
+ expect(output).toMatch("success");
227
+ expect(output).not.toMatch("warning");
228
+ expect(output).toMatch(/Styling:\s*CSS Modules/);
229
+ });
230
+ });
231
+ it("scaffolds Vanilla Extract", async () => {
232
+ await temporaryDirectoryTask(async (tmpDir) => {
233
+ await runInit({
234
+ path: tmpDir,
235
+ git: false,
236
+ language: "ts",
237
+ styling: "vanilla-extract"
238
+ });
239
+ const packageJson = await readFile(`${tmpDir}/package.json`);
240
+ expect(packageJson).toMatch(/"@remix-run\/css-bundle": "/);
241
+ expect(packageJson).toMatch(/"@vanilla-extract\/css": "/);
242
+ const rootFile = await readFile(`${tmpDir}/app/root.tsx`);
243
+ await expect(rootFile).toMatch(/import {cssBundleHref} from/);
244
+ await expect(rootFile).toMatch(
245
+ /export function links\(\) \{.*?return \[.*\{rel: 'stylesheet', href: cssBundleHref\}/ims
246
+ );
247
+ const output = outputMock.info();
248
+ expect(output).toMatch("success");
249
+ expect(output).not.toMatch("warning");
250
+ expect(output).toMatch(/Styling:\s*Vanilla Extract/);
251
+ });
252
+ });
92
253
  });
93
- });
94
- it("displays inventory information when using the demo-store template", async () => {
95
- await temporaryDirectoryTask(async (tmpDir) => {
96
- const options = defaultOptions({
97
- installDeps: false,
98
- path: tmpDir,
99
- template: "demo-store"
254
+ describe("i18n strategies", () => {
255
+ it("scaffolds i18n with domains strategy", async () => {
256
+ await temporaryDirectoryTask(async (tmpDir) => {
257
+ await runInit({
258
+ path: tmpDir,
259
+ git: false,
260
+ language: "ts",
261
+ i18n: "domains",
262
+ routes: true
263
+ });
264
+ const projectFiles = await glob("**/*", { cwd: tmpDir });
265
+ expect(projectFiles).toContain("app/routes/_index.tsx");
266
+ const serverFile = await readFile(`${tmpDir}/server.ts`);
267
+ expect(serverFile).toMatch(/i18n: getLocaleFromRequest\(request\),/);
268
+ expect(serverFile).toMatch(/domain = url.hostname/);
269
+ const output = outputMock.info();
270
+ expect(output).toMatch("success");
271
+ expect(output).not.toMatch("warning");
272
+ expect(output).toMatch(/Markets:\s*Top-level domains/);
273
+ });
274
+ });
275
+ it("scaffolds i18n with subdomains strategy", async () => {
276
+ await temporaryDirectoryTask(async (tmpDir) => {
277
+ await runInit({
278
+ path: tmpDir,
279
+ git: false,
280
+ language: "ts",
281
+ i18n: "subdomains",
282
+ routes: true
283
+ });
284
+ const projectFiles = await glob("**/*", { cwd: tmpDir });
285
+ expect(projectFiles).toContain("app/routes/_index.tsx");
286
+ const serverFile = await readFile(`${tmpDir}/server.ts`);
287
+ expect(serverFile).toMatch(/i18n: getLocaleFromRequest\(request\),/);
288
+ expect(serverFile).toMatch(/firstSubdomain = url.hostname/);
289
+ const output = outputMock.info();
290
+ expect(output).toMatch("success");
291
+ expect(output).not.toMatch("warning");
292
+ expect(output).toMatch(/Markets:\s*Subdomains/);
293
+ });
294
+ });
295
+ it("scaffolds i18n with subfolders strategy", async () => {
296
+ await temporaryDirectoryTask(async (tmpDir) => {
297
+ await runInit({
298
+ path: tmpDir,
299
+ git: false,
300
+ language: "ts",
301
+ i18n: "subfolders",
302
+ routes: true
303
+ });
304
+ const projectFiles = await glob("**/*", { cwd: tmpDir });
305
+ expect(projectFiles).toContain("app/routes/($locale)._index.tsx");
306
+ const serverFile = await readFile(`${tmpDir}/server.ts`);
307
+ expect(serverFile).toMatch(/i18n: getLocaleFromRequest\(request\),/);
308
+ expect(serverFile).toMatch(/url.pathname/);
309
+ const output = outputMock.info();
310
+ expect(output).toMatch("success");
311
+ expect(output).not.toMatch("warning");
312
+ expect(output).toMatch(/Markets:\s*Subfolders/);
313
+ });
100
314
  });
101
- await runInit(options);
102
- expect(renderInfo).toHaveBeenCalledTimes(1);
103
- expect(renderInfo).toHaveBeenCalledWith(
104
- expect.objectContaining({
105
- body: expect.stringContaining(
106
- "To connect this project to your Shopify store\u2019s inventory"
107
- ),
108
- headline: expect.stringContaining(
109
- "Your project will display inventory from the Hydrogen Demo Store"
110
- )
111
- })
112
- );
113
315
  });
114
- });
115
- it("does not display inventory information when using non-demo-store templates", async () => {
116
- await temporaryDirectoryTask(async (tmpDir) => {
117
- const options = defaultOptions({
118
- installDeps: false,
119
- path: tmpDir,
120
- template: "pizza-store"
316
+ describe("git", () => {
317
+ it("initializes a git repository and creates initial commits", async () => {
318
+ await temporaryDirectoryTask(async (tmpDir) => {
319
+ renderTasksHook.mockImplementationOnce(async () => {
320
+ await writeFile(`${tmpDir}/package-lock.json`, "{}");
321
+ });
322
+ await runInit({
323
+ path: tmpDir,
324
+ git: true,
325
+ language: "js",
326
+ styling: "tailwind",
327
+ i18n: "domains",
328
+ routes: true,
329
+ installDeps: true
330
+ });
331
+ expect(isDirectory(`${tmpDir}/.git`)).resolves.toBeTruthy();
332
+ const { stdout: gitLog } = await execAsync(`git log --oneline`, {
333
+ cwd: tmpDir
334
+ });
335
+ expect(gitLog.split("\n")).toEqual(
336
+ expect.arrayContaining([
337
+ expect.stringContaining("Lockfile"),
338
+ expect.stringContaining("Generate routes for core functionality"),
339
+ expect.stringContaining("Setup Tailwind"),
340
+ expect.stringContaining("Scaffold Storefront")
341
+ ])
342
+ );
343
+ });
344
+ });
345
+ });
346
+ describe("project validity", () => {
347
+ it("typechecks the project", async () => {
348
+ await temporaryDirectoryTask(async (tmpDir) => {
349
+ renderTasksHook.mockImplementationOnce(async () => {
350
+ await writeFile(`${tmpDir}/package-lock.json`, "{}");
351
+ });
352
+ await runInit({
353
+ path: tmpDir,
354
+ git: true,
355
+ language: "ts",
356
+ styling: "tailwind",
357
+ i18n: "subfolders",
358
+ routes: true,
359
+ installDeps: true
360
+ });
361
+ await rmdir(joinPath(tmpDir, "node_modules")).catch(() => {
362
+ });
363
+ await symlink(
364
+ fileURLToPath(
365
+ new URL("../../../../../node_modules", import.meta.url)
366
+ ),
367
+ joinPath(tmpDir, "node_modules")
368
+ );
369
+ await expect(
370
+ exec("npm", ["run", "typecheck"], {
371
+ cwd: tmpDir
372
+ })
373
+ ).resolves.not.toThrow();
374
+ });
121
375
  });
122
- await runInit(options);
123
- expect(renderInfo).toHaveBeenCalledTimes(0);
124
376
  });
125
377
  });
126
378
  });
@@ -1,19 +1,22 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
- import { renderConfirmationPrompt, renderWarning, renderSelectPrompt, renderSuccess } from '@shopify/cli-kit/node/ui';
3
+ import { basename } from '@shopify/cli-kit/node/path';
4
+ import { renderSuccess, renderConfirmationPrompt, renderWarning, renderSelectPrompt, renderTextPrompt, renderTasks } from '@shopify/cli-kit/node/ui';
5
+ import { AbortError } from '@shopify/cli-kit/node/error';
4
6
  import { commonFlags } from '../../lib/flags.js';
5
- import { getHydrogenShop } from '../../lib/shop.js';
6
7
  import { getStorefronts } from '../../lib/graphql/admin/link-storefront.js';
7
- import { getConfig, setStorefront } from '../../lib/shopify-config.js';
8
- import { logMissingStorefronts } from '../../lib/missing-storefronts.js';
8
+ import { setStorefront } from '../../lib/shopify-config.js';
9
+ import { createStorefront } from '../../lib/graphql/admin/create-storefront.js';
10
+ import { waitForJob } from '../../lib/graphql/admin/fetch-job.js';
11
+ import { titleize } from '../../lib/string.js';
9
12
  import { getCliCommand } from '../../lib/shell.js';
13
+ import { login } from '../../lib/auth.js';
10
14
 
11
15
  class Link extends Command {
12
16
  static description = "Link a local project to one of your shop's Hydrogen storefronts.";
13
17
  static flags = {
14
18
  force: commonFlags.force,
15
19
  path: commonFlags.path,
16
- shop: commonFlags.shop,
17
20
  storefront: Flags.string({
18
21
  description: `The name of a Hydrogen Storefront (e.g. "Jane's Apparel")`,
19
22
  env: "SHOPIFY_HYDROGEN_STOREFRONT"
@@ -21,33 +24,54 @@ class Link extends Command {
21
24
  };
22
25
  async run() {
23
26
  const { flags } = await this.parse(Link);
24
- await linkStorefront(flags);
27
+ await runLink(flags);
25
28
  }
26
29
  }
27
- async function linkStorefront({
30
+ async function runLink({
28
31
  force,
29
- path,
30
- shop: flagShop,
31
- storefront: flagStorefront,
32
- silent = false
32
+ path: root = process.cwd(),
33
+ storefront: flagStorefront
33
34
  }) {
34
- const shop = await getHydrogenShop({ path, shop: flagShop });
35
- const { storefront: configStorefront } = await getConfig(path ?? process.cwd());
36
- if (configStorefront && !force) {
35
+ const [{ session, config }, cliCommand] = await Promise.all([
36
+ login(root),
37
+ getCliCommand()
38
+ ]);
39
+ const linkedStore = await linkStorefront(root, session, config, {
40
+ force,
41
+ flagStorefront,
42
+ cliCommand
43
+ });
44
+ if (!linkedStore)
45
+ return;
46
+ renderSuccess({
47
+ body: [{ userInput: linkedStore.title }, "is now linked"],
48
+ nextSteps: [
49
+ [
50
+ "Run",
51
+ { command: `${cliCommand} dev` },
52
+ "to start your local development server and start building"
53
+ ]
54
+ ]
55
+ });
56
+ }
57
+ async function linkStorefront(root, session, config, {
58
+ force = false,
59
+ flagStorefront,
60
+ cliCommand
61
+ }) {
62
+ if (!config.shop) {
63
+ throw new AbortError("No shop found in local config, login first.");
64
+ }
65
+ if (config.storefront?.id && !force) {
37
66
  const overwriteLink = await renderConfirmationPrompt({
38
- message: `Your project is currently linked to ${configStorefront.title}. Do you want to link to a different Hydrogen storefront on Shopify?`
67
+ message: `Your project is currently linked to ${config.storefront.title}. Do you want to link to a different Hydrogen storefront on Shopify?`
39
68
  });
40
69
  if (!overwriteLink) {
41
70
  return;
42
71
  }
43
72
  }
44
- const { storefronts, adminSession } = await getStorefronts(shop);
45
- if (storefronts.length === 0) {
46
- logMissingStorefronts(adminSession);
47
- return;
48
- }
73
+ const storefronts = await getStorefronts(session);
49
74
  let selectedStorefront;
50
- const cliCommand = await getCliCommand();
51
75
  if (flagStorefront) {
52
76
  selectedStorefront = storefronts.find(
53
77
  ({ title }) => title === flagStorefront
@@ -59,7 +83,7 @@ async function linkStorefront({
59
83
  "There's no storefront matching",
60
84
  { userInput: flagStorefront },
61
85
  "on your",
62
- { userInput: shop },
86
+ { userInput: config.shop },
63
87
  "shop. To see all available Hydrogen storefronts, run",
64
88
  {
65
89
  command: `${cliCommand} list`
@@ -69,32 +93,66 @@ async function linkStorefront({
69
93
  return;
70
94
  }
71
95
  } else {
72
- const choices = storefronts.map(({ id, title, productionUrl }) => ({
73
- value: id,
74
- label: `${title} (${productionUrl})`
75
- }));
96
+ const choices = [
97
+ {
98
+ label: "Create a new storefront",
99
+ value: null
100
+ },
101
+ ...storefronts.map(({ id, title, productionUrl }) => ({
102
+ label: `${title} (${productionUrl})`,
103
+ value: id
104
+ }))
105
+ ];
76
106
  const storefrontId = await renderSelectPrompt({
77
- message: "Choose a Hydrogen storefront to link",
107
+ message: "Select a Hydrogen storefront to link",
78
108
  choices
79
109
  });
80
- selectedStorefront = storefronts.find(({ id }) => id === storefrontId);
110
+ if (storefrontId) {
111
+ selectedStorefront = storefronts.find(({ id }) => id === storefrontId);
112
+ } else {
113
+ selectedStorefront = await createNewStorefront(root, session);
114
+ }
81
115
  }
82
- if (!selectedStorefront) {
83
- return;
116
+ if (selectedStorefront) {
117
+ await setStorefront(root, selectedStorefront);
84
118
  }
85
- await setStorefront(path ?? process.cwd(), selectedStorefront);
86
- if (!silent) {
87
- renderSuccess({
88
- body: [{ userInput: selectedStorefront.title }, "is now linked"],
89
- nextSteps: [
90
- [
91
- "Run",
92
- { command: `${cliCommand} dev` },
93
- "to start your local development server and start building"
94
- ]
95
- ]
96
- });
119
+ return selectedStorefront;
120
+ }
121
+ async function createNewStorefront(root, session) {
122
+ const projectDirectory = basename(root);
123
+ const projectName = await renderTextPrompt({
124
+ message: "New storefront name",
125
+ defaultValue: titleize(projectDirectory)
126
+ });
127
+ let storefront;
128
+ let jobId;
129
+ await renderTasks([
130
+ {
131
+ title: "Creating storefront",
132
+ task: async () => {
133
+ const result = await createStorefront(session, projectName);
134
+ storefront = result.storefront;
135
+ jobId = result.jobId;
136
+ }
137
+ },
138
+ {
139
+ title: "Creating API tokens",
140
+ task: async () => {
141
+ try {
142
+ await waitForJob(session, jobId);
143
+ } catch (_err) {
144
+ storefront = void 0;
145
+ }
146
+ },
147
+ skip: () => !jobId
148
+ }
149
+ ]);
150
+ if (!storefront) {
151
+ throw new AbortError(
152
+ "Unknown error ocurred. Please try again or contact support if the error persists."
153
+ );
97
154
  }
155
+ return storefront;
98
156
  }
99
157
 
100
- export { Link as default, linkStorefront };
158
+ export { Link as default, linkStorefront, runLink };