@shopify/cli-hydrogen 5.4.2 → 5.5.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.
- package/dist/commands/hydrogen/debug/cpu.js +98 -0
- package/dist/commands/hydrogen/dev.js +3 -4
- package/dist/commands/hydrogen/init.test.js +2 -2
- package/dist/generator-templates/starter/app/root.tsx +3 -1
- package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +1 -1
- package/dist/generator-templates/starter/app/routes/account_.recover.tsx +7 -2
- package/dist/generator-templates/starter/app/routes/account_.register.tsx +3 -3
- package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +1 -0
- package/dist/generator-templates/starter/package.json +4 -3
- package/dist/generator-templates/starter/storefrontapi.generated.d.ts +4 -4
- package/dist/hooks/init.js +4 -1
- package/dist/lib/cpu-profiler.js +92 -0
- package/dist/lib/mini-oxygen/node.js +5 -5
- package/dist/lib/mini-oxygen/workerd.js +21 -14
- package/dist/lib/onboarding/common.js +2 -2
- package/dist/lib/request-events.js +20 -16
- package/dist/lib/setups/i18n/domains.test.js +14 -0
- package/dist/lib/setups/i18n/index.js +3 -3
- package/dist/lib/setups/i18n/replacers.js +84 -64
- package/dist/lib/setups/i18n/replacers.test.js +242 -0
- package/dist/lib/setups/i18n/subdomains.test.js +14 -0
- package/dist/lib/setups/i18n/subfolders.test.js +14 -0
- package/dist/lib/setups/i18n/templates/domains.js +1 -1
- package/dist/lib/setups/i18n/templates/domains.ts +5 -2
- package/dist/lib/setups/i18n/templates/subdomains.ts +4 -1
- package/dist/lib/setups/i18n/templates/subfolders.js +1 -2
- package/dist/lib/setups/i18n/templates/subfolders.ts +7 -6
- package/dist/lib/setups/routes/generate.js +5 -2
- package/dist/lib/transpile/file.js +6 -0
- package/dist/lib/transpile/index.js +2 -0
- package/dist/lib/transpile/morph/classes.js +48 -0
- package/dist/lib/transpile/morph/functions.js +76 -0
- package/dist/lib/transpile/morph/index.js +67 -0
- package/dist/lib/transpile/morph/typedefs.js +133 -0
- package/dist/lib/transpile/morph/utils.js +15 -0
- package/dist/lib/{transpile-ts.js → transpile/project.js} +38 -59
- package/oclif.manifest.json +27 -1
- package/package.json +7 -7
|
@@ -16,16 +16,16 @@ const I18N_STRATEGY_NAME_MAP = {
|
|
|
16
16
|
};
|
|
17
17
|
const I18N_CHOICES = [...SETUP_I18N_STRATEGIES, "none"];
|
|
18
18
|
async function setupI18nStrategy(strategy, options) {
|
|
19
|
-
const
|
|
19
|
+
const isJs = options.serverEntryPoint?.endsWith(".js") ?? false;
|
|
20
20
|
const templatePath = fileURLToPath(
|
|
21
|
-
new URL(`./templates/${strategy}
|
|
21
|
+
new URL(`./templates/${strategy}.ts`, import.meta.url)
|
|
22
22
|
);
|
|
23
23
|
if (!await fileExists(templatePath)) {
|
|
24
24
|
throw new Error("Unknown strategy");
|
|
25
25
|
}
|
|
26
26
|
const template = await readFile(templatePath);
|
|
27
27
|
const formatConfig = await getCodeFormatOptions(options.rootDirectory);
|
|
28
|
-
await replaceServerI18n(options, formatConfig, template);
|
|
28
|
+
await replaceServerI18n(options, formatConfig, template, isJs);
|
|
29
29
|
await replaceRemixEnv(options, formatConfig, template);
|
|
30
30
|
}
|
|
31
31
|
async function renderI18nPrompt(options) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
2
|
-
import { joinPath
|
|
2
|
+
import { joinPath } from '@shopify/cli-kit/node/path';
|
|
3
3
|
import { fileExists } from '@shopify/cli-kit/node/fs';
|
|
4
4
|
import { replaceFileContent, findFileWithExtension } from '../../file.js';
|
|
5
5
|
import { importLangAstGrep } from '../../ast.js';
|
|
6
|
+
import { transpileFile } from '../../transpile/index.js';
|
|
6
7
|
|
|
7
|
-
async function replaceServerI18n({ rootDirectory, serverEntryPoint = "server" }, formatConfig,
|
|
8
|
+
async function replaceServerI18n({ rootDirectory, serverEntryPoint = "server" }, formatConfig, localeExtractImpl, isJs) {
|
|
8
9
|
const { filepath, astType } = await findEntryFile({
|
|
9
10
|
rootDirectory,
|
|
10
11
|
serverEntryPoint
|
|
@@ -34,7 +35,7 @@ async function replaceServerI18n({ rootDirectory, serverEntryPoint = "server" },
|
|
|
34
35
|
}
|
|
35
36
|
});
|
|
36
37
|
const requestIdentifierName = requestIdentifier?.text() ?? "request";
|
|
37
|
-
const i18nFunctionName =
|
|
38
|
+
const i18nFunctionName = localeExtractImpl.match(
|
|
38
39
|
/^(export )?function (\w+)/m
|
|
39
40
|
)?.[2];
|
|
40
41
|
if (!i18nFunctionName) {
|
|
@@ -91,6 +92,36 @@ async function replaceServerI18n({ rootDirectory, serverEntryPoint = "server" },
|
|
|
91
92
|
`Please add a call to ${importName}({...})`
|
|
92
93
|
);
|
|
93
94
|
}
|
|
95
|
+
const defaultExportObject = root.find({
|
|
96
|
+
rule: {
|
|
97
|
+
kind: "export_statement",
|
|
98
|
+
regex: "^export default \\{"
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
if (!defaultExportObject) {
|
|
102
|
+
throw new AbortError(
|
|
103
|
+
"Could not find a default export in the server entry point"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
let localeExtractFn = localeExtractImpl.match(/^(\/\*\*.*?\*\/\n)?^function .+?^}/ms)?.[0] || "";
|
|
107
|
+
if (!localeExtractFn) {
|
|
108
|
+
throw new AbortError(
|
|
109
|
+
"Could not find the locale extract function. This is a bug in Hydrogen."
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (isJs) {
|
|
113
|
+
localeExtractFn = await transpileFile(
|
|
114
|
+
localeExtractFn,
|
|
115
|
+
"locale-extract-server.ts"
|
|
116
|
+
);
|
|
117
|
+
} else {
|
|
118
|
+
localeExtractFn = localeExtractFn.replace(/\/\*\*.*?\*\//gms, "");
|
|
119
|
+
}
|
|
120
|
+
const defaultExportEnd = defaultExportObject.range().end.index;
|
|
121
|
+
content = content.slice(0, defaultExportEnd) + `
|
|
122
|
+
|
|
123
|
+
${localeExtractFn}
|
|
124
|
+
` + content.slice(defaultExportEnd);
|
|
94
125
|
const i18nProperty = argumentObject.find({
|
|
95
126
|
rule: {
|
|
96
127
|
kind: "property_identifier",
|
|
@@ -112,49 +143,21 @@ async function replaceServerI18n({ rootDirectory, serverEntryPoint = "server" },
|
|
|
112
143
|
const firstPart = content.slice(0, end.index - 1);
|
|
113
144
|
content = firstPart + ((/,\s*$/.test(firstPart) ? "" : ",") + `i18n: ${i18nFunctionCall}`) + content.slice(end.index - 1);
|
|
114
145
|
}
|
|
115
|
-
|
|
116
|
-
/import\s+type\s+[^;]+?;/
|
|
117
|
-
)?.[0];
|
|
118
|
-
if (importTypes) {
|
|
119
|
-
localeExtractImplementation = localeExtractImplementation.replace(
|
|
120
|
-
importTypes,
|
|
121
|
-
""
|
|
122
|
-
);
|
|
123
|
-
const lastImportNode = root.findAll({ rule: { kind: "import_statement" } }).pop();
|
|
124
|
-
if (lastImportNode) {
|
|
125
|
-
const lastImportContent = lastImportNode.text();
|
|
126
|
-
content = content.replace(
|
|
127
|
-
lastImportContent,
|
|
128
|
-
lastImportContent + "\n" + importTypes.replace(
|
|
129
|
-
/'[^']+'/,
|
|
130
|
-
`'@shopify/hydrogen/storefront-api-types'`
|
|
131
|
-
)
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return content + `
|
|
136
|
-
|
|
137
|
-
${localeExtractImplementation.replace(/^export function/m, "function").replace(/^export {.*?;/m, "")}
|
|
138
|
-
`;
|
|
146
|
+
return content;
|
|
139
147
|
});
|
|
140
148
|
}
|
|
141
|
-
async function replaceRemixEnv({ rootDirectory
|
|
149
|
+
async function replaceRemixEnv({ rootDirectory }, formatConfig, localeExtractImpl) {
|
|
142
150
|
const remixEnvPath = joinPath(rootDirectory, "remix.env.d.ts");
|
|
143
151
|
if (!await fileExists(remixEnvPath)) {
|
|
144
152
|
return;
|
|
145
153
|
}
|
|
146
|
-
const
|
|
154
|
+
const i18nType = localeExtractImpl.match(
|
|
155
|
+
/^(export )?(type \w+ =\s+\{.*?\};)\n/ms
|
|
156
|
+
)?.[2];
|
|
157
|
+
const i18nTypeName = i18nType?.match(/^type (\w+)/)?.[1];
|
|
147
158
|
if (!i18nTypeName) {
|
|
148
159
|
return;
|
|
149
160
|
}
|
|
150
|
-
const { filepath: entryFilepath } = await findEntryFile({
|
|
151
|
-
rootDirectory,
|
|
152
|
-
serverEntryPoint
|
|
153
|
-
});
|
|
154
|
-
const relativePathToEntry = relativePath(
|
|
155
|
-
rootDirectory,
|
|
156
|
-
entryFilepath
|
|
157
|
-
).replace(/.[tj]sx?$/, "");
|
|
158
161
|
await replaceFileContent(remixEnvPath, formatConfig, async (content) => {
|
|
159
162
|
if (content.includes(`Storefront<`))
|
|
160
163
|
return;
|
|
@@ -176,38 +179,55 @@ async function replaceRemixEnv({ rootDirectory, serverEntryPoint }, formatConfig
|
|
|
176
179
|
}
|
|
177
180
|
}
|
|
178
181
|
});
|
|
179
|
-
if (
|
|
180
|
-
|
|
182
|
+
if (storefrontTypeNode) {
|
|
183
|
+
const storefrontTypeNodeRange = storefrontTypeNode.range();
|
|
184
|
+
content = content.slice(0, storefrontTypeNodeRange.end.index) + `<${i18nTypeName}>` + content.slice(storefrontTypeNodeRange.end.index);
|
|
181
185
|
}
|
|
182
|
-
const
|
|
183
|
-
content = content.slice(0, end.index) + `<${i18nTypeName}>` + content.slice(end.index);
|
|
184
|
-
const serverImportNode = root.findAll({
|
|
186
|
+
const ambientDeclarationContentNode = root.find({
|
|
185
187
|
rule: {
|
|
186
|
-
kind: "
|
|
187
|
-
|
|
188
|
-
kind: "
|
|
189
|
-
stopBy: "end",
|
|
190
|
-
regex: `^(\\./)?${relativePathToEntry.replaceAll(
|
|
191
|
-
".",
|
|
192
|
-
"\\."
|
|
193
|
-
)}(\\.[jt]sx?)?$`
|
|
188
|
+
kind: "statement_block",
|
|
189
|
+
inside: {
|
|
190
|
+
kind: "ambient_declaration"
|
|
194
191
|
}
|
|
195
192
|
}
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
193
|
+
});
|
|
194
|
+
const i18nTypeDeclaration = `
|
|
195
|
+
/**
|
|
196
|
+
* The I18nLocale used for Storefront API query context.
|
|
197
|
+
*/
|
|
198
|
+
${i18nType}`;
|
|
199
|
+
if (ambientDeclarationContentNode) {
|
|
200
|
+
const { end } = ambientDeclarationContentNode.range();
|
|
201
|
+
content = content.slice(0, end.index - 1) + `
|
|
202
|
+
|
|
203
|
+
${i18nTypeDeclaration}
|
|
204
|
+
` + content.slice(end.index - 1);
|
|
202
205
|
} else {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
content = content + `
|
|
207
|
+
|
|
208
|
+
declare global {
|
|
209
|
+
${i18nTypeDeclaration}
|
|
210
|
+
}`;
|
|
211
|
+
}
|
|
212
|
+
const importImplTypes = localeExtractImpl.match(
|
|
213
|
+
/import\s+type\s+[^;]+?;/
|
|
214
|
+
)?.[0];
|
|
215
|
+
if (importImplTypes) {
|
|
216
|
+
const importPlace = root.findAll({
|
|
217
|
+
rule: {
|
|
218
|
+
kind: "import_statement",
|
|
219
|
+
has: {
|
|
220
|
+
kind: "string_fragment",
|
|
221
|
+
stopBy: "end",
|
|
222
|
+
regex: `^@shopify/hydrogen$`
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}).pop() ?? root.findAll({ rule: { kind: "import_statement" } }).pop() ?? root.findAll({ rule: { kind: "comment", regex: "^/// <reference" } }).pop();
|
|
226
|
+
const importPlaceRange = importPlace?.range() ?? { end: { index: 0 } };
|
|
227
|
+
content = content.slice(0, importPlaceRange.end.index) + importImplTypes.replace(
|
|
228
|
+
/'[^']+'/,
|
|
229
|
+
`'@shopify/hydrogen/storefront-api-types'`
|
|
230
|
+
) + content.slice(importPlaceRange.end.index);
|
|
211
231
|
}
|
|
212
232
|
return content;
|
|
213
233
|
});
|
|
@@ -0,0 +1,242 @@
|
|
|
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 { Storefront, HydrogenCart } from \\"@shopify/hydrogen\\";
|
|
52
|
+
import type {
|
|
53
|
+
LanguageCode,
|
|
54
|
+
CountryCode,
|
|
55
|
+
} from \\"@shopify/hydrogen/storefront-api-types\\";
|
|
56
|
+
import type { CustomerAccessToken } from \\"@shopify/hydrogen/storefront-api-types\\";
|
|
57
|
+
import type { HydrogenSession } from \\"./server\\";
|
|
58
|
+
|
|
59
|
+
declare global {
|
|
60
|
+
/**
|
|
61
|
+
* A global \`process\` object is only available during build to access NODE_ENV.
|
|
62
|
+
*/
|
|
63
|
+
const process: { env: { NODE_ENV: \\"production\\" | \\"development\\" } };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Declare expected Env parameter in fetch handler.
|
|
67
|
+
*/
|
|
68
|
+
interface Env {
|
|
69
|
+
SESSION_SECRET: string;
|
|
70
|
+
PUBLIC_STOREFRONT_API_TOKEN: string;
|
|
71
|
+
PRIVATE_STOREFRONT_API_TOKEN: string;
|
|
72
|
+
PUBLIC_STORE_DOMAIN: string;
|
|
73
|
+
PUBLIC_STOREFRONT_ID: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The I18nLocale used for Storefront API query context.
|
|
78
|
+
*/
|
|
79
|
+
type I18nLocale = { language: LanguageCode; country: CountryCode };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
declare module \\"@shopify/remix-oxygen\\" {
|
|
83
|
+
/**
|
|
84
|
+
* Declare local additions to the Remix loader context.
|
|
85
|
+
*/
|
|
86
|
+
export interface AppLoadContext {
|
|
87
|
+
env: Env;
|
|
88
|
+
cart: HydrogenCart;
|
|
89
|
+
storefront: Storefront<I18nLocale>;
|
|
90
|
+
session: HydrogenSession;
|
|
91
|
+
waitUntil: ExecutionContext[\\"waitUntil\\"];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Declare the data we expect to access via \`context.session\`.
|
|
96
|
+
*/
|
|
97
|
+
export interface SessionData {
|
|
98
|
+
customerAccessToken: CustomerAccessToken;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
"
|
|
102
|
+
`);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it("adds i18n type to server.ts", async () => {
|
|
106
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
107
|
+
const skeletonDir = getSkeletonSourceDir();
|
|
108
|
+
await writeFile(
|
|
109
|
+
joinPath(tmpDir, serverTs),
|
|
110
|
+
// Remove the part that is not needed for this test (HydrogenSession, Cart query, etc);
|
|
111
|
+
(await readFile(joinPath(skeletonDir, serverTs))).replace(/^};$.*/ms, "};")
|
|
112
|
+
);
|
|
113
|
+
await replaceServerI18n(
|
|
114
|
+
{ rootDirectory: tmpDir, serverEntryPoint: serverTs },
|
|
115
|
+
{},
|
|
116
|
+
await readFile(
|
|
117
|
+
fileURLToPath(new URL("./templates/domains.ts", import.meta.url))
|
|
118
|
+
),
|
|
119
|
+
false
|
|
120
|
+
);
|
|
121
|
+
const newContent = await readFile(joinPath(tmpDir, serverTs));
|
|
122
|
+
expect(() => checkTypes(newContent)).not.toThrow();
|
|
123
|
+
expect(newContent).toMatchInlineSnapshot(`
|
|
124
|
+
"// Virtual entry point for the app
|
|
125
|
+
import * as remixBuild from \\"@remix-run/dev/server-build\\";
|
|
126
|
+
import {
|
|
127
|
+
cartGetIdDefault,
|
|
128
|
+
cartSetIdDefault,
|
|
129
|
+
createCartHandler,
|
|
130
|
+
createStorefrontClient,
|
|
131
|
+
storefrontRedirect,
|
|
132
|
+
} from \\"@shopify/hydrogen\\";
|
|
133
|
+
import {
|
|
134
|
+
createRequestHandler,
|
|
135
|
+
getStorefrontHeaders,
|
|
136
|
+
createCookieSessionStorage,
|
|
137
|
+
type SessionStorage,
|
|
138
|
+
type Session,
|
|
139
|
+
} from \\"@shopify/remix-oxygen\\";
|
|
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
|
+
HydrogenSession.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 cart handler that will be used to
|
|
180
|
+
* create and update the cart in the session.
|
|
181
|
+
*/
|
|
182
|
+
const cart = createCartHandler({
|
|
183
|
+
storefront,
|
|
184
|
+
getCartId: cartGetIdDefault(request.headers),
|
|
185
|
+
setCartId: cartSetIdDefault(),
|
|
186
|
+
cartQueryFragment: CART_QUERY_FRAGMENT,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create a Remix request handler and pass
|
|
191
|
+
* Hydrogen's Storefront client to the loader context.
|
|
192
|
+
*/
|
|
193
|
+
const handleRequest = createRequestHandler({
|
|
194
|
+
build: remixBuild,
|
|
195
|
+
mode: process.env.NODE_ENV,
|
|
196
|
+
getLoadContext: () => ({ session, storefront, cart, env, waitUntil }),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const response = await handleRequest(request);
|
|
200
|
+
|
|
201
|
+
if (response.status === 404) {
|
|
202
|
+
/**
|
|
203
|
+
* Check for redirects only when there's a 404 from the app.
|
|
204
|
+
* If the redirect doesn't exist, then \`storefrontRedirect\`
|
|
205
|
+
* will pass through the 404 response.
|
|
206
|
+
*/
|
|
207
|
+
return storefrontRedirect({ request, response, storefront });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return response;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
// eslint-disable-next-line no-console
|
|
213
|
+
console.error(error);
|
|
214
|
+
return new Response(\\"An unexpected error occurred\\", { status: 500 });
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
function getLocaleFromRequest(request: Request): I18nLocale {
|
|
220
|
+
const defaultLocale: I18nLocale = { language: \\"EN\\", country: \\"US\\" };
|
|
221
|
+
const supportedLocales = {
|
|
222
|
+
ES: \\"ES\\",
|
|
223
|
+
FR: \\"FR\\",
|
|
224
|
+
DE: \\"DE\\",
|
|
225
|
+
JP: \\"JA\\",
|
|
226
|
+
} as Record<I18nLocale[\\"country\\"], I18nLocale[\\"language\\"]>;
|
|
227
|
+
|
|
228
|
+
const url = new URL(request.url);
|
|
229
|
+
const domain = url.hostname
|
|
230
|
+
.split(\\".\\")
|
|
231
|
+
.pop()
|
|
232
|
+
?.toUpperCase() as keyof typeof supportedLocales;
|
|
233
|
+
|
|
234
|
+
return domain && supportedLocales[domain]
|
|
235
|
+
? { language: supportedLocales[domain], country: domain }
|
|
236
|
+
: defaultLocale;
|
|
237
|
+
}
|
|
238
|
+
"
|
|
239
|
+
`);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
1
2
|
import { describe, it, expect } from 'vitest';
|
|
2
3
|
import { getLocaleFromRequest } from './templates/subdomains.js';
|
|
4
|
+
import { readFile } from '@shopify/cli-kit/node/fs';
|
|
3
5
|
|
|
4
6
|
describe("Setup i18n with subdomains", () => {
|
|
5
7
|
it("extracts the locale from the subdomain", () => {
|
|
@@ -22,4 +24,16 @@ describe("Setup i18n with subdomains", () => {
|
|
|
22
24
|
country: "ES"
|
|
23
25
|
});
|
|
24
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
|
+
});
|
|
25
39
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
1
2
|
import { describe, it, expect } from 'vitest';
|
|
2
3
|
import { getLocaleFromRequest } from './templates/subfolders.js';
|
|
4
|
+
import { readFile } from '@shopify/cli-kit/node/fs';
|
|
3
5
|
|
|
4
6
|
describe("Setup i18n with subfolders", () => {
|
|
5
7
|
it("extracts the locale from the pathname", () => {
|
|
@@ -22,4 +24,16 @@ describe("Setup i18n with subfolders", () => {
|
|
|
22
24
|
country: "ES"
|
|
23
25
|
});
|
|
24
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
|
+
});
|
|
25
39
|
});
|
|
@@ -8,7 +8,7 @@ function getLocaleFromRequest(request) {
|
|
|
8
8
|
};
|
|
9
9
|
const url = new URL(request.url);
|
|
10
10
|
const domain = url.hostname.split(".").pop()?.toUpperCase();
|
|
11
|
-
return supportedLocales[domain] ? { language: supportedLocales[domain], country: domain } : defaultLocale;
|
|
11
|
+
return domain && supportedLocales[domain] ? { language: supportedLocales[domain], country: domain } : defaultLocale;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export { getLocaleFromRequest };
|
|
@@ -2,6 +2,9 @@ import type {LanguageCode, CountryCode} from '../mock-i18n-types.js';
|
|
|
2
2
|
|
|
3
3
|
export type I18nLocale = {language: LanguageCode; country: CountryCode};
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @returns {I18nLocale}
|
|
7
|
+
*/
|
|
5
8
|
function getLocaleFromRequest(request: Request): I18nLocale {
|
|
6
9
|
const defaultLocale: I18nLocale = {language: 'EN', country: 'US'};
|
|
7
10
|
const supportedLocales = {
|
|
@@ -9,7 +12,7 @@ function getLocaleFromRequest(request: Request): I18nLocale {
|
|
|
9
12
|
FR: 'FR',
|
|
10
13
|
DE: 'DE',
|
|
11
14
|
JP: 'JA',
|
|
12
|
-
} as Record<
|
|
15
|
+
} as Record<I18nLocale['country'], I18nLocale['language']>;
|
|
13
16
|
|
|
14
17
|
const url = new URL(request.url);
|
|
15
18
|
const domain = url.hostname
|
|
@@ -17,7 +20,7 @@ function getLocaleFromRequest(request: Request): I18nLocale {
|
|
|
17
20
|
.pop()
|
|
18
21
|
?.toUpperCase() as keyof typeof supportedLocales;
|
|
19
22
|
|
|
20
|
-
return supportedLocales[domain]
|
|
23
|
+
return domain && supportedLocales[domain]
|
|
21
24
|
? {language: supportedLocales[domain], country: domain}
|
|
22
25
|
: defaultLocale;
|
|
23
26
|
}
|
|
@@ -2,6 +2,9 @@ import type {LanguageCode, CountryCode} from '../mock-i18n-types.js';
|
|
|
2
2
|
|
|
3
3
|
export type I18nLocale = {language: LanguageCode; country: CountryCode};
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @returns {I18nLocale}
|
|
7
|
+
*/
|
|
5
8
|
function getLocaleFromRequest(request: Request): I18nLocale {
|
|
6
9
|
const defaultLocale: I18nLocale = {language: 'EN', country: 'US'};
|
|
7
10
|
const supportedLocales = {
|
|
@@ -9,7 +12,7 @@ function getLocaleFromRequest(request: Request): I18nLocale {
|
|
|
9
12
|
FR: 'FR',
|
|
10
13
|
DE: 'DE',
|
|
11
14
|
JP: 'JA',
|
|
12
|
-
} as Record<
|
|
15
|
+
} as Record<I18nLocale['country'], I18nLocale['language']>;
|
|
13
16
|
|
|
14
17
|
const url = new URL(request.url);
|
|
15
18
|
const firstSubdomain = url.hostname
|
|
@@ -2,8 +2,7 @@ function getLocaleFromRequest(request) {
|
|
|
2
2
|
const url = new URL(request.url);
|
|
3
3
|
const firstPathPart = url.pathname.split("/")[1]?.toUpperCase() ?? "";
|
|
4
4
|
let pathPrefix = "";
|
|
5
|
-
let language = "EN";
|
|
6
|
-
let country = "US";
|
|
5
|
+
let [language, country] = ["EN", "US"];
|
|
7
6
|
if (/^[A-Z]{2}-[A-Z]{2}$/i.test(firstPathPart)) {
|
|
8
7
|
pathPrefix = "/" + firstPathPart;
|
|
9
8
|
[language, country] = firstPathPart.split("-");
|
|
@@ -6,20 +6,21 @@ export type I18nLocale = {
|
|
|
6
6
|
pathPrefix: string;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @returns {I18nLocale}
|
|
11
|
+
*/
|
|
9
12
|
function getLocaleFromRequest(request: Request): I18nLocale {
|
|
10
13
|
const url = new URL(request.url);
|
|
11
14
|
const firstPathPart = url.pathname.split('/')[1]?.toUpperCase() ?? '';
|
|
12
15
|
|
|
16
|
+
type I18nFromUrl = [I18nLocale['language'], I18nLocale['country']];
|
|
17
|
+
|
|
13
18
|
let pathPrefix = '';
|
|
14
|
-
let language:
|
|
15
|
-
let country: CountryCode = 'US';
|
|
19
|
+
let [language, country]: I18nFromUrl = ['EN', 'US'];
|
|
16
20
|
|
|
17
21
|
if (/^[A-Z]{2}-[A-Z]{2}$/i.test(firstPathPart)) {
|
|
18
22
|
pathPrefix = '/' + firstPathPart;
|
|
19
|
-
[language, country] = firstPathPart.split('-') as
|
|
20
|
-
LanguageCode,
|
|
21
|
-
CountryCode,
|
|
22
|
-
];
|
|
23
|
+
[language, country] = firstPathPart.split('-') as I18nFromUrl;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
return {language, country, pathPrefix};
|
|
@@ -3,7 +3,7 @@ import { fileExists, mkdir, copyFile, readFile, writeFile } from '@shopify/cli-k
|
|
|
3
3
|
import { joinPath, relativizePath, dirname, relativePath, resolvePath, basename } from '@shopify/cli-kit/node/path';
|
|
4
4
|
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
5
5
|
import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
|
|
6
|
-
import { transpileFile } from '
|
|
6
|
+
import { transpileFile } from '../../transpile/index.js';
|
|
7
7
|
import { getCodeFormatOptions, formatCode } from '../../../lib/format-code.js';
|
|
8
8
|
import { getTemplateAppFile, GENERATOR_ROUTE_DIR, getStarterDir } from '../../../lib/build.js';
|
|
9
9
|
import { getV2Flags, convertTemplateToRemixVersion, convertRouteToV1 } from '../../../lib/remix-version-interop.js';
|
|
@@ -165,7 +165,10 @@ async function generateProjectFile(routeFrom, {
|
|
|
165
165
|
v2Flags
|
|
166
166
|
);
|
|
167
167
|
if (!typescript) {
|
|
168
|
-
templateContent = await transpileFile(
|
|
168
|
+
templateContent = await transpileFile(
|
|
169
|
+
templateContent,
|
|
170
|
+
templateAppFilePath
|
|
171
|
+
);
|
|
169
172
|
}
|
|
170
173
|
if (adapter) {
|
|
171
174
|
templateContent = templateContent.replace(
|