@shopify/cli-hydrogen 7.1.1 → 8.0.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 (138) hide show
  1. package/dist/commands/hydrogen/build-vite.js +19 -10
  2. package/dist/commands/hydrogen/build.js +10 -2
  3. package/dist/commands/hydrogen/check.js +1 -0
  4. package/dist/commands/hydrogen/codegen.js +1 -0
  5. package/dist/commands/hydrogen/customer-account/push.js +170 -0
  6. package/dist/commands/hydrogen/debug/cpu.js +3 -0
  7. package/dist/commands/hydrogen/deploy.js +121 -36
  8. package/dist/commands/hydrogen/dev-vite.js +128 -59
  9. package/dist/commands/hydrogen/dev.js +108 -51
  10. package/dist/commands/hydrogen/env/list.js +7 -8
  11. package/dist/commands/hydrogen/env/pull.js +17 -1
  12. package/dist/commands/hydrogen/env/{push__unstable.js → push.js} +23 -50
  13. package/dist/commands/hydrogen/generate/route.js +1 -0
  14. package/dist/commands/hydrogen/init.js +45 -17
  15. package/dist/commands/hydrogen/link.js +20 -4
  16. package/dist/commands/hydrogen/list.js +1 -0
  17. package/dist/commands/hydrogen/login.js +1 -0
  18. package/dist/commands/hydrogen/logout.js +1 -0
  19. package/dist/commands/hydrogen/preview.js +31 -16
  20. package/dist/commands/hydrogen/setup/css.js +8 -1
  21. package/dist/commands/hydrogen/setup/markets.js +1 -0
  22. package/dist/commands/hydrogen/setup/vite.js +244 -138
  23. package/dist/commands/hydrogen/setup.js +21 -22
  24. package/dist/commands/hydrogen/shortcut.js +10 -0
  25. package/dist/commands/hydrogen/unlink.js +1 -0
  26. package/dist/commands/hydrogen/upgrade.js +2 -1
  27. package/dist/generator-templates/assets/vite/package.json +2 -3
  28. package/dist/generator-templates/assets/vite/vite.config.js +10 -2
  29. package/dist/generator-templates/starter/CHANGELOG.md +104 -0
  30. package/dist/generator-templates/starter/README.md +3 -44
  31. package/dist/generator-templates/starter/app/components/Search.tsx +12 -7
  32. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +1 -0
  33. package/dist/generator-templates/starter/app/lib/fragments.ts +2 -0
  34. package/dist/generator-templates/starter/app/root.tsx +2 -5
  35. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +1 -1
  36. package/dist/generator-templates/starter/app/routes/account.tsx +1 -1
  37. package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +8 -15
  38. package/dist/generator-templates/starter/app/routes/collections.all.tsx +160 -0
  39. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +1 -2
  40. package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +6 -3
  41. package/dist/generator-templates/starter/{remix.env.d.ts → env.d.ts} +8 -2
  42. package/dist/generator-templates/starter/package.json +14 -9
  43. package/dist/generator-templates/starter/server.ts +2 -1
  44. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +59 -3
  45. package/dist/generator-templates/starter/vite.config.ts +21 -0
  46. package/dist/{commands/hydrogen/init.d.ts → init.d.ts} +11 -3
  47. package/dist/lib/check-lockfile.js +12 -18
  48. package/dist/lib/codegen.js +37 -13
  49. package/dist/lib/common.js +50 -0
  50. package/dist/lib/cpu-profiler.js +4 -1
  51. package/dist/lib/dev-shared.js +97 -0
  52. package/dist/lib/environment-variables.js +51 -30
  53. package/dist/lib/file.js +8 -1
  54. package/dist/lib/flags.js +37 -16
  55. package/dist/lib/graphql/admin/customer-application-update.js +29 -0
  56. package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
  57. package/dist/lib/graphql/admin/list-environments.js +1 -0
  58. package/dist/lib/graphql/admin/pull-variables.js +4 -4
  59. package/dist/lib/graphql/admin/test-helper.js +37 -0
  60. package/dist/lib/log.js +86 -13
  61. package/dist/lib/mini-oxygen/common.js +19 -33
  62. package/dist/lib/mini-oxygen/index.js +6 -2
  63. package/dist/lib/mini-oxygen/node.js +43 -31
  64. package/dist/lib/mini-oxygen/workerd.js +72 -165
  65. package/dist/lib/missing-routes.js +1 -1
  66. package/dist/lib/onboarding/common.js +82 -70
  67. package/dist/lib/onboarding/local.js +19 -9
  68. package/dist/lib/onboarding/remote.js +35 -30
  69. package/dist/lib/package-managers.js +24 -0
  70. package/dist/lib/remix-config.js +17 -1
  71. package/dist/lib/request-events.js +6 -1
  72. package/dist/lib/setups/i18n/replacers.js +9 -6
  73. package/dist/lib/setups/routes/generate.js +1 -0
  74. package/dist/lib/shell.js +2 -1
  75. package/dist/lib/shopify-config.js +19 -1
  76. package/dist/lib/template-diff.js +36 -15
  77. package/dist/lib/template-downloader.js +35 -5
  78. package/dist/lib/transpile/morph/typedefs.js +5 -2
  79. package/dist/lib/transpile/project.js +8 -4
  80. package/dist/lib/tunneling.js +44 -0
  81. package/dist/lib/virtual-routes.js +1 -1
  82. package/dist/lib/vite-config.js +39 -9
  83. package/oclif.manifest.json +711 -498
  84. package/package.json +30 -22
  85. package/dist/commands/hydrogen/deploy.test.js +0 -553
  86. package/dist/commands/hydrogen/env/list.test.js +0 -148
  87. package/dist/commands/hydrogen/env/pull.test.js +0 -207
  88. package/dist/commands/hydrogen/env/push__unstable.test.js +0 -383
  89. package/dist/commands/hydrogen/generate/route.test.js +0 -43
  90. package/dist/commands/hydrogen/init.test.js +0 -641
  91. package/dist/commands/hydrogen/link.test.js +0 -187
  92. package/dist/commands/hydrogen/list.test.js +0 -111
  93. package/dist/commands/hydrogen/setup.test.js +0 -61
  94. package/dist/commands/hydrogen/shortcut.test.js +0 -30
  95. package/dist/commands/hydrogen/unlink.test.js +0 -36
  96. package/dist/commands/hydrogen/upgrade.test.js +0 -786
  97. package/dist/generator-templates/starter/remix.config.js +0 -24
  98. package/dist/lib/auth.test.js +0 -157
  99. package/dist/lib/check-lockfile.test.js +0 -81
  100. package/dist/lib/check-version.test.js +0 -86
  101. package/dist/lib/environment-variables.test.js +0 -149
  102. package/dist/lib/file.test.js +0 -68
  103. package/dist/lib/flags.test.js +0 -43
  104. package/dist/lib/get-oxygen-deployment-data.test.js +0 -120
  105. package/dist/lib/gid.test.js +0 -15
  106. package/dist/lib/graphql/admin/client.test.js +0 -76
  107. package/dist/lib/graphql/admin/create-storefront.test.js +0 -64
  108. package/dist/lib/graphql/admin/link-storefront.test.js +0 -38
  109. package/dist/lib/graphql/admin/list-environments.test.js +0 -44
  110. package/dist/lib/graphql/admin/list-storefronts.test.js +0 -44
  111. package/dist/lib/graphql/admin/pull-variables.test.js +0 -43
  112. package/dist/lib/graphql/business-platform/user-account.test.js +0 -80
  113. package/dist/lib/log.test.js +0 -92
  114. package/dist/lib/mini-oxygen/assets.js +0 -134
  115. package/dist/lib/mini-oxygen/mini-oxygen.test.js +0 -214
  116. package/dist/lib/mini-oxygen/workerd-inspector-logs.js +0 -227
  117. package/dist/lib/mini-oxygen/workerd-inspector-proxy.js +0 -200
  118. package/dist/lib/mini-oxygen/workerd-inspector.js +0 -219
  119. package/dist/lib/missing-routes.test.js +0 -45
  120. package/dist/lib/remix-version-check.test.js +0 -39
  121. package/dist/lib/remix-version-interop.test.js +0 -13
  122. package/dist/lib/setups/i18n/domains.test.js +0 -39
  123. package/dist/lib/setups/i18n/replacers.test.js +0 -261
  124. package/dist/lib/setups/i18n/subdomains.test.js +0 -39
  125. package/dist/lib/setups/i18n/subfolders.test.js +0 -39
  126. package/dist/lib/setups/routes/generate.test.js +0 -296
  127. package/dist/lib/shell.test.js +0 -111
  128. package/dist/lib/shopify-config.test.js +0 -199
  129. package/dist/lib/string.test.js +0 -16
  130. package/dist/lib/virtual-routes.test.js +0 -49
  131. package/dist/lib/vite/hydrogen-middleware.js +0 -82
  132. package/dist/lib/vite/mini-oxygen.js +0 -152
  133. package/dist/lib/vite/plugins.d.ts +0 -27
  134. package/dist/lib/vite/plugins.js +0 -139
  135. package/dist/lib/vite/shared.js +0 -10
  136. package/dist/lib/vite/utils.js +0 -55
  137. package/dist/lib/vite/worker-entry.js +0 -1518
  138. /package/dist/generator-templates/starter/{.eslintrc.js → .eslintrc.cjs} +0 -0
@@ -1,261 +0,0 @@
1
- import { fileURLToPath } from 'node:url';
2
- import { describe, it, expect } from 'vitest';
3
- import { inTemporaryDirectory, copyFile, readFile, writeFile } from '@shopify/cli-kit/node/fs';
4
- import { joinPath } from '@shopify/cli-kit/node/path';
5
- import { ts } from 'ts-morph';
6
- import { getSkeletonSourceDir } from '../../build.js';
7
- import { replaceRemixEnv, replaceServerI18n } from './replacers.js';
8
- import { DEFAULT_COMPILER_OPTIONS } from '../../transpile/morph/index.js';
9
-
10
- const remixDts = "remix.env.d.ts";
11
- const serverTs = "server.ts";
12
- const checkTypes = (content) => {
13
- const { diagnostics } = ts.transpileModule(content, {
14
- reportDiagnostics: true,
15
- compilerOptions: DEFAULT_COMPILER_OPTIONS
16
- });
17
- if (diagnostics && diagnostics.length > 0) {
18
- throw new Error(
19
- ts.formatDiagnostics(
20
- diagnostics,
21
- ts.createCompilerHost(DEFAULT_COMPILER_OPTIONS)
22
- )
23
- );
24
- }
25
- };
26
- describe("i18n replacers", () => {
27
- it("adds i18n type to remix.env.d.ts", async () => {
28
- await inTemporaryDirectory(async (tmpDir) => {
29
- const skeletonDir = getSkeletonSourceDir();
30
- await copyFile(
31
- joinPath(skeletonDir, remixDts),
32
- joinPath(tmpDir, remixDts)
33
- );
34
- await replaceRemixEnv(
35
- { rootDirectory: tmpDir },
36
- {},
37
- await readFile(
38
- fileURLToPath(new URL("./templates/domains.ts", import.meta.url))
39
- )
40
- );
41
- const newContent = await readFile(joinPath(tmpDir, remixDts));
42
- expect(() => checkTypes(newContent)).not.toThrow();
43
- expect(newContent).toMatchInlineSnapshot(`
44
- "/// <reference types="@remix-run/dev" />
45
- /// <reference types="@shopify/remix-oxygen" />
46
- /// <reference types="@shopify/oxygen-workers-types" />
47
-
48
- // Enhance TypeScript's built-in typings.
49
- import "@total-typescript/ts-reset";
50
-
51
- import type {
52
- Storefront,
53
- CustomerAccount,
54
- HydrogenCart,
55
- } from "@shopify/hydrogen";
56
- import type {
57
- LanguageCode,
58
- CountryCode,
59
- } from "@shopify/hydrogen/storefront-api-types";
60
- import type { AppSession } from "~/lib/session";
61
-
62
- declare global {
63
- /**
64
- * A global \`process\` object is only available during build to access NODE_ENV.
65
- */
66
- const process: { env: { NODE_ENV: "production" | "development" } };
67
-
68
- /**
69
- * Declare expected Env parameter in fetch handler.
70
- */
71
- interface Env {
72
- SESSION_SECRET: string;
73
- PUBLIC_STOREFRONT_API_TOKEN: string;
74
- PRIVATE_STOREFRONT_API_TOKEN: string;
75
- PUBLIC_STORE_DOMAIN: string;
76
- PUBLIC_STOREFRONT_ID: string;
77
- PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: string;
78
- PUBLIC_CUSTOMER_ACCOUNT_API_URL: string;
79
- }
80
-
81
- /**
82
- * The I18nLocale used for Storefront API query context.
83
- */
84
- type I18nLocale = { language: LanguageCode; country: CountryCode };
85
- }
86
-
87
- declare module "@shopify/remix-oxygen" {
88
- /**
89
- * Declare local additions to the Remix loader context.
90
- */
91
- export interface AppLoadContext {
92
- env: Env;
93
- cart: HydrogenCart;
94
- storefront: Storefront<I18nLocale>;
95
- customerAccount: CustomerAccount;
96
- session: AppSession;
97
- waitUntil: ExecutionContext["waitUntil"];
98
- }
99
- }
100
- "
101
- `);
102
- });
103
- });
104
- it("adds i18n type to server.ts", async () => {
105
- await inTemporaryDirectory(async (tmpDir) => {
106
- const skeletonDir = getSkeletonSourceDir();
107
- await writeFile(
108
- joinPath(tmpDir, serverTs),
109
- // Remove the part that is not needed for this test (AppSession, Cart query, etc);
110
- (await readFile(joinPath(skeletonDir, serverTs))).replace(/^};$.*/ms, "};")
111
- );
112
- await replaceServerI18n(
113
- { rootDirectory: tmpDir, serverEntryPoint: serverTs },
114
- {},
115
- await readFile(
116
- fileURLToPath(new URL("./templates/domains.ts", import.meta.url))
117
- ),
118
- false
119
- );
120
- const newContent = await readFile(joinPath(tmpDir, serverTs));
121
- expect(() => checkTypes(newContent)).not.toThrow();
122
- expect(newContent).toMatchInlineSnapshot(`
123
- "// Virtual entry point for the app
124
- import * as remixBuild from "@remix-run/dev/server-build";
125
- import {
126
- cartGetIdDefault,
127
- cartSetIdDefault,
128
- createCartHandler,
129
- createStorefrontClient,
130
- storefrontRedirect,
131
- createCustomerAccountClient,
132
- } from "@shopify/hydrogen";
133
- import {
134
- createRequestHandler,
135
- getStorefrontHeaders,
136
- type AppLoadContext,
137
- } from "@shopify/remix-oxygen";
138
- import { AppSession } from "~/lib/session";
139
- import { CART_QUERY_FRAGMENT } from "~/lib/fragments";
140
-
141
- /**
142
- * Export a fetch handler in module format.
143
- */
144
- export default {
145
- async fetch(
146
- request: Request,
147
- env: Env,
148
- executionContext: ExecutionContext
149
- ): Promise<Response> {
150
- try {
151
- /**
152
- * Open a cache instance in the worker and a custom session instance.
153
- */
154
- if (!env?.SESSION_SECRET) {
155
- throw new Error("SESSION_SECRET environment variable is not set");
156
- }
157
-
158
- const waitUntil = executionContext.waitUntil.bind(executionContext);
159
- const [cache, session] = await Promise.all([
160
- caches.open("hydrogen"),
161
- AppSession.init(request, [env.SESSION_SECRET]),
162
- ]);
163
-
164
- /**
165
- * Create Hydrogen's Storefront client.
166
- */
167
- const { storefront } = createStorefrontClient({
168
- cache,
169
- waitUntil,
170
- i18n: getLocaleFromRequest(request),
171
- publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
172
- privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
173
- storeDomain: env.PUBLIC_STORE_DOMAIN,
174
- storefrontId: env.PUBLIC_STOREFRONT_ID,
175
- storefrontHeaders: getStorefrontHeaders(request),
176
- });
177
-
178
- /**
179
- * Create a client for Customer Account API.
180
- */
181
- const customerAccount = createCustomerAccountClient({
182
- waitUntil,
183
- request,
184
- session,
185
- customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID,
186
- customerAccountUrl: env.PUBLIC_CUSTOMER_ACCOUNT_API_URL,
187
- });
188
-
189
- /*
190
- * Create a cart handler that will be used to
191
- * create and update the cart in the session.
192
- */
193
- const cart = createCartHandler({
194
- storefront,
195
- customerAccount,
196
- getCartId: cartGetIdDefault(request.headers),
197
- setCartId: cartSetIdDefault(),
198
- cartQueryFragment: CART_QUERY_FRAGMENT,
199
- });
200
-
201
- /**
202
- * Create a Remix request handler and pass
203
- * Hydrogen's Storefront client to the loader context.
204
- */
205
- const handleRequest = createRequestHandler({
206
- build: remixBuild,
207
- mode: process.env.NODE_ENV,
208
- getLoadContext: (): AppLoadContext => ({
209
- session,
210
- storefront,
211
- customerAccount,
212
- cart,
213
- env,
214
- waitUntil,
215
- }),
216
- });
217
-
218
- const response = await handleRequest(request);
219
-
220
- if (response.status === 404) {
221
- /**
222
- * Check for redirects only when there's a 404 from the app.
223
- * If the redirect doesn't exist, then \`storefrontRedirect\`
224
- * will pass through the 404 response.
225
- */
226
- return storefrontRedirect({ request, response, storefront });
227
- }
228
-
229
- return response;
230
- } catch (error) {
231
- // eslint-disable-next-line no-console
232
- console.error(error);
233
- return new Response("An unexpected error occurred", { status: 500 });
234
- }
235
- },
236
- };
237
-
238
- function getLocaleFromRequest(request: Request): I18nLocale {
239
- const defaultLocale: I18nLocale = { language: "EN", country: "US" };
240
- const supportedLocales = {
241
- ES: "ES",
242
- FR: "FR",
243
- DE: "DE",
244
- JP: "JA",
245
- } as Record<I18nLocale["country"], I18nLocale["language"]>;
246
-
247
- const url = new URL(request.url);
248
- const domain = url.hostname
249
- .split(".")
250
- .pop()
251
- ?.toUpperCase() as keyof typeof supportedLocales;
252
-
253
- return domain && supportedLocales[domain]
254
- ? { language: supportedLocales[domain], country: domain }
255
- : defaultLocale;
256
- }
257
- "
258
- `);
259
- });
260
- });
261
- });
@@ -1,39 +0,0 @@
1
- import { fileURLToPath } from 'node:url';
2
- import { describe, it, expect } from 'vitest';
3
- import { getLocaleFromRequest } from './templates/subdomains.js';
4
- import { readFile } from '@shopify/cli-kit/node/fs';
5
-
6
- describe("Setup i18n with subdomains", () => {
7
- it("extracts the locale from the subdomain", () => {
8
- expect(
9
- getLocaleFromRequest(new Request("https://example.com"))
10
- ).toMatchObject({
11
- language: "EN",
12
- country: "US"
13
- });
14
- expect(
15
- getLocaleFromRequest(new Request("https://jp.example.com"))
16
- ).toMatchObject({
17
- language: "JA",
18
- country: "JP"
19
- });
20
- expect(
21
- getLocaleFromRequest(new Request("https://es.sub.example.com"))
22
- ).toMatchObject({
23
- language: "ES",
24
- country: "ES"
25
- });
26
- });
27
- it("does not access imported types directly", async () => {
28
- const template = await readFile(
29
- fileURLToPath(new URL("./templates/domains.ts", import.meta.url))
30
- );
31
- const typeImports = (template.match(/import\s+type\s+{([^}]+)}/)?.[1] || "").trim().split(/\s*,\s*/);
32
- expect(typeImports).not.toHaveLength(0);
33
- const fnCode = template.match(/function .*\n}$/ms)?.[0] || "";
34
- expect(fnCode).toBeTruthy();
35
- typeImports.forEach(
36
- (typeImport) => expect(fnCode).not.toContain(typeImport)
37
- );
38
- });
39
- });
@@ -1,39 +0,0 @@
1
- import { fileURLToPath } from 'node:url';
2
- import { describe, it, expect } from 'vitest';
3
- import { getLocaleFromRequest } from './templates/subfolders.js';
4
- import { readFile } from '@shopify/cli-kit/node/fs';
5
-
6
- describe("Setup i18n with subfolders", () => {
7
- it("extracts the locale from the pathname", () => {
8
- expect(
9
- getLocaleFromRequest(new Request("https://example.com"))
10
- ).toMatchObject({
11
- language: "EN",
12
- country: "US"
13
- });
14
- expect(
15
- getLocaleFromRequest(new Request("https://example.com/ja-jp"))
16
- ).toMatchObject({
17
- language: "JA",
18
- country: "JP"
19
- });
20
- expect(
21
- getLocaleFromRequest(new Request("https://example.com/es-es/path"))
22
- ).toMatchObject({
23
- language: "ES",
24
- country: "ES"
25
- });
26
- });
27
- it("does not access imported types directly", async () => {
28
- const template = await readFile(
29
- fileURLToPath(new URL("./templates/domains.ts", import.meta.url))
30
- );
31
- const typeImports = (template.match(/import\s+type\s+{([^}]+)}/)?.[1] || "").trim().split(/\s*,\s*/);
32
- expect(typeImports).not.toHaveLength(0);
33
- const fnCode = template.match(/function .*\n}$/ms)?.[0] || "";
34
- expect(fnCode).toBeTruthy();
35
- typeImports.forEach(
36
- (typeImport) => expect(fnCode).not.toContain(typeImport)
37
- );
38
- });
39
- });
@@ -1,296 +0,0 @@
1
- import { describe, beforeEach, vi, it, expect } from 'vitest';
2
- import { getResolvedRoutes, generateRoutes, generateProjectFile } from './generate.js';
3
- import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
4
- import { inTemporaryDirectory, fileExists, readFile, mkdir, writeFile } from '@shopify/cli-kit/node/fs';
5
- import { joinPath, dirname } from '@shopify/cli-kit/node/path';
6
- import { getTemplateAppFile } from '../../../lib/build.js';
7
- import { getRemixConfig } from '../../remix-config.js';
8
-
9
- const readProjectFile = (dirs, fileBasename, ext = "tsx") => readFile(joinPath(dirs.appDirectory, `${fileBasename}.${ext}`));
10
- describe("generate/route", () => {
11
- beforeEach(() => {
12
- vi.resetAllMocks();
13
- vi.mock("@shopify/cli-kit/node/output");
14
- vi.mock("@shopify/cli-kit/node/ui");
15
- vi.mock("../../remix-config.js", async () => ({ getRemixConfig: vi.fn() }));
16
- });
17
- describe("generateRoutes", () => {
18
- it("generates all routes with correct configuration", async () => {
19
- const { resolvedRouteFiles } = await getResolvedRoutes();
20
- expect(
21
- resolvedRouteFiles.find((item) => /account_?\.login/.test(item))
22
- ).toBeTruthy();
23
- await inTemporaryDirectory(async (tmpDir) => {
24
- const directories = await createHydrogenFixture(tmpDir, {
25
- files: [
26
- ["jsconfig.json", JSON.stringify({ compilerOptions: { test: "js" } })],
27
- [".prettierrc.json", JSON.stringify({ singleQuote: false })]
28
- ],
29
- templates: resolvedRouteFiles.map(
30
- (filepath) => ["routes/" + filepath + ".tsx", ""]
31
- )
32
- });
33
- vi.mocked(getRemixConfig).mockResolvedValue(directories);
34
- const result = await generateRoutes({
35
- routeName: "all",
36
- directory: directories.rootDirectory,
37
- templatesRoot: directories.templatesRoot
38
- });
39
- expect(result).toMatchObject(
40
- expect.objectContaining({
41
- isTypescript: false,
42
- formatOptions: { singleQuote: false },
43
- routes: expect.any(Array)
44
- })
45
- );
46
- expect(result.routes).toHaveLength(
47
- Object.values(resolvedRouteFiles).length
48
- );
49
- });
50
- });
51
- it("figures out the locale if a home route already exists", async () => {
52
- await inTemporaryDirectory(async (tmpDir) => {
53
- const route = "routes/pages.$handle";
54
- const directories = await createHydrogenFixture(tmpDir, {
55
- files: [
56
- ["tsconfig.json", JSON.stringify({ compilerOptions: { test: "ts" } })],
57
- ["app/routes/($locale)._index.tsx", "export const test = true;"]
58
- ],
59
- templates: [[route + ".tsx", `const str = "hello world"`]]
60
- });
61
- vi.mocked(getRemixConfig).mockResolvedValue({
62
- ...directories,
63
- tsconfigPath: "somewhere/tsconfig.json"
64
- });
65
- const result = await generateRoutes({
66
- routeName: ["page"],
67
- directory: directories.rootDirectory,
68
- templatesRoot: directories.templatesRoot
69
- });
70
- expect(result).toMatchObject(
71
- expect.objectContaining({
72
- isTypescript: true,
73
- routes: expect.any(Array),
74
- formatOptions: expect.any(Object)
75
- })
76
- );
77
- expect(result.routes).toHaveLength(1);
78
- expect(result.routes[0]).toMatchObject({
79
- destinationRoute: expect.stringContaining("($locale).pages.$handle")
80
- });
81
- });
82
- });
83
- });
84
- describe("generateProjectFile", () => {
85
- it("generates a route file for Remix v1", async () => {
86
- await inTemporaryDirectory(async (tmpDir) => {
87
- const route = "routes/pages.$handle";
88
- const directories = await createHydrogenFixture(tmpDir, {
89
- files: [],
90
- templates: [[route + ".tsx", `const str = "hello world"`]]
91
- });
92
- await generateProjectFile(route, {
93
- ...directories,
94
- v1RouteConvention: true
95
- });
96
- expect(
97
- await readProjectFile(directories, route.replace(".", "/"), "jsx")
98
- ).toContain(`const str = 'hello world'`);
99
- });
100
- });
101
- it("generates a route file for Remix v2", async () => {
102
- await inTemporaryDirectory(async (tmpDir) => {
103
- const route = "routes/custom.path.$handle._index";
104
- const directories = await createHydrogenFixture(tmpDir, {
105
- files: [],
106
- templates: [[route + ".tsx", `const str = "hello world"`]]
107
- });
108
- await generateProjectFile(route, directories);
109
- expect(await readProjectFile(directories, route, "jsx")).toContain(
110
- `const str = 'hello world'`
111
- );
112
- });
113
- });
114
- it("generates route files with locale prefix", async () => {
115
- await inTemporaryDirectory(async (tmpDir) => {
116
- const routeCode = `const str = 'hello world'`;
117
- const directories = await createHydrogenFixture(tmpDir, {
118
- files: [],
119
- templates: [
120
- ["routes/_index.tsx", routeCode],
121
- ["routes/pages.$handle.tsx", routeCode],
122
- ["routes/[robots.txt].tsx", routeCode],
123
- ["routes/[sitemap.xml].tsx", routeCode]
124
- ]
125
- });
126
- const localePrefix = "locale";
127
- await generateProjectFile("routes/_index", {
128
- ...directories,
129
- localePrefix,
130
- typescript: true
131
- });
132
- await generateProjectFile("routes/pages.$handle", {
133
- ...directories,
134
- v1RouteConvention: true,
135
- localePrefix,
136
- typescript: true
137
- });
138
- await generateProjectFile("routes/[sitemap.xml]", {
139
- ...directories,
140
- localePrefix,
141
- typescript: true
142
- });
143
- await generateProjectFile("routes/[robots.txt]", {
144
- ...directories,
145
- localePrefix,
146
- typescript: true
147
- });
148
- await expect(
149
- readProjectFile(directories, `routes/($locale)._index`)
150
- ).resolves.toContain(routeCode);
151
- await expect(
152
- readProjectFile(directories, `routes/($locale).[sitemap.xml]`)
153
- ).resolves.toContain(routeCode);
154
- await expect(
155
- readProjectFile(directories, `routes/[robots.txt]`)
156
- ).resolves.toContain(routeCode);
157
- await expect(
158
- readProjectFile(directories, `routes/($locale)/pages/$handle`)
159
- ).resolves.toContain(routeCode);
160
- });
161
- });
162
- it("produces a typescript file when typescript argument is true", async () => {
163
- await inTemporaryDirectory(async (tmpDir) => {
164
- const route = "routes/pages.$handle";
165
- const directories = await createHydrogenFixture(tmpDir, {
166
- files: [],
167
- templates: [[route + ".tsx", 'const str = "hello typescript"']]
168
- });
169
- await generateProjectFile(route, {
170
- ...directories,
171
- typescript: true
172
- });
173
- expect(await readProjectFile(directories, route)).toContain(
174
- `const str = 'hello typescript'`
175
- );
176
- });
177
- });
178
- it("prompts the user if there the file already exists", async () => {
179
- await inTemporaryDirectory(async (tmpDir) => {
180
- vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
181
- async () => true
182
- );
183
- const route = "routes/page.$handle";
184
- const directories = await createHydrogenFixture(tmpDir, {
185
- files: [[`app/${route}.jsx`, 'const str = "I exist"']],
186
- templates: [[route + ".tsx", 'const str = "hello world"']]
187
- });
188
- await generateProjectFile(route, {
189
- ...directories
190
- });
191
- expect(renderConfirmationPrompt).toHaveBeenCalledWith(
192
- expect.objectContaining({
193
- message: expect.stringContaining("already exists")
194
- })
195
- );
196
- });
197
- });
198
- it("does not prompt the user if the force property is true", async () => {
199
- await inTemporaryDirectory(async (tmpDir) => {
200
- vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
201
- async () => true
202
- );
203
- const route = "routes/page.$pageHandle";
204
- const directories = await createHydrogenFixture(tmpDir, {
205
- files: [[`app/${route}.jsx`, 'const str = "I exist"']],
206
- templates: [[route + ".tsx", 'const str = "hello world"']]
207
- });
208
- await generateProjectFile(route, {
209
- ...directories,
210
- force: true
211
- });
212
- expect(renderConfirmationPrompt).not.toHaveBeenCalled();
213
- });
214
- });
215
- it("generates all the route dependencies", async () => {
216
- await inTemporaryDirectory(async (tmpDir) => {
217
- const templates = [
218
- [
219
- "routes/pages.$pageHandle.tsx",
220
- `import Dep from 'some-node-dep';
221
- import AnotherRoute from './AnotherRoute';
222
- import Form from '~/components/Form';
223
- import {
224
-
225
-
226
- Button} from '../components/Button';
227
- import {stuff} from '../utils';
228
- import {serverOnly} from '../something.server';
229
- import styles from '../styles/app.css';
230
- export {Dep, AnotherRoute, Form, Button, stuff, serverOnly, styles};
231
- `
232
- ],
233
- [
234
- "components/Form.tsx",
235
- `import {Button} from './Button';
236
- import {Text} from './Text';
237
- export {Button, Text};
238
- `
239
- ],
240
- ["components/Button.tsx", `export const Button = '';
241
- `],
242
- ["components/Text.tsx", `export const Text = '';
243
- `],
244
- ["utils/index.ts", `export {stuff} from './stuff';
245
- `],
246
- ["utils/stuff.ts", `export const stuff = '';
247
- `],
248
- ["something.server.ts", `export const serverOnly = '';
249
- `],
250
- ["styles/app.css", `.red{color:red;}`]
251
- ];
252
- const directories = await createHydrogenFixture(tmpDir, { templates });
253
- vi.mocked(getRemixConfig).mockResolvedValue(directories);
254
- await generateProjectFile("routes/pages.$pageHandle", {
255
- ...directories,
256
- force: true
257
- });
258
- await Promise.all(
259
- templates.map(async ([file, content]) => {
260
- const actualFile = joinPath(
261
- directories.appDirectory,
262
- file.replace(".ts", ".js")
263
- );
264
- await expect(fileExists(actualFile)).resolves.toBeTruthy();
265
- await expect(readFile(actualFile)).resolves.toEqual(
266
- content.replace(/\{\n+/, "{")
267
- );
268
- })
269
- );
270
- });
271
- });
272
- });
273
- });
274
- async function createHydrogenFixture(directory, {
275
- files = [],
276
- templates = []
277
- }) {
278
- const projectDir = "project";
279
- for (const item of files) {
280
- const [filePath, fileContent] = item;
281
- const fullFilePath = joinPath(directory, projectDir, filePath);
282
- await mkdir(dirname(fullFilePath));
283
- await writeFile(fullFilePath, fileContent);
284
- }
285
- for (const item of templates) {
286
- const [filePath, fileContent] = item;
287
- const fullFilePath = getTemplateAppFile(filePath, directory);
288
- await mkdir(dirname(fullFilePath));
289
- await writeFile(fullFilePath, fileContent);
290
- }
291
- return {
292
- rootDirectory: joinPath(directory, projectDir),
293
- appDirectory: joinPath(directory, projectDir, "app"),
294
- templatesRoot: directory
295
- };
296
- }