@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,227 @@
1
+ import { AbortError } from '@shopify/cli-kit/node/error';
2
+ import { joinPath, relativePath } from '@shopify/cli-kit/node/path';
3
+ import { fileExists } from '@shopify/cli-kit/node/fs';
4
+ import { ts, tsx, js, jsx } from '@ast-grep/napi';
5
+ import { replaceFileContent, findFileWithExtension } from '../../file.js';
6
+
7
+ const astGrep = { ts, tsx, js, jsx };
8
+ async function replaceServerI18n({ rootDirectory, serverEntryPoint = "server" }, formatConfig, localeExtractImplementation) {
9
+ const { filepath, astType } = await findEntryFile({
10
+ rootDirectory,
11
+ serverEntryPoint
12
+ });
13
+ await replaceFileContent(filepath, formatConfig, async (content) => {
14
+ const root = astGrep[astType].parse(content).root();
15
+ const requestIdentifier = root.find({
16
+ rule: {
17
+ kind: "identifier",
18
+ inside: {
19
+ kind: "formal_parameters",
20
+ stopBy: "end",
21
+ inside: {
22
+ kind: "method_definition",
23
+ stopBy: "end",
24
+ has: {
25
+ kind: "property_identifier",
26
+ regex: "^fetch$"
27
+ },
28
+ inside: {
29
+ kind: "export_statement",
30
+ stopBy: "end"
31
+ }
32
+ }
33
+ }
34
+ }
35
+ });
36
+ const requestIdentifierName = requestIdentifier?.text() ?? "request";
37
+ const i18nFunctionName = localeExtractImplementation.match(
38
+ /^(export )?function (\w+)/m
39
+ )?.[2];
40
+ if (!i18nFunctionName) {
41
+ throw new Error("Could not find the i18n function name");
42
+ }
43
+ const i18nFunctionCall = `${i18nFunctionName}(${requestIdentifierName})`;
44
+ const hydrogenImportPath = "@shopify/hydrogen";
45
+ const hydrogenImportName = "createStorefrontClient";
46
+ const importSpecifier = root.find({
47
+ rule: {
48
+ kind: "import_specifier",
49
+ inside: {
50
+ kind: "import_statement",
51
+ stopBy: "end",
52
+ has: {
53
+ kind: "string_fragment",
54
+ stopBy: "end",
55
+ regex: `^${hydrogenImportPath}$`
56
+ }
57
+ },
58
+ has: {
59
+ kind: "identifier",
60
+ regex: `^${hydrogenImportName}`
61
+ }
62
+ }
63
+ });
64
+ let [importName, importAlias] = importSpecifier?.text().split(/\s+as\s+/) || [];
65
+ importName = importAlias ?? importName;
66
+ if (!importName) {
67
+ throw new AbortError(
68
+ `Could not find a Hydrogen import in ${serverEntryPoint}`,
69
+ `Please import "${hydrogenImportName}" from "${hydrogenImportPath}"`
70
+ );
71
+ }
72
+ const argumentObject = root.find({
73
+ rule: {
74
+ kind: "object",
75
+ inside: {
76
+ kind: "arguments",
77
+ inside: {
78
+ kind: "call_expression",
79
+ has: {
80
+ kind: "identifier",
81
+ regex: importName
82
+ }
83
+ }
84
+ }
85
+ }
86
+ });
87
+ if (!argumentObject) {
88
+ throw new AbortError(
89
+ `Could not find a Hydrogen client instantiation with an inline object as argument in ${serverEntryPoint}`,
90
+ `Please add a call to ${importName}({...})`
91
+ );
92
+ }
93
+ const i18nProperty = argumentObject.find({
94
+ rule: {
95
+ kind: "property_identifier",
96
+ regex: "^i18n$"
97
+ }
98
+ });
99
+ const i18nValue = i18nProperty?.next()?.next();
100
+ if (i18nValue) {
101
+ if (i18nValue.text().includes(i18nFunctionName)) {
102
+ throw new AbortError(
103
+ "An i18n strategy is already set up.",
104
+ `Remove the existing i18n property in the ${importName}({...}) call to set up a new one.`
105
+ );
106
+ }
107
+ const { start, end } = i18nValue.range();
108
+ content = content.slice(0, start.index) + i18nFunctionCall + content.slice(end.index);
109
+ } else {
110
+ const { end } = argumentObject.range();
111
+ const firstPart = content.slice(0, end.index - 1);
112
+ content = firstPart + ((/,\s*$/.test(firstPart) ? "" : ",") + `i18n: ${i18nFunctionCall}`) + content.slice(end.index - 1);
113
+ }
114
+ const importTypes = localeExtractImplementation.match(
115
+ /import\s+type\s+[^;]+?;/
116
+ )?.[0];
117
+ if (importTypes) {
118
+ localeExtractImplementation = localeExtractImplementation.replace(
119
+ importTypes,
120
+ ""
121
+ );
122
+ const lastImportNode = root.findAll({ rule: { kind: "import_statement" } }).pop();
123
+ if (lastImportNode) {
124
+ const lastImportContent = lastImportNode.text();
125
+ content = content.replace(
126
+ lastImportContent,
127
+ lastImportContent + "\n" + importTypes.replace(
128
+ /'[^']+'/,
129
+ `'@shopify/hydrogen/storefront-api-types'`
130
+ )
131
+ );
132
+ }
133
+ }
134
+ return content + `
135
+
136
+ ${localeExtractImplementation.replace(/^export function/m, "function").replace(/^export {.*?;/m, "")}
137
+ `;
138
+ });
139
+ }
140
+ async function replaceRemixEnv({ rootDirectory, serverEntryPoint }, formatConfig, localeExtractImplementation) {
141
+ const remixEnvPath = joinPath(rootDirectory, "remix.env.d.ts");
142
+ if (!await fileExists(remixEnvPath)) {
143
+ return;
144
+ }
145
+ const i18nTypeName = localeExtractImplementation.match(/export type (\w+)/)?.[1];
146
+ if (!i18nTypeName) {
147
+ return;
148
+ }
149
+ const { filepath: entryFilepath } = await findEntryFile({
150
+ rootDirectory,
151
+ serverEntryPoint
152
+ });
153
+ const relativePathToEntry = relativePath(
154
+ rootDirectory,
155
+ entryFilepath
156
+ ).replace(/.[tj]sx?$/, "");
157
+ await replaceFileContent(remixEnvPath, formatConfig, (content) => {
158
+ if (content.includes(`Storefront<`))
159
+ return;
160
+ const root = astGrep.ts.parse(content).root();
161
+ const storefrontTypeNode = root.find({
162
+ rule: {
163
+ kind: "property_signature",
164
+ has: {
165
+ kind: "type_annotation",
166
+ has: {
167
+ regex: "^Storefront$"
168
+ }
169
+ },
170
+ inside: {
171
+ kind: "interface_declaration",
172
+ stopBy: "end",
173
+ regex: "AppLoadContext"
174
+ }
175
+ }
176
+ });
177
+ if (!storefrontTypeNode) {
178
+ return;
179
+ }
180
+ const { end } = storefrontTypeNode.range();
181
+ content = content.slice(0, end.index) + `<${i18nTypeName}>` + content.slice(end.index);
182
+ const serverImportNode = root.findAll({
183
+ rule: {
184
+ kind: "import_statement",
185
+ has: {
186
+ kind: "string_fragment",
187
+ stopBy: "end",
188
+ regex: `^(\\./)?${relativePathToEntry.replaceAll(
189
+ ".",
190
+ "\\."
191
+ )}(\\.[jt]sx?)?$`
192
+ }
193
+ }
194
+ }).pop();
195
+ if (serverImportNode) {
196
+ content = content.replace(
197
+ serverImportNode.text(),
198
+ serverImportNode.text().replace("{", `{${i18nTypeName},`)
199
+ );
200
+ } else {
201
+ const lastImportNode = root.findAll({ rule: { kind: "import_statement" } }).pop() ?? root.findAll({ rule: { kind: "comment", regex: "^/// <reference" } }).pop();
202
+ const { end: end2 } = lastImportNode?.range() ?? { end: { index: 0 } };
203
+ const typeImport = `
204
+ import type {${i18nTypeName}} from './${serverEntryPoint.replace(
205
+ /\.[jt]s$/,
206
+ ""
207
+ )}';`;
208
+ content = content.slice(0, end2.index) + typeImport + content.slice(end2.index);
209
+ }
210
+ return content;
211
+ });
212
+ }
213
+ async function findEntryFile({
214
+ rootDirectory,
215
+ serverEntryPoint = "server"
216
+ }) {
217
+ const match = serverEntryPoint.match(/\.([jt]sx?)$/)?.[1];
218
+ const { filepath, astType } = match ? { filepath: joinPath(rootDirectory, serverEntryPoint), astType: match } : await findFileWithExtension(rootDirectory, serverEntryPoint);
219
+ if (!filepath || !astType) {
220
+ throw new AbortError(
221
+ `Could not find a server entry point at ${serverEntryPoint}`
222
+ );
223
+ }
224
+ return { filepath, astType };
225
+ }
226
+
227
+ export { replaceRemixEnv, replaceServerI18n };
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getLocaleFromRequest } from './templates/subdomains.js';
3
+
4
+ describe("Setup i18n with subdomains", () => {
5
+ it("extracts the locale from the subdomain", () => {
6
+ expect(
7
+ getLocaleFromRequest(new Request("https://example.com"))
8
+ ).toMatchObject({
9
+ language: "EN",
10
+ country: "US"
11
+ });
12
+ expect(
13
+ getLocaleFromRequest(new Request("https://jp.example.com"))
14
+ ).toMatchObject({
15
+ language: "JA",
16
+ country: "JP"
17
+ });
18
+ expect(
19
+ getLocaleFromRequest(new Request("https://es.sub.example.com"))
20
+ ).toMatchObject({
21
+ language: "ES",
22
+ country: "ES"
23
+ });
24
+ });
25
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getLocaleFromRequest } from './templates/subfolders.js';
3
+
4
+ describe("Setup i18n with subfolders", () => {
5
+ it("extracts the locale from the pathname", () => {
6
+ expect(
7
+ getLocaleFromRequest(new Request("https://example.com"))
8
+ ).toMatchObject({
9
+ language: "EN",
10
+ country: "US"
11
+ });
12
+ expect(
13
+ getLocaleFromRequest(new Request("https://example.com/ja-jp"))
14
+ ).toMatchObject({
15
+ language: "JA",
16
+ country: "JP"
17
+ });
18
+ expect(
19
+ getLocaleFromRequest(new Request("https://example.com/es-es/path"))
20
+ ).toMatchObject({
21
+ language: "ES",
22
+ country: "ES"
23
+ });
24
+ });
25
+ });
@@ -0,0 +1,14 @@
1
+ function getLocaleFromRequest(request) {
2
+ const defaultLocale = { language: "EN", country: "US" };
3
+ const supportedLocales = {
4
+ ES: "ES",
5
+ FR: "FR",
6
+ DE: "DE",
7
+ JP: "JA"
8
+ };
9
+ const url = new URL(request.url);
10
+ const domain = url.hostname.split(".").pop()?.toUpperCase();
11
+ return supportedLocales[domain] ? { language: supportedLocales[domain], country: domain } : defaultLocale;
12
+ }
13
+
14
+ export { getLocaleFromRequest };
@@ -0,0 +1,25 @@
1
+ import type {LanguageCode, CountryCode} from '../mock-i18n-types.js';
2
+
3
+ export type I18nLocale = {language: LanguageCode; country: CountryCode};
4
+
5
+ function getLocaleFromRequest(request: Request): I18nLocale {
6
+ const defaultLocale: I18nLocale = {language: 'EN', country: 'US'};
7
+ const supportedLocales = {
8
+ ES: 'ES',
9
+ FR: 'FR',
10
+ DE: 'DE',
11
+ JP: 'JA',
12
+ } as Record<CountryCode, LanguageCode>;
13
+
14
+ const url = new URL(request.url);
15
+ const domain = url.hostname
16
+ .split('.')
17
+ .pop()
18
+ ?.toUpperCase() as keyof typeof supportedLocales;
19
+
20
+ return supportedLocales[domain]
21
+ ? {language: supportedLocales[domain], country: domain}
22
+ : defaultLocale;
23
+ }
24
+
25
+ export {getLocaleFromRequest};
@@ -0,0 +1,14 @@
1
+ function getLocaleFromRequest(request) {
2
+ const defaultLocale = { language: "EN", country: "US" };
3
+ const supportedLocales = {
4
+ ES: "ES",
5
+ FR: "FR",
6
+ DE: "DE",
7
+ JP: "JA"
8
+ };
9
+ const url = new URL(request.url);
10
+ const firstSubdomain = url.hostname.split(".")[0]?.toUpperCase();
11
+ return supportedLocales[firstSubdomain] ? { language: supportedLocales[firstSubdomain], country: firstSubdomain } : defaultLocale;
12
+ }
13
+
14
+ export { getLocaleFromRequest };
@@ -0,0 +1,24 @@
1
+ import type {LanguageCode, CountryCode} from '../mock-i18n-types.js';
2
+
3
+ export type I18nLocale = {language: LanguageCode; country: CountryCode};
4
+
5
+ function getLocaleFromRequest(request: Request): I18nLocale {
6
+ const defaultLocale: I18nLocale = {language: 'EN', country: 'US'};
7
+ const supportedLocales = {
8
+ ES: 'ES',
9
+ FR: 'FR',
10
+ DE: 'DE',
11
+ JP: 'JA',
12
+ } as Record<CountryCode, LanguageCode>;
13
+
14
+ const url = new URL(request.url);
15
+ const firstSubdomain = url.hostname
16
+ .split('.')[0]
17
+ ?.toUpperCase() as keyof typeof supportedLocales;
18
+
19
+ return supportedLocales[firstSubdomain]
20
+ ? {language: supportedLocales[firstSubdomain], country: firstSubdomain}
21
+ : defaultLocale;
22
+ }
23
+
24
+ export {getLocaleFromRequest};
@@ -0,0 +1,14 @@
1
+ function getLocaleFromRequest(request) {
2
+ const url = new URL(request.url);
3
+ const firstPathPart = url.pathname.split("/")[1]?.toUpperCase() ?? "";
4
+ let pathPrefix = "";
5
+ let language = "EN";
6
+ let country = "US";
7
+ if (/^[A-Z]{2}-[A-Z]{2}$/i.test(firstPathPart)) {
8
+ pathPrefix = "/" + firstPathPart;
9
+ [language, country] = firstPathPart.split("-");
10
+ }
11
+ return { language, country, pathPrefix };
12
+ }
13
+
14
+ export { getLocaleFromRequest };
@@ -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 '../../remix-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 };