@shopify/cli-hydrogen 7.1.2 → 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 (136) 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 +3 -4
  28. package/dist/generator-templates/assets/vite/vite.config.js +10 -2
  29. package/dist/generator-templates/starter/CHANGELOG.md +89 -0
  30. package/dist/generator-templates/starter/README.md +3 -44
  31. package/dist/generator-templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +1 -0
  32. package/dist/generator-templates/starter/app/lib/fragments.ts +2 -0
  33. package/dist/generator-templates/starter/app/root.tsx +2 -5
  34. package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +1 -1
  35. package/dist/generator-templates/starter/app/routes/account.tsx +1 -1
  36. package/dist/generator-templates/starter/app/routes/collections.all.tsx +160 -0
  37. package/dist/generator-templates/starter/app/routes/products.$handle.tsx +1 -2
  38. package/dist/generator-templates/starter/customer-accountapi.generated.d.ts +6 -3
  39. package/dist/generator-templates/starter/{remix.env.d.ts → env.d.ts} +8 -2
  40. package/dist/generator-templates/starter/package.json +14 -9
  41. package/dist/generator-templates/starter/server.ts +2 -1
  42. package/dist/generator-templates/starter/storefrontapi.generated.d.ts +59 -3
  43. package/dist/generator-templates/starter/vite.config.ts +21 -0
  44. package/dist/{commands/hydrogen/init.d.ts → init.d.ts} +11 -3
  45. package/dist/lib/check-lockfile.js +12 -18
  46. package/dist/lib/codegen.js +37 -13
  47. package/dist/lib/common.js +50 -0
  48. package/dist/lib/cpu-profiler.js +4 -1
  49. package/dist/lib/dev-shared.js +97 -0
  50. package/dist/lib/environment-variables.js +51 -30
  51. package/dist/lib/file.js +8 -1
  52. package/dist/lib/flags.js +37 -16
  53. package/dist/lib/graphql/admin/customer-application-update.js +29 -0
  54. package/dist/lib/graphql/admin/get-oxygen-data.js +1 -0
  55. package/dist/lib/graphql/admin/list-environments.js +1 -0
  56. package/dist/lib/graphql/admin/pull-variables.js +4 -4
  57. package/dist/lib/graphql/admin/test-helper.js +37 -0
  58. package/dist/lib/log.js +86 -13
  59. package/dist/lib/mini-oxygen/common.js +19 -33
  60. package/dist/lib/mini-oxygen/index.js +6 -2
  61. package/dist/lib/mini-oxygen/node.js +43 -31
  62. package/dist/lib/mini-oxygen/workerd.js +72 -165
  63. package/dist/lib/missing-routes.js +1 -1
  64. package/dist/lib/onboarding/common.js +82 -70
  65. package/dist/lib/onboarding/local.js +19 -9
  66. package/dist/lib/onboarding/remote.js +35 -30
  67. package/dist/lib/package-managers.js +24 -0
  68. package/dist/lib/remix-config.js +17 -1
  69. package/dist/lib/request-events.js +6 -1
  70. package/dist/lib/setups/i18n/replacers.js +9 -6
  71. package/dist/lib/setups/routes/generate.js +1 -0
  72. package/dist/lib/shell.js +2 -1
  73. package/dist/lib/shopify-config.js +19 -1
  74. package/dist/lib/template-diff.js +36 -15
  75. package/dist/lib/template-downloader.js +35 -5
  76. package/dist/lib/transpile/morph/typedefs.js +5 -2
  77. package/dist/lib/transpile/project.js +8 -4
  78. package/dist/lib/tunneling.js +44 -0
  79. package/dist/lib/virtual-routes.js +1 -1
  80. package/dist/lib/vite-config.js +39 -9
  81. package/oclif.manifest.json +711 -498
  82. package/package.json +32 -24
  83. package/dist/commands/hydrogen/deploy.test.js +0 -553
  84. package/dist/commands/hydrogen/env/list.test.js +0 -148
  85. package/dist/commands/hydrogen/env/pull.test.js +0 -207
  86. package/dist/commands/hydrogen/env/push__unstable.test.js +0 -383
  87. package/dist/commands/hydrogen/generate/route.test.js +0 -43
  88. package/dist/commands/hydrogen/init.test.js +0 -641
  89. package/dist/commands/hydrogen/link.test.js +0 -187
  90. package/dist/commands/hydrogen/list.test.js +0 -111
  91. package/dist/commands/hydrogen/setup.test.js +0 -61
  92. package/dist/commands/hydrogen/shortcut.test.js +0 -30
  93. package/dist/commands/hydrogen/unlink.test.js +0 -36
  94. package/dist/commands/hydrogen/upgrade.test.js +0 -786
  95. package/dist/generator-templates/starter/remix.config.js +0 -24
  96. package/dist/lib/auth.test.js +0 -157
  97. package/dist/lib/check-lockfile.test.js +0 -81
  98. package/dist/lib/check-version.test.js +0 -86
  99. package/dist/lib/environment-variables.test.js +0 -149
  100. package/dist/lib/file.test.js +0 -68
  101. package/dist/lib/flags.test.js +0 -43
  102. package/dist/lib/get-oxygen-deployment-data.test.js +0 -120
  103. package/dist/lib/gid.test.js +0 -15
  104. package/dist/lib/graphql/admin/client.test.js +0 -76
  105. package/dist/lib/graphql/admin/create-storefront.test.js +0 -64
  106. package/dist/lib/graphql/admin/link-storefront.test.js +0 -38
  107. package/dist/lib/graphql/admin/list-environments.test.js +0 -44
  108. package/dist/lib/graphql/admin/list-storefronts.test.js +0 -44
  109. package/dist/lib/graphql/admin/pull-variables.test.js +0 -43
  110. package/dist/lib/graphql/business-platform/user-account.test.js +0 -80
  111. package/dist/lib/log.test.js +0 -92
  112. package/dist/lib/mini-oxygen/assets.js +0 -134
  113. package/dist/lib/mini-oxygen/mini-oxygen.test.js +0 -214
  114. package/dist/lib/mini-oxygen/workerd-inspector-logs.js +0 -227
  115. package/dist/lib/mini-oxygen/workerd-inspector-proxy.js +0 -200
  116. package/dist/lib/mini-oxygen/workerd-inspector.js +0 -219
  117. package/dist/lib/missing-routes.test.js +0 -45
  118. package/dist/lib/remix-version-check.test.js +0 -39
  119. package/dist/lib/remix-version-interop.test.js +0 -13
  120. package/dist/lib/setups/i18n/domains.test.js +0 -39
  121. package/dist/lib/setups/i18n/replacers.test.js +0 -261
  122. package/dist/lib/setups/i18n/subdomains.test.js +0 -39
  123. package/dist/lib/setups/i18n/subfolders.test.js +0 -39
  124. package/dist/lib/setups/routes/generate.test.js +0 -296
  125. package/dist/lib/shell.test.js +0 -111
  126. package/dist/lib/shopify-config.test.js +0 -199
  127. package/dist/lib/string.test.js +0 -16
  128. package/dist/lib/virtual-routes.test.js +0 -49
  129. package/dist/lib/vite/hydrogen-middleware.js +0 -82
  130. package/dist/lib/vite/mini-oxygen.js +0 -152
  131. package/dist/lib/vite/plugins.d.ts +0 -27
  132. package/dist/lib/vite/plugins.js +0 -139
  133. package/dist/lib/vite/shared.js +0 -10
  134. package/dist/lib/vite/utils.js +0 -55
  135. package/dist/lib/vite/worker-entry.js +0 -1518
  136. /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
- }