@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,137 @@
1
+ import { AbortError } from '@shopify/cli-kit/node/error';
2
+ import { ts, tsx, js, jsx } from '@ast-grep/napi';
3
+ import { findFileWithExtension, replaceFileContent } from '../../file.js';
4
+
5
+ const astGrep = { ts, tsx, js, jsx };
6
+ async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
7
+ const { filepath, astType } = await findFileWithExtension(
8
+ rootDirectory,
9
+ "remix.config"
10
+ );
11
+ if (!filepath || !astType) {
12
+ throw new AbortError(
13
+ `Could not find remix.config.js file in ${rootDirectory}`
14
+ );
15
+ }
16
+ await replaceFileContent(filepath, formatConfig, async (content) => {
17
+ const root = astGrep[astType].parse(content).root();
18
+ const remixConfigNode = root.find({
19
+ rule: {
20
+ kind: "object",
21
+ inside: {
22
+ any: [
23
+ {
24
+ kind: "export_statement"
25
+ },
26
+ {
27
+ kind: "assignment_expression",
28
+ has: {
29
+ kind: "member_expression",
30
+ field: "left",
31
+ pattern: "module.exports"
32
+ }
33
+ }
34
+ ]
35
+ }
36
+ }
37
+ });
38
+ if (!remixConfigNode) {
39
+ throw new AbortError(
40
+ "Could not find a default export in remix.config.js"
41
+ );
42
+ }
43
+ newProperties = { ...newProperties };
44
+ for (const key of Object.keys(newProperties)) {
45
+ const propertyNode = remixConfigNode.find({
46
+ rule: {
47
+ kind: "pair",
48
+ has: {
49
+ field: "key",
50
+ regex: `^${key}$`
51
+ }
52
+ }
53
+ });
54
+ if (propertyNode?.text().endsWith(" " + JSON.stringify(newProperties[key]))) {
55
+ delete newProperties[key];
56
+ }
57
+ }
58
+ if (Object.keys(newProperties).length === 0) {
59
+ return;
60
+ }
61
+ const childrenNodes = remixConfigNode.children();
62
+ const lastNode = childrenNodes.find((node) => node.text().startsWith("future:")) ?? childrenNodes.pop();
63
+ if (!lastNode) {
64
+ throw new AbortError("Could not add properties to Remix config");
65
+ }
66
+ const { start } = lastNode.range();
67
+ return content.slice(0, start.index) + JSON.stringify(newProperties).slice(1, -1) + "," + content.slice(start.index);
68
+ });
69
+ }
70
+ async function replaceRootLinks(appDirectory, formatConfig, importer) {
71
+ const { filepath, astType } = await findFileWithExtension(appDirectory, "root");
72
+ if (!filepath || !astType) {
73
+ throw new AbortError(`Could not find root file in ${appDirectory}`);
74
+ }
75
+ await replaceFileContent(filepath, formatConfig, async (content) => {
76
+ const importStatement = `import ${importer.isDefault ? importer.name : `{${importer.name}}`} from '${(importer.isAbsolute ? "" : "./") + importer.path}';`;
77
+ if (content.includes(importStatement.split("from")[0])) {
78
+ return;
79
+ }
80
+ const root = astGrep[astType].parse(content).root();
81
+ const lastImportNode = root.findAll({ rule: { kind: "import_statement" } }).pop();
82
+ const linksReturnNode = root.find({
83
+ utils: {
84
+ "has-links-id": {
85
+ has: {
86
+ kind: "identifier",
87
+ pattern: "links"
88
+ }
89
+ }
90
+ },
91
+ rule: {
92
+ kind: "return_statement",
93
+ pattern: "return [$$$]",
94
+ inside: {
95
+ any: [
96
+ {
97
+ kind: "function_declaration",
98
+ matches: "has-links-id"
99
+ },
100
+ {
101
+ kind: "variable_declarator",
102
+ matches: "has-links-id"
103
+ }
104
+ ],
105
+ stopBy: "end",
106
+ inside: {
107
+ stopBy: "end",
108
+ kind: "export_statement"
109
+ }
110
+ }
111
+ }
112
+ });
113
+ if (!lastImportNode || !linksReturnNode) {
114
+ throw new AbortError(
115
+ 'Could not find a "links" export in root file. Please add one and try again.'
116
+ );
117
+ }
118
+ const lastImportContent = lastImportNode.text();
119
+ const linksExportReturnContent = linksReturnNode.text();
120
+ const newLinkReturnItem = importer.isConditional ? `...(${importer.name} ? [{ rel: 'stylesheet', href: ${importer.name} }] : [])` : `{rel: 'stylesheet', href: ${importer.name}}`;
121
+ return content.replace(lastImportContent, lastImportContent + "\n" + importStatement).replace(
122
+ linksExportReturnContent,
123
+ linksExportReturnContent.replace("[", `[${newLinkReturnItem},`)
124
+ );
125
+ });
126
+ }
127
+ function injectCssBundlingLink(appDirectory, formatConfig) {
128
+ return replaceRootLinks(appDirectory, formatConfig, {
129
+ name: "cssBundleHref",
130
+ path: "@remix-run/css-bundle",
131
+ isDefault: false,
132
+ isConditional: true,
133
+ isAbsolute: true
134
+ });
135
+ }
136
+
137
+ export { injectCssBundlingLink, replaceRemixConfig, replaceRootLinks };
@@ -0,0 +1,54 @@
1
+ import { outputInfo } from '@shopify/cli-kit/node/output';
2
+ import { relativePath, joinPath } from '@shopify/cli-kit/node/path';
3
+ import { canWriteFiles, mergePackageJson, copyAssets } from './assets.js';
4
+ import { getCodeFormatOptions } from '../../format-code.js';
5
+ import { replaceRemixConfig, replaceRootLinks } from './replacers.js';
6
+
7
+ const tailwindCssPath = "styles/tailwind.css";
8
+ async function setupTailwind({ rootDirectory, appDirectory, ...futureOptions }, force = false) {
9
+ const relativeAppDirectory = relativePath(rootDirectory, appDirectory);
10
+ const assetMap = {
11
+ "tailwind.config.js": "tailwind.config.js",
12
+ "postcss.config.js": "postcss.config.js",
13
+ "tailwind.css": joinPath(relativeAppDirectory, tailwindCssPath)
14
+ };
15
+ if (futureOptions.tailwind && futureOptions.postcss) {
16
+ outputInfo(`Tailwind and PostCSS are already setup in ${rootDirectory}.`);
17
+ return;
18
+ }
19
+ if (!await canWriteFiles(assetMap, appDirectory, force)) {
20
+ outputInfo(
21
+ `Skipping CSS setup as some files already exist. You may use \`--force\` or \`-f\` to override it.`
22
+ );
23
+ return;
24
+ }
25
+ const workPromise = Promise.all([
26
+ mergePackageJson("tailwind", rootDirectory),
27
+ copyAssets(
28
+ "tailwind",
29
+ assetMap,
30
+ rootDirectory,
31
+ (content, filepath) => filepath === "tailwind.config.js" ? content.replace("{src-dir}", relativeAppDirectory) : content
32
+ ),
33
+ getCodeFormatOptions(rootDirectory).then(
34
+ (formatConfig) => Promise.all([
35
+ replaceRemixConfig(rootDirectory, formatConfig, {
36
+ tailwind: true,
37
+ postcss: true
38
+ }),
39
+ replaceRootLinks(appDirectory, formatConfig, {
40
+ name: "tailwindCss",
41
+ path: tailwindCssPath,
42
+ isDefault: true
43
+ })
44
+ ])
45
+ )
46
+ ]);
47
+ return {
48
+ workPromise,
49
+ generatedAssets: Object.values(assetMap),
50
+ helpUrl: "https://tailwindcss.com/docs/configuration"
51
+ };
52
+ }
53
+
54
+ export { setupTailwind };
@@ -0,0 +1,22 @@
1
+ import { mergePackageJson } from './assets.js';
2
+ import { getCodeFormatOptions } from '../../format-code.js';
3
+ import { injectCssBundlingLink } from './replacers.js';
4
+
5
+ async function setupVanillaExtract({
6
+ rootDirectory,
7
+ appDirectory
8
+ }) {
9
+ const workPromise = Promise.all([
10
+ mergePackageJson("vanilla-extract", rootDirectory),
11
+ getCodeFormatOptions(rootDirectory).then(
12
+ (formatConfig) => injectCssBundlingLink(appDirectory, formatConfig)
13
+ )
14
+ ]);
15
+ return {
16
+ workPromise,
17
+ generatedAssets: [],
18
+ helpUrl: "https://vanilla-extract.style/documentation/styling/"
19
+ };
20
+ }
21
+
22
+ export { setupVanillaExtract };
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getLocaleFromRequest } from './templates/domains.js';
3
+
4
+ describe("Setup i18n with domains", () => {
5
+ it("extracts the locale from the domain", () => {
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.jp"))
14
+ ).toMatchObject({
15
+ language: "JA",
16
+ country: "JP"
17
+ });
18
+ expect(
19
+ getLocaleFromRequest(new Request("https://www.example.es"))
20
+ ).toMatchObject({
21
+ language: "ES",
22
+ country: "ES"
23
+ });
24
+ });
25
+ });
@@ -0,0 +1,46 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { renderSelectPrompt } from '@shopify/cli-kit/node/ui';
3
+ import { fileExists, readFile } from '@shopify/cli-kit/node/fs';
4
+ import { getCodeFormatOptions } from '../../format-code.js';
5
+ import { replaceServerI18n, replaceRemixEnv } from './replacers.js';
6
+
7
+ const SETUP_I18N_STRATEGIES = [
8
+ "subfolders",
9
+ "domains",
10
+ "subdomains"
11
+ ];
12
+ const I18N_STRATEGY_NAME_MAP = {
13
+ subfolders: "Subfolders (example.com/fr-ca/...)",
14
+ subdomains: "Subdomains (de.example.com/...)",
15
+ domains: "Top-level domains (example.jp/...)"
16
+ };
17
+ const I18N_CHOICES = [...SETUP_I18N_STRATEGIES, "none"];
18
+ async function setupI18nStrategy(strategy, options) {
19
+ const isTs = options.serverEntryPoint?.endsWith(".ts") ?? false;
20
+ const templatePath = fileURLToPath(
21
+ new URL(`./templates/${strategy}${isTs ? ".ts" : ".js"}`, import.meta.url)
22
+ );
23
+ if (!await fileExists(templatePath)) {
24
+ throw new Error("Unknown strategy");
25
+ }
26
+ const template = await readFile(templatePath);
27
+ const formatConfig = await getCodeFormatOptions(options.rootDirectory);
28
+ await replaceServerI18n(options, formatConfig, template);
29
+ await replaceRemixEnv(options, formatConfig, template);
30
+ }
31
+ async function renderI18nPrompt(options) {
32
+ const i18nStrategies = Object.entries({
33
+ ...I18N_STRATEGY_NAME_MAP,
34
+ ...options?.extraChoices
35
+ });
36
+ return renderSelectPrompt({
37
+ message: "Select a URL structure to support multiple markets",
38
+ ...options,
39
+ choices: i18nStrategies.map(([value, label]) => ({
40
+ value,
41
+ label
42
+ }))
43
+ });
44
+ }
45
+
46
+ export { I18N_CHOICES, I18N_STRATEGY_NAME_MAP, SETUP_I18N_STRATEGIES, renderI18nPrompt, setupI18nStrategy };
@@ -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 };