@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,28 @@
1
+ import type {LanguageCode, CountryCode} from '../mock-i18n-types.js';
2
+
3
+ export type I18nLocale = {
4
+ language: LanguageCode;
5
+ country: CountryCode;
6
+ pathPrefix: string;
7
+ };
8
+
9
+ function getLocaleFromRequest(request: Request): I18nLocale {
10
+ const url = new URL(request.url);
11
+ const firstPathPart = url.pathname.split('/')[1]?.toUpperCase() ?? '';
12
+
13
+ let pathPrefix = '';
14
+ let language: LanguageCode = 'EN';
15
+ let country: CountryCode = 'US';
16
+
17
+ if (/^[A-Z]{2}-[A-Z]{2}$/i.test(firstPathPart)) {
18
+ pathPrefix = '/' + firstPathPart;
19
+ [language, country] = firstPathPart.split('-') as [
20
+ LanguageCode,
21
+ CountryCode,
22
+ ];
23
+ }
24
+
25
+ return {language, country, pathPrefix};
26
+ }
27
+
28
+ export {getLocaleFromRequest};
@@ -0,0 +1,244 @@
1
+ import { readdir } from 'fs/promises';
2
+ import { fileExists, mkdir, copyFile, readFile, writeFile } from '@shopify/cli-kit/node/fs';
3
+ import { joinPath, relativizePath, dirname, relativePath, resolvePath, basename } from '@shopify/cli-kit/node/path';
4
+ import { AbortError } from '@shopify/cli-kit/node/error';
5
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
6
+ import { transpileFile } from '../../../lib/transpile-ts.js';
7
+ import { getCodeFormatOptions, formatCode } from '../../../lib/format-code.js';
8
+ import { getTemplateAppFile, GENERATOR_ROUTE_DIR, getStarterDir } from '../../../lib/build.js';
9
+ import { getV2Flags, convertTemplateToRemixVersion, convertRouteToV1 } from '../../../lib/remix-version-interop.js';
10
+ import { getRemixConfig } from '../../../lib/config.js';
11
+ import { findFileWithExtension } from '../../file.js';
12
+
13
+ const NO_LOCALE_PATTERNS = [/robots\.txt/];
14
+ const ROUTE_MAP = {
15
+ home: ["_index", "$"],
16
+ page: "pages*",
17
+ cart: "cart",
18
+ products: "products*",
19
+ collections: "collections*",
20
+ policies: "policies*",
21
+ blogs: "blogs*",
22
+ account: "account*",
23
+ search: ["search", "api.predictive-search"],
24
+ robots: "[robots.txt]",
25
+ sitemap: "[sitemap.xml]"
26
+ };
27
+ let allRouteTemplateFiles = [];
28
+ async function getResolvedRoutes(routeKeys = Object.keys(ROUTE_MAP)) {
29
+ if (allRouteTemplateFiles.length === 0) {
30
+ allRouteTemplateFiles = (await readdir(getTemplateAppFile(GENERATOR_ROUTE_DIR))).map((item) => item.replace(/\.tsx?$/, ""));
31
+ }
32
+ const routeGroups = {};
33
+ const resolvedRouteFiles = [];
34
+ for (const key of routeKeys) {
35
+ routeGroups[key] = [];
36
+ const value = ROUTE_MAP[key];
37
+ if (!value) {
38
+ throw new AbortError(
39
+ `No route found for ${key}. Try one of ${ALL_ROUTE_CHOICES.join()}.`
40
+ );
41
+ }
42
+ const routes = Array.isArray(value) ? value : [value];
43
+ for (const route of routes) {
44
+ const routePrefix = route.replace("*", "");
45
+ routeGroups[key].push(
46
+ ...allRouteTemplateFiles.filter((file) => file.startsWith(routePrefix))
47
+ );
48
+ }
49
+ resolvedRouteFiles.push(...routeGroups[key]);
50
+ }
51
+ return { routeGroups, resolvedRouteFiles };
52
+ }
53
+ const ALL_ROUTE_CHOICES = [...Object.keys(ROUTE_MAP), "all"];
54
+ async function generateRoutes(options) {
55
+ const { routeGroups, resolvedRouteFiles } = options.routeName === "all" ? await getResolvedRoutes() : await getResolvedRoutes([options.routeName]);
56
+ const { rootDirectory, appDirectory, future, tsconfigPath } = await getRemixConfig(options.directory);
57
+ const routesArray = resolvedRouteFiles.flatMap(
58
+ (item) => GENERATOR_ROUTE_DIR + "/" + item
59
+ );
60
+ const v2Flags = await getV2Flags(rootDirectory, future);
61
+ const formatOptions = await getCodeFormatOptions(rootDirectory);
62
+ const localePrefix = await getLocalePrefix(
63
+ appDirectory,
64
+ options,
65
+ v2Flags.isV2RouteConvention
66
+ );
67
+ const typescript = options.typescript ?? !!tsconfigPath;
68
+ const transpilerOptions = typescript ? void 0 : await getJsTranspilerOptions(rootDirectory);
69
+ const routes = [];
70
+ for (const route of routesArray) {
71
+ routes.push(
72
+ await generateProjectFile(route, {
73
+ ...options,
74
+ typescript,
75
+ localePrefix,
76
+ rootDirectory,
77
+ appDirectory,
78
+ formatOptions,
79
+ transpilerOptions,
80
+ v2Flags
81
+ })
82
+ );
83
+ }
84
+ return {
85
+ routes,
86
+ routeGroups,
87
+ isTypescript: typescript,
88
+ transpilerOptions,
89
+ v2Flags,
90
+ formatOptions
91
+ };
92
+ }
93
+ async function getLocalePrefix(appDirectory, { localePrefix, routeName }, isV2RouteConvention = true) {
94
+ if (localePrefix)
95
+ return localePrefix;
96
+ if (localePrefix !== void 0 || routeName === "all")
97
+ return;
98
+ const existingFiles = await readdir(joinPath(appDirectory, "routes")).catch(
99
+ () => []
100
+ );
101
+ const homeRouteWithLocaleRE = isV2RouteConvention ? /^\(\$(\w+)\)\._index.[jt]sx?$/ : /^\(\$(\w+)\)$/;
102
+ const homeRouteWithLocale = existingFiles.find(
103
+ (file) => homeRouteWithLocaleRE.test(file)
104
+ );
105
+ if (homeRouteWithLocale) {
106
+ return homeRouteWithLocale.match(homeRouteWithLocaleRE)?.[1];
107
+ }
108
+ }
109
+ async function generateProjectFile(routeFrom, {
110
+ rootDirectory,
111
+ appDirectory,
112
+ typescript,
113
+ force,
114
+ adapter,
115
+ templatesRoot = getStarterDir(),
116
+ transpilerOptions,
117
+ formatOptions,
118
+ localePrefix,
119
+ v2Flags = {},
120
+ signal
121
+ }) {
122
+ const extension = (routeFrom.match(/(\.[jt]sx?)$/) ?? [])[1] ?? ".tsx";
123
+ routeFrom = routeFrom.replace(extension, "");
124
+ const routeTemplatePath = getTemplateAppFile(
125
+ routeFrom + extension,
126
+ templatesRoot
127
+ );
128
+ const allFilesToGenerate = await findRouteDependencies(
129
+ routeTemplatePath,
130
+ getTemplateAppFile("", templatesRoot)
131
+ );
132
+ const routeDestinationPath = joinPath(
133
+ appDirectory,
134
+ getDestinationRoute(routeFrom, localePrefix, v2Flags) + (typescript ? extension : extension.replace(".ts", ".js"))
135
+ );
136
+ const result = {
137
+ operation: "created",
138
+ sourceRoute: routeFrom,
139
+ destinationRoute: relativizePath(routeDestinationPath, rootDirectory)
140
+ };
141
+ if (!force && await fileExists(routeDestinationPath)) {
142
+ const shouldOverwrite = await renderConfirmationPrompt({
143
+ message: `The file ${result.destinationRoute} already exists. Do you want to replace it?`,
144
+ defaultValue: false,
145
+ confirmationMessage: "Yes",
146
+ cancellationMessage: "No",
147
+ abortSignal: signal
148
+ });
149
+ if (!shouldOverwrite)
150
+ return { ...result, operation: "skipped" };
151
+ result.operation = "replaced";
152
+ }
153
+ for (const filePath of allFilesToGenerate) {
154
+ const isRoute = filePath.startsWith(GENERATOR_ROUTE_DIR + "/");
155
+ const destinationPath = isRoute ? routeDestinationPath : joinPath(
156
+ appDirectory,
157
+ filePath.replace(/\.ts(x?)$/, `.${typescript ? "ts$1" : "js$1"}`)
158
+ );
159
+ if (!await fileExists(dirname(destinationPath))) {
160
+ await mkdir(dirname(destinationPath));
161
+ }
162
+ if (!/\.[jt]sx?$/.test(filePath)) {
163
+ await copyFile(
164
+ getTemplateAppFile(filePath, templatesRoot),
165
+ destinationPath
166
+ );
167
+ continue;
168
+ }
169
+ let templateContent = convertTemplateToRemixVersion(
170
+ await readFile(getTemplateAppFile(filePath, templatesRoot)),
171
+ v2Flags
172
+ );
173
+ if (!typescript) {
174
+ templateContent = transpileFile(templateContent, transpilerOptions);
175
+ }
176
+ if (adapter) {
177
+ templateContent = templateContent.replace(
178
+ /@shopify\/remix-oxygen/g,
179
+ adapter
180
+ );
181
+ }
182
+ templateContent = formatCode(
183
+ templateContent,
184
+ formatOptions,
185
+ destinationPath
186
+ );
187
+ await writeFile(destinationPath, templateContent);
188
+ }
189
+ return result;
190
+ }
191
+ function getDestinationRoute(routeFrom, localePrefix, v2Flags) {
192
+ const routePath = routeFrom.replace(GENERATOR_ROUTE_DIR + "/", "");
193
+ const filePrefix = localePrefix && !NO_LOCALE_PATTERNS.some((pattern) => pattern.test(routePath)) ? `($${localePrefix})` + (v2Flags.isV2RouteConvention ? "." : "/") : "";
194
+ return GENERATOR_ROUTE_DIR + "/" + filePrefix + (v2Flags.isV2RouteConvention ? routePath : convertRouteToV1(routePath));
195
+ }
196
+ async function findRouteDependencies(routeFilePath, appDirectory) {
197
+ const filesToCheck = /* @__PURE__ */ new Set([routeFilePath]);
198
+ const fileDependencies = /* @__PURE__ */ new Set([relativePath(appDirectory, routeFilePath)]);
199
+ for (const filePath of filesToCheck) {
200
+ const importMatches = (await readFile(filePath, { encoding: "utf8" })).matchAll(/^(import|export)\s+.*?\s+from\s+['"](.*?)['"];?$/gims);
201
+ for (let [, , match] of importMatches) {
202
+ if (!match || !/^(\.|~)/.test(match))
203
+ continue;
204
+ match = match.replace(
205
+ "~",
206
+ relativePath(dirname(filePath), appDirectory) || "."
207
+ );
208
+ const resolvedMatchPath = resolvePath(dirname(filePath), match);
209
+ const absoluteFilepath = (await findFileWithExtension(
210
+ dirname(resolvedMatchPath),
211
+ basename(resolvedMatchPath)
212
+ )).filepath || resolvedMatchPath;
213
+ if (!absoluteFilepath.includes(`/${GENERATOR_ROUTE_DIR}/`)) {
214
+ fileDependencies.add(relativePath(appDirectory, absoluteFilepath));
215
+ if (/\.[jt]sx?$/.test(absoluteFilepath)) {
216
+ filesToCheck.add(absoluteFilepath);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ return [...fileDependencies];
222
+ }
223
+ async function getJsTranspilerOptions(rootDirectory) {
224
+ const jsConfigPath = joinPath(rootDirectory, "jsconfig.json");
225
+ if (!await fileExists(jsConfigPath))
226
+ return;
227
+ return JSON.parse(
228
+ (await readFile(jsConfigPath, { encoding: "utf8" })).replace(
229
+ /^\s*\/\/.*$/gm,
230
+ ""
231
+ )
232
+ )?.compilerOptions;
233
+ }
234
+ async function renderRoutePrompt(options) {
235
+ const generateAll = await renderConfirmationPrompt({
236
+ message: "Scaffold all standard route files? " + Object.keys(ROUTE_MAP).join(", "),
237
+ confirmationMessage: "Yes",
238
+ cancellationMessage: "No",
239
+ ...options
240
+ });
241
+ return generateAll ? "all" : [];
242
+ }
243
+
244
+ export { ALL_ROUTE_CHOICES, generateProjectFile, generateRoutes, getResolvedRoutes, renderRoutePrompt };
@@ -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 '../../../lib/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("../../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 };