@shopify/cli-hydrogen 7.0.1 → 7.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/hydrogen/build-vite.js +131 -0
- package/dist/commands/hydrogen/build.js +7 -21
- package/dist/commands/hydrogen/check.js +1 -1
- package/dist/commands/hydrogen/codegen.js +3 -3
- package/dist/commands/hydrogen/debug/cpu.js +1 -1
- package/dist/commands/hydrogen/deploy.js +113 -51
- package/dist/commands/hydrogen/deploy.test.js +162 -19
- package/dist/commands/hydrogen/dev-vite.js +159 -0
- package/dist/commands/hydrogen/dev.js +11 -14
- package/dist/commands/hydrogen/env/list.js +1 -1
- package/dist/commands/hydrogen/env/pull.js +3 -3
- package/dist/commands/hydrogen/env/pull.test.js +2 -0
- package/dist/commands/hydrogen/env/push__unstable.js +190 -0
- package/dist/commands/hydrogen/env/push__unstable.test.js +383 -0
- package/dist/commands/hydrogen/generate/route.js +8 -3
- package/dist/commands/hydrogen/init.d.ts +69 -0
- package/dist/commands/hydrogen/init.js +5 -5
- package/dist/commands/hydrogen/init.test.js +3 -2
- package/dist/commands/hydrogen/link.js +2 -2
- package/dist/commands/hydrogen/list.js +1 -1
- package/dist/commands/hydrogen/login.js +2 -9
- package/dist/commands/hydrogen/logout.js +1 -1
- package/dist/commands/hydrogen/preview.js +15 -7
- package/dist/commands/hydrogen/setup/css.js +3 -3
- package/dist/commands/hydrogen/setup/markets.js +4 -4
- package/dist/commands/hydrogen/setup/vite.js +209 -0
- package/dist/commands/hydrogen/setup.js +8 -6
- package/dist/commands/hydrogen/unlink.js +1 -1
- package/dist/commands/hydrogen/upgrade.js +5 -3
- package/dist/generator-templates/assets/vite/package.json +15 -0
- package/dist/generator-templates/assets/vite/vite.config.js +13 -0
- package/dist/generator-templates/starter/CHANGELOG.md +69 -0
- package/dist/generator-templates/starter/README.md +25 -2
- package/dist/generator-templates/starter/app/components/Cart.tsx +2 -2
- package/dist/generator-templates/starter/app/components/Layout.tsx +9 -1
- package/dist/generator-templates/starter/app/components/Search.tsx +44 -15
- package/dist/generator-templates/starter/app/lib/search.ts +29 -0
- package/dist/generator-templates/starter/app/root.tsx +1 -4
- package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +2 -2
- package/dist/generator-templates/starter/app/routes/api.predictive-search.tsx +1 -21
- package/dist/generator-templates/starter/app/routes/cart.tsx +1 -5
- package/dist/generator-templates/starter/app/routes/search.tsx +8 -2
- package/dist/generator-templates/starter/app/styles/app.css +10 -15
- package/dist/generator-templates/starter/package.json +9 -8
- package/dist/generator-templates/starter/public/.gitkeep +0 -0
- package/dist/generator-templates/starter/remix.config.js +1 -0
- package/dist/generator-templates/starter/server.ts +1 -0
- package/dist/hooks/init.js +3 -3
- package/dist/lib/build.js +2 -1
- package/dist/lib/codegen.js +8 -3
- package/dist/lib/environment-variables.test.js +4 -2
- package/dist/lib/flags.js +149 -89
- package/dist/lib/graphql/admin/pull-variables.js +1 -0
- package/dist/lib/graphql/admin/pull-variables.test.js +7 -1
- package/dist/lib/graphql/admin/push-variables.js +35 -0
- package/dist/lib/log.js +1 -0
- package/dist/lib/mini-oxygen/common.js +2 -1
- package/dist/lib/mini-oxygen/node.js +2 -2
- package/dist/lib/mini-oxygen/workerd-inspector.js +1 -1
- package/dist/lib/mini-oxygen/workerd.js +29 -17
- package/dist/lib/onboarding/common.js +0 -3
- package/dist/lib/onboarding/local.js +4 -1
- package/dist/lib/onboarding/remote.js +16 -11
- package/dist/lib/remix-config.js +1 -1
- package/dist/lib/request-events.js +3 -3
- package/dist/lib/setups/css/assets.js +7 -2
- package/dist/lib/setups/i18n/replacers.test.js +1 -0
- package/dist/lib/setups/routes/generate.js +58 -10
- package/dist/lib/setups/routes/templates/locale-check.js +9 -0
- package/dist/lib/setups/routes/templates/locale-check.ts +16 -0
- package/dist/lib/template-diff.js +26 -11
- package/dist/lib/template-downloader.js +11 -2
- package/dist/lib/vite/hydrogen-middleware.js +82 -0
- package/dist/lib/vite/mini-oxygen.js +152 -0
- package/dist/lib/vite/plugins.d.ts +27 -0
- package/dist/lib/vite/plugins.js +139 -0
- package/dist/lib/vite/shared.js +10 -0
- package/dist/lib/vite/utils.js +55 -0
- package/dist/lib/vite/worker-entry.js +1518 -0
- package/dist/lib/vite-config.js +45 -0
- package/dist/virtual-routes/lib/useDebugNetworkServer.jsx +4 -2
- package/dist/virtual-routes/routes/index.jsx +5 -5
- package/dist/virtual-routes/routes/subrequest-profiler.jsx +1 -1
- package/dist/virtual-routes/virtual-root.jsx +1 -1
- package/oclif.manifest.json +1146 -474
- package/package.json +36 -11
- /package/dist/generator-templates/starter/{public → app/assets}/favicon.svg +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
|
+
import { diffLines } from 'diff';
|
|
3
|
+
import { Flags } from '@oclif/core';
|
|
4
|
+
import { commonFlags, flagsToCamelObject } from '../../../lib/flags.js';
|
|
5
|
+
import { login } from '../../../lib/auth.js';
|
|
6
|
+
import { getCliCommand } from '../../../lib/shell.js';
|
|
7
|
+
import { resolvePath } from '@shopify/cli-kit/node/path';
|
|
8
|
+
import { renderConfirmationPrompt, renderSelectPrompt, renderInfo, renderSuccess } from '@shopify/cli-kit/node/ui';
|
|
9
|
+
import { outputContent, outputToken, outputWarn } from '@shopify/cli-kit/node/output';
|
|
10
|
+
import { renderMissingLink } from '../../../lib/render-errors.js';
|
|
11
|
+
import { getStorefrontEnvironments } from '../../../lib/graphql/admin/list-environments.js';
|
|
12
|
+
import { linkStorefront } from '../link.js';
|
|
13
|
+
import { getStorefrontEnvVariables } from '../../../lib/graphql/admin/pull-variables.js';
|
|
14
|
+
import { pushStorefrontEnvVariables } from '../../../lib/graphql/admin/push-variables.js';
|
|
15
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
16
|
+
import { readAndParseDotEnv } from '@shopify/cli-kit/node/dot-env';
|
|
17
|
+
|
|
18
|
+
class EnvPush extends Command {
|
|
19
|
+
static description = "Push environment variables from the local .env file to your linked Hydrogen storefront.";
|
|
20
|
+
static hidden = true;
|
|
21
|
+
static flags = {
|
|
22
|
+
...commonFlags.env,
|
|
23
|
+
"env-file": Flags.string({
|
|
24
|
+
description: "Specify the environment variable file name. Default value is '.env'.",
|
|
25
|
+
env: "SHOPIFY_HYDROGEN_ENVIRONMENT_FILENAME"
|
|
26
|
+
}),
|
|
27
|
+
...commonFlags.path
|
|
28
|
+
};
|
|
29
|
+
async run() {
|
|
30
|
+
const { flags } = await this.parse(EnvPush);
|
|
31
|
+
await runEnvPush({ ...flagsToCamelObject(flags) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function runEnvPush({
|
|
35
|
+
env: environmentName,
|
|
36
|
+
envFile = ".env",
|
|
37
|
+
path = process.cwd()
|
|
38
|
+
}) {
|
|
39
|
+
let validatedEnvironment = {};
|
|
40
|
+
const dotEnvPath = resolvePath(path, envFile);
|
|
41
|
+
const { variables: localVariables } = await readAndParseDotEnv(dotEnvPath);
|
|
42
|
+
const [{ session, config }, cliCommand] = await Promise.all([
|
|
43
|
+
login(path),
|
|
44
|
+
getCliCommand()
|
|
45
|
+
]);
|
|
46
|
+
if (!config.storefront?.id) {
|
|
47
|
+
renderMissingLink({ session, cliCommand });
|
|
48
|
+
const runLink = await renderConfirmationPrompt({
|
|
49
|
+
message: outputContent`Run ${outputToken.genericShellCommand(
|
|
50
|
+
`${cliCommand} link`
|
|
51
|
+
)}?`.value
|
|
52
|
+
});
|
|
53
|
+
if (!runLink)
|
|
54
|
+
return;
|
|
55
|
+
config.storefront = await linkStorefront(path, session, config, {
|
|
56
|
+
cliCommand
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!config.storefront?.id)
|
|
60
|
+
return;
|
|
61
|
+
const { environments: environmentsData } = await getStorefrontEnvironments(session, config.storefront.id) ?? {};
|
|
62
|
+
if (!environmentsData) {
|
|
63
|
+
throw new AbortError("Failed to fetch environments");
|
|
64
|
+
}
|
|
65
|
+
const environments = [
|
|
66
|
+
...environmentsData.filter((environment) => environment.type === "PREVIEW"),
|
|
67
|
+
...environmentsData.filter((environment) => environment.type === "CUSTOM"),
|
|
68
|
+
...environmentsData.filter(
|
|
69
|
+
(environment) => environment.type === "PRODUCTION"
|
|
70
|
+
)
|
|
71
|
+
];
|
|
72
|
+
if (environments.length === 0) {
|
|
73
|
+
throw new AbortError("No environments found");
|
|
74
|
+
}
|
|
75
|
+
if (environmentName) {
|
|
76
|
+
const matchedEnvironments = environments.filter(
|
|
77
|
+
({ name }) => name === environmentName
|
|
78
|
+
);
|
|
79
|
+
if (matchedEnvironments.length === 0) {
|
|
80
|
+
throw new AbortError(
|
|
81
|
+
"Environment not found",
|
|
82
|
+
`We could not find an environment matching the name '${environmentName}'.`
|
|
83
|
+
);
|
|
84
|
+
} else if (matchedEnvironments.length === 1) {
|
|
85
|
+
const { id, name, branch, type } = matchedEnvironments[0] ?? {};
|
|
86
|
+
validatedEnvironment = { id, name, branch, type };
|
|
87
|
+
} else {
|
|
88
|
+
const selection = await renderSelectPrompt({
|
|
89
|
+
message: `There were multiple environments found with the name ${environmentName}:`,
|
|
90
|
+
choices: [
|
|
91
|
+
...matchedEnvironments.map(({ id: id2, name: name2, branch: branch2, type: type2, url }) => ({
|
|
92
|
+
label: `${name2} (${branch2}) ${type2} ${url}`,
|
|
93
|
+
value: id2
|
|
94
|
+
}))
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
const { id, name, branch, type } = matchedEnvironments.find(({ id: id2 }) => id2 === selection) ?? {};
|
|
98
|
+
validatedEnvironment = { id, name, branch, type };
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
const choices = [
|
|
102
|
+
...environments.map(({ id: id2, name: name2, branch: branch2 }) => ({
|
|
103
|
+
label: branch2 ? `${name2} (${branch2})` : name2,
|
|
104
|
+
value: id2
|
|
105
|
+
}))
|
|
106
|
+
];
|
|
107
|
+
const pushToBranchSelection = await renderSelectPrompt({
|
|
108
|
+
message: "Select a set of environment variables to overwrite:",
|
|
109
|
+
choices
|
|
110
|
+
});
|
|
111
|
+
const { id, name, branch, type } = environments.find(({ id: id2 }) => id2 === pushToBranchSelection) ?? {};
|
|
112
|
+
validatedEnvironment = { id, name, branch, type };
|
|
113
|
+
}
|
|
114
|
+
const { environmentVariables = [] } = await getStorefrontEnvVariables(
|
|
115
|
+
session,
|
|
116
|
+
config.storefront.id,
|
|
117
|
+
validatedEnvironment.branch ?? void 0
|
|
118
|
+
) ?? {};
|
|
119
|
+
const remoteVars = environmentVariables.filter(
|
|
120
|
+
({ isSecret, readOnly }) => !isSecret && !readOnly
|
|
121
|
+
);
|
|
122
|
+
const comparableRemoteVars = remoteVars.sort((a, b) => a.key.localeCompare(b.key)).map(({ key, value }) => `${key}=${value}`).join("\n") + "\n";
|
|
123
|
+
const compareableLocalVars = Object.keys(localVariables).sort((a, b) => a.localeCompare(b)).reduce((acc, key) => {
|
|
124
|
+
const { isSecret, readOnly } = environmentVariables.find((variable) => variable.key === key) ?? {};
|
|
125
|
+
if (isSecret || readOnly)
|
|
126
|
+
return acc;
|
|
127
|
+
return [...acc, `${key}=${localVariables[key]}`];
|
|
128
|
+
}, []).join("\n") + "\n";
|
|
129
|
+
if (!validatedEnvironment.name)
|
|
130
|
+
throw new AbortError("Missing environment name");
|
|
131
|
+
const remoteReadOnlyOrSecrets = environmentVariables.reduce(
|
|
132
|
+
(acc, { isSecret, readOnly, key }) => {
|
|
133
|
+
if (!isSecret && !readOnly)
|
|
134
|
+
return acc;
|
|
135
|
+
const localVar = localVariables[key];
|
|
136
|
+
const remoteVar = environmentVariables.find(
|
|
137
|
+
(variable) => variable.key === key
|
|
138
|
+
);
|
|
139
|
+
if (localVar === remoteVar?.value)
|
|
140
|
+
return acc;
|
|
141
|
+
return [...acc, key];
|
|
142
|
+
},
|
|
143
|
+
[]
|
|
144
|
+
);
|
|
145
|
+
if (remoteReadOnlyOrSecrets.length) {
|
|
146
|
+
outputWarn(
|
|
147
|
+
`Variables that are read only or contain secret values cannot be pushed from the CLI: ${remoteReadOnlyOrSecrets.join(
|
|
148
|
+
", "
|
|
149
|
+
)}.
|
|
150
|
+
`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (compareableLocalVars === comparableRemoteVars) {
|
|
154
|
+
renderInfo({
|
|
155
|
+
body: "No changes to your environment variables."
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
} else {
|
|
159
|
+
const diff = diffLines(comparableRemoteVars, compareableLocalVars);
|
|
160
|
+
const confirmPush = await renderConfirmationPrompt({
|
|
161
|
+
confirmationMessage: "Yes, confirm changes",
|
|
162
|
+
cancellationMessage: "No, make changes later",
|
|
163
|
+
message: outputContent`We'll make the following changes to your environment variables for ${validatedEnvironment.name}:
|
|
164
|
+
|
|
165
|
+
${outputToken.linesDiff(diff)}
|
|
166
|
+
Continue?`.value
|
|
167
|
+
});
|
|
168
|
+
if (!confirmPush)
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!validatedEnvironment.id)
|
|
172
|
+
throw new AbortError("Missing environment ID");
|
|
173
|
+
const { userErrors } = await pushStorefrontEnvVariables(
|
|
174
|
+
session,
|
|
175
|
+
config.storefront.id,
|
|
176
|
+
validatedEnvironment.id,
|
|
177
|
+
Object.entries(localVariables).map(([key, value]) => ({ key, value }))
|
|
178
|
+
);
|
|
179
|
+
if (userErrors.length) {
|
|
180
|
+
throw new AbortError(
|
|
181
|
+
"Failed to upload and save environment variables.",
|
|
182
|
+
userErrors[0]?.message
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
renderSuccess({
|
|
186
|
+
body: `Environment variables push to ${validatedEnvironment.name ?? "Preview"} was successful.`
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { EnvPush as default, runEnvPush };
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
|
|
2
|
+
import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
|
|
3
|
+
import { inTemporaryDirectory, writeFile } from '@shopify/cli-kit/node/fs';
|
|
4
|
+
import { joinPath } from '@shopify/cli-kit/node/path';
|
|
5
|
+
import { renderConfirmationPrompt, renderSelectPrompt } from '@shopify/cli-kit/node/ui';
|
|
6
|
+
import { login } from '../../../lib/auth.js';
|
|
7
|
+
import { getStorefrontEnvVariables } from '../../../lib/graphql/admin/pull-variables.js';
|
|
8
|
+
import { getStorefrontEnvironments } from '../../../lib/graphql/admin/list-environments.js';
|
|
9
|
+
import { pushStorefrontEnvVariables } from '../../../lib/graphql/admin/push-variables.js';
|
|
10
|
+
import { runEnvPush } from './push__unstable.js';
|
|
11
|
+
|
|
12
|
+
vi.mock("@shopify/cli-kit/node/ui", async () => {
|
|
13
|
+
const original = await vi.importActual("@shopify/cli-kit/node/ui");
|
|
14
|
+
return {
|
|
15
|
+
...original,
|
|
16
|
+
renderConfirmationPrompt: vi.fn(),
|
|
17
|
+
renderSelectPrompt: vi.fn()
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
vi.mock("../link.js");
|
|
21
|
+
vi.mock("../../../lib/auth.js");
|
|
22
|
+
vi.mock("../../../lib/render-errors.js");
|
|
23
|
+
vi.mock("../../../lib/graphql/admin/pull-variables.js");
|
|
24
|
+
vi.mock("../../../lib/graphql/admin/list-environments.js");
|
|
25
|
+
vi.mock("../../../lib/graphql/admin/push-variables.js");
|
|
26
|
+
const ADMIN_SESSION = {
|
|
27
|
+
token: "abc123",
|
|
28
|
+
storeFqdn: "my-shop"
|
|
29
|
+
};
|
|
30
|
+
const SHOPIFY_CONFIG = {
|
|
31
|
+
shop: "my-shop",
|
|
32
|
+
shopName: "My Shop",
|
|
33
|
+
email: "email",
|
|
34
|
+
storefront: {
|
|
35
|
+
id: "gid://shopify/HydrogenStorefront/2",
|
|
36
|
+
title: "Existing Link"
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const PRODUCTION_ENV = {
|
|
40
|
+
id: "gid://shopify/HydrogenStorefrontEnvironment/1",
|
|
41
|
+
createdAt: "2024-01-01",
|
|
42
|
+
branch: "main",
|
|
43
|
+
name: "Production",
|
|
44
|
+
type: "PRODUCTION",
|
|
45
|
+
url: "production.com"
|
|
46
|
+
};
|
|
47
|
+
const PREVIEW_ENV = {
|
|
48
|
+
id: "gid://shopify/HydrogenStorefrontEnvironment/2",
|
|
49
|
+
createdAt: "2024-01-01",
|
|
50
|
+
branch: null,
|
|
51
|
+
name: "Preview",
|
|
52
|
+
type: "PREVIEW",
|
|
53
|
+
url: null
|
|
54
|
+
};
|
|
55
|
+
const CUSTOM_ENV = {
|
|
56
|
+
id: "gid://shopify/HydrogenStorefrontEnvironment/3",
|
|
57
|
+
createdAt: "2024-01-01",
|
|
58
|
+
branch: "staging",
|
|
59
|
+
name: "Staging",
|
|
60
|
+
type: "CUSTOM",
|
|
61
|
+
url: "custom.com"
|
|
62
|
+
};
|
|
63
|
+
const outputMock = mockAndCaptureOutput();
|
|
64
|
+
const processExit = vi.spyOn(process, "exit");
|
|
65
|
+
describe("pushVariables", () => {
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
processExit.mockImplementation(() => {
|
|
68
|
+
throw "mockExit";
|
|
69
|
+
});
|
|
70
|
+
vi.mocked(login).mockResolvedValue({
|
|
71
|
+
session: ADMIN_SESSION,
|
|
72
|
+
config: SHOPIFY_CONFIG
|
|
73
|
+
});
|
|
74
|
+
vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
|
|
75
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
76
|
+
environmentVariables: [
|
|
77
|
+
{
|
|
78
|
+
id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
|
|
79
|
+
key: "PUBLIC_API_TOKEN",
|
|
80
|
+
value: "abc123",
|
|
81
|
+
readOnly: true,
|
|
82
|
+
isSecret: false
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/2",
|
|
86
|
+
key: "PRIVATE_API_TOKEN",
|
|
87
|
+
value: "",
|
|
88
|
+
readOnly: true,
|
|
89
|
+
isSecret: true
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
});
|
|
93
|
+
vi.mocked(getStorefrontEnvironments).mockResolvedValue({
|
|
94
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
95
|
+
productionUrl: "prod.com",
|
|
96
|
+
environments: [PRODUCTION_ENV, PREVIEW_ENV, CUSTOM_ENV]
|
|
97
|
+
});
|
|
98
|
+
vi.mocked(pushStorefrontEnvVariables).mockResolvedValue({
|
|
99
|
+
userErrors: []
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
outputMock.clear();
|
|
104
|
+
vi.clearAllMocks();
|
|
105
|
+
vi.resetAllMocks();
|
|
106
|
+
});
|
|
107
|
+
it("calls getStorefrontEnvironments", async () => {
|
|
108
|
+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
|
|
109
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
110
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
111
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
112
|
+
await expect(
|
|
113
|
+
runEnvPush({ path: tmpDir, env: "Preview" })
|
|
114
|
+
).resolves.not.toThrow();
|
|
115
|
+
expect(getStorefrontEnvironments).toHaveBeenCalledWith(
|
|
116
|
+
ADMIN_SESSION,
|
|
117
|
+
SHOPIFY_CONFIG.storefront.id
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
it("errors if no environments data", async () => {
|
|
122
|
+
vi.mocked(getStorefrontEnvironments).mockResolvedValue({
|
|
123
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
124
|
+
productionUrl: "prod.com",
|
|
125
|
+
environments: []
|
|
126
|
+
});
|
|
127
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
128
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
129
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
130
|
+
await expect(
|
|
131
|
+
runEnvPush({ path: tmpDir, env: "Preview" })
|
|
132
|
+
).rejects.toThrowError("No environments found");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
it("prompts the user to select an environment", async () => {
|
|
136
|
+
vi.mocked(renderSelectPrompt).mockResolvedValue(PREVIEW_ENV.id);
|
|
137
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
138
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
139
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
140
|
+
await expect(runEnvPush({ path: tmpDir })).resolves.not.toThrow();
|
|
141
|
+
expect(renderSelectPrompt).toHaveBeenCalledWith({
|
|
142
|
+
message: "Select a set of environment variables to overwrite:",
|
|
143
|
+
choices: [
|
|
144
|
+
expect.objectContaining({ label: expect.stringContaining("Preview") }),
|
|
145
|
+
expect.objectContaining({ label: expect.stringContaining("Staging") }),
|
|
146
|
+
expect.objectContaining({
|
|
147
|
+
label: expect.stringContaining("Production")
|
|
148
|
+
})
|
|
149
|
+
]
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe("when an environment is passed", () => {
|
|
154
|
+
it("errors when the environment does not match graphql", async () => {
|
|
155
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
156
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
157
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
158
|
+
await expect(
|
|
159
|
+
runEnvPush({ path: tmpDir, env: "Something random" })
|
|
160
|
+
).rejects.toThrowError("Environment not found");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
it("prompts the user if there are multiple matches", async () => {
|
|
164
|
+
vi.mocked(renderSelectPrompt).mockResolvedValue(PREVIEW_ENV.id);
|
|
165
|
+
vi.mocked(getStorefrontEnvironments).mockResolvedValue({
|
|
166
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
167
|
+
productionUrl: "prod.com",
|
|
168
|
+
environments: [
|
|
169
|
+
PRODUCTION_ENV,
|
|
170
|
+
PREVIEW_ENV,
|
|
171
|
+
{ ...CUSTOM_ENV, name: "Preview" }
|
|
172
|
+
]
|
|
173
|
+
});
|
|
174
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
175
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
176
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
177
|
+
await expect(
|
|
178
|
+
runEnvPush({ path: tmpDir, env: "Preview" })
|
|
179
|
+
).resolves.not.toThrow();
|
|
180
|
+
expect(renderSelectPrompt).toHaveBeenCalledWith({
|
|
181
|
+
message: "There were multiple environments found with the name Preview:",
|
|
182
|
+
choices: expect.any(Array)
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
it("exits if variables are identical", async () => {
|
|
188
|
+
vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
|
|
189
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
190
|
+
environmentVariables: [
|
|
191
|
+
{
|
|
192
|
+
id: "1",
|
|
193
|
+
key: "EXISTING_TOKEN",
|
|
194
|
+
value: "1",
|
|
195
|
+
isSecret: false,
|
|
196
|
+
readOnly: false
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "2",
|
|
200
|
+
key: "SECOND_TOKEN",
|
|
201
|
+
value: "2",
|
|
202
|
+
isSecret: false,
|
|
203
|
+
readOnly: false
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
});
|
|
207
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
208
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
209
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
210
|
+
await expect(
|
|
211
|
+
runEnvPush({ path: tmpDir, env: "Preview" })
|
|
212
|
+
).resolves.not.toThrow();
|
|
213
|
+
expect(outputMock.info()).toMatch(
|
|
214
|
+
/No changes to your environment variables/
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
it("renders a diff when a variable is updated", async () => {
|
|
219
|
+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
|
|
220
|
+
vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
|
|
221
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
222
|
+
environmentVariables: [
|
|
223
|
+
{
|
|
224
|
+
id: "1",
|
|
225
|
+
key: "EXISTING_TOKEN",
|
|
226
|
+
value: "1",
|
|
227
|
+
isSecret: false,
|
|
228
|
+
readOnly: false
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "2",
|
|
232
|
+
key: "SECOND_TOKEN",
|
|
233
|
+
value: "updated value",
|
|
234
|
+
isSecret: false,
|
|
235
|
+
readOnly: false
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
});
|
|
239
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
240
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
241
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
242
|
+
await expect(
|
|
243
|
+
runEnvPush({ path: tmpDir, env: "Preview" })
|
|
244
|
+
).resolves.not.toThrow();
|
|
245
|
+
expect(renderConfirmationPrompt).toHaveBeenCalled();
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
it("ignores comparison against secrets", async () => {
|
|
249
|
+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
|
|
250
|
+
vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
|
|
251
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
252
|
+
environmentVariables: [
|
|
253
|
+
{
|
|
254
|
+
id: "1",
|
|
255
|
+
key: "EXISTING_TOKEN",
|
|
256
|
+
value: "1",
|
|
257
|
+
isSecret: false,
|
|
258
|
+
readOnly: false
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "2",
|
|
262
|
+
key: "SECOND_TOKEN",
|
|
263
|
+
value: "updated value",
|
|
264
|
+
isSecret: true,
|
|
265
|
+
readOnly: false
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
270
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
271
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
272
|
+
await expect(
|
|
273
|
+
runEnvPush({ path: tmpDir, env: "Preview" })
|
|
274
|
+
).resolves.not.toThrow();
|
|
275
|
+
});
|
|
276
|
+
expect(outputMock.info()).toMatch(
|
|
277
|
+
/No changes to your environment variables/
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
it("ignores comparison against read only variables", async () => {
|
|
281
|
+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
|
|
282
|
+
vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
|
|
283
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
284
|
+
environmentVariables: [
|
|
285
|
+
{
|
|
286
|
+
id: "1",
|
|
287
|
+
key: "EXISTING_TOKEN",
|
|
288
|
+
value: "1",
|
|
289
|
+
isSecret: false,
|
|
290
|
+
readOnly: false
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: "2",
|
|
294
|
+
key: "SECOND_TOKEN",
|
|
295
|
+
value: "updated value",
|
|
296
|
+
isSecret: false,
|
|
297
|
+
readOnly: true
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
});
|
|
301
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
302
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
303
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
304
|
+
await expect(
|
|
305
|
+
runEnvPush({ path: tmpDir, env: "Preview" })
|
|
306
|
+
).resolves.not.toThrow();
|
|
307
|
+
expect(outputMock.info()).toMatch(
|
|
308
|
+
/No changes to your environment variables/
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
it("exits when diff is not confirmed", async () => {
|
|
313
|
+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(false);
|
|
314
|
+
vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
|
|
315
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
316
|
+
environmentVariables: [
|
|
317
|
+
{
|
|
318
|
+
id: "1",
|
|
319
|
+
key: "EXISTING_TOKEN",
|
|
320
|
+
value: "1",
|
|
321
|
+
isSecret: false,
|
|
322
|
+
readOnly: false
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: "2",
|
|
326
|
+
key: "SECOND_TOKEN",
|
|
327
|
+
value: "updated value",
|
|
328
|
+
isSecret: false,
|
|
329
|
+
readOnly: true
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
});
|
|
333
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
334
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
335
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=2");
|
|
336
|
+
await expect(
|
|
337
|
+
runEnvPush({ path: tmpDir, env: "Preview" })
|
|
338
|
+
).resolves.not.toThrow();
|
|
339
|
+
expect(pushStorefrontEnvVariables).not.toHaveBeenCalled();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
it("calls pushStorefrontEnvVariables when diff is confirmed", async () => {
|
|
343
|
+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
|
|
344
|
+
vi.mocked(getStorefrontEnvVariables).mockResolvedValue({
|
|
345
|
+
id: SHOPIFY_CONFIG.storefront.id,
|
|
346
|
+
environmentVariables: [
|
|
347
|
+
{
|
|
348
|
+
id: "1",
|
|
349
|
+
key: "EXISTING_TOKEN",
|
|
350
|
+
value: "1",
|
|
351
|
+
isSecret: false,
|
|
352
|
+
readOnly: false
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: "2",
|
|
356
|
+
key: "SECOND_TOKEN",
|
|
357
|
+
value: "2",
|
|
358
|
+
isSecret: false,
|
|
359
|
+
readOnly: false
|
|
360
|
+
}
|
|
361
|
+
]
|
|
362
|
+
});
|
|
363
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
364
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
365
|
+
await writeFile(filePath, "EXISTING_TOKEN=1\nSECOND_TOKEN=NEW_VALUE");
|
|
366
|
+
await expect(
|
|
367
|
+
runEnvPush({ path: tmpDir, env: "Preview" })
|
|
368
|
+
).resolves.not.toThrow();
|
|
369
|
+
expect(pushStorefrontEnvVariables).toHaveBeenCalledWith(
|
|
370
|
+
{ storeFqdn: "my-shop", token: "abc123" },
|
|
371
|
+
"gid://shopify/HydrogenStorefront/2",
|
|
372
|
+
"gid://shopify/HydrogenStorefrontEnvironment/2",
|
|
373
|
+
[
|
|
374
|
+
{ key: "EXISTING_TOKEN", value: "1" },
|
|
375
|
+
{ key: "SECOND_TOKEN", value: "NEW_VALUE" }
|
|
376
|
+
]
|
|
377
|
+
);
|
|
378
|
+
expect(outputMock.info()).toMatch(
|
|
379
|
+
/Environment variables push to Preview was successful/
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -18,8 +18,12 @@ class GenerateRoute extends Command {
|
|
|
18
18
|
description: "Generate TypeScript files",
|
|
19
19
|
env: "SHOPIFY_HYDROGEN_FLAG_TYPESCRIPT"
|
|
20
20
|
}),
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
"locale-param": Flags.string({
|
|
22
|
+
description: "The param name in Remix routes for the i18n locale, if any. Example: `locale` becomes ($locale).",
|
|
23
|
+
env: "SHOPIFY_HYDROGEN_FLAG_ADAPTER"
|
|
24
|
+
}),
|
|
25
|
+
...commonFlags.force,
|
|
26
|
+
...commonFlags.path
|
|
23
27
|
};
|
|
24
28
|
static hidden;
|
|
25
29
|
static args = {
|
|
@@ -40,7 +44,8 @@ class GenerateRoute extends Command {
|
|
|
40
44
|
await runGenerate({
|
|
41
45
|
...flags,
|
|
42
46
|
directory,
|
|
43
|
-
routeName
|
|
47
|
+
routeName,
|
|
48
|
+
localePrefix: flags["locale-param"]
|
|
44
49
|
});
|
|
45
50
|
}
|
|
46
51
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
|
|
2
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
3
|
+
|
|
4
|
+
declare const GENERATOR_SETUP_ASSETS_SUB_DIRS: readonly ["tailwind", "css-modules", "vanilla-extract", "postcss", "vite"];
|
|
5
|
+
type AssetDir = (typeof GENERATOR_SETUP_ASSETS_SUB_DIRS)[number];
|
|
6
|
+
|
|
7
|
+
type CssStrategy = Exclude<AssetDir, 'vite'>;
|
|
8
|
+
|
|
9
|
+
declare const I18N_CHOICES: readonly ["subfolders", "domains", "subdomains", "none"];
|
|
10
|
+
type I18nChoice = (typeof I18N_CHOICES)[number];
|
|
11
|
+
|
|
12
|
+
declare const STYLING_CHOICES: readonly [...CssStrategy[], "none"];
|
|
13
|
+
type StylingChoice = (typeof STYLING_CHOICES)[number];
|
|
14
|
+
|
|
15
|
+
type InitOptions = {
|
|
16
|
+
path?: string;
|
|
17
|
+
template?: string;
|
|
18
|
+
language?: Language;
|
|
19
|
+
mockShop?: boolean;
|
|
20
|
+
styling?: StylingChoice;
|
|
21
|
+
i18n?: I18nChoice;
|
|
22
|
+
token?: string;
|
|
23
|
+
force?: boolean;
|
|
24
|
+
routes?: boolean;
|
|
25
|
+
shortcut?: boolean;
|
|
26
|
+
installDeps?: boolean;
|
|
27
|
+
git?: boolean;
|
|
28
|
+
};
|
|
29
|
+
declare const LANGUAGES: {
|
|
30
|
+
readonly js: "JavaScript";
|
|
31
|
+
readonly ts: "TypeScript";
|
|
32
|
+
};
|
|
33
|
+
type Language = keyof typeof LANGUAGES;
|
|
34
|
+
|
|
35
|
+
declare class Init extends Command {
|
|
36
|
+
static description: string;
|
|
37
|
+
static flags: {
|
|
38
|
+
routes: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
39
|
+
git: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
40
|
+
shortcut: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
41
|
+
markets: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
42
|
+
styling: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
43
|
+
'mock-shop': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
44
|
+
'install-deps': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
45
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
46
|
+
language: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
47
|
+
template: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
48
|
+
force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
49
|
+
};
|
|
50
|
+
run(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
declare function runInit(options?: InitOptions): Promise<{
|
|
53
|
+
language?: "js" | "ts" | undefined;
|
|
54
|
+
packageManager: "npm" | "pnpm" | "yarn" | "bun" | "unknown";
|
|
55
|
+
cssStrategy?: CssStrategy | undefined;
|
|
56
|
+
cliCommand: "h2" | "pnpm shopify hydrogen" | "yarn shopify hydrogen" | "bun shopify hydrogen" | "npx shopify hydrogen";
|
|
57
|
+
depsInstalled: boolean;
|
|
58
|
+
depsError?: Error | undefined;
|
|
59
|
+
i18n?: "subfolders" | "domains" | "subdomains" | undefined;
|
|
60
|
+
i18nError?: Error | undefined;
|
|
61
|
+
routes?: Record<string, string | string[]> | undefined;
|
|
62
|
+
routesError?: Error | undefined;
|
|
63
|
+
location: string;
|
|
64
|
+
name: string;
|
|
65
|
+
directory: string;
|
|
66
|
+
storefrontTitle?: string | undefined;
|
|
67
|
+
} | undefined>;
|
|
68
|
+
|
|
69
|
+
export { Init as default, runInit };
|
|
@@ -16,7 +16,7 @@ const FLAG_MAP = { f: "force" };
|
|
|
16
16
|
class Init extends Command {
|
|
17
17
|
static description = "Creates a new Hydrogen storefront.";
|
|
18
18
|
static flags = {
|
|
19
|
-
|
|
19
|
+
...commonFlags.force,
|
|
20
20
|
path: Flags.string({
|
|
21
21
|
description: "The path to the directory of the new Hydrogen storefront.",
|
|
22
22
|
env: "SHOPIFY_HYDROGEN_FLAG_PATH"
|
|
@@ -30,15 +30,15 @@ class Init extends Command {
|
|
|
30
30
|
description: "Scaffolds project based on an existing template or example from the Hydrogen repository.",
|
|
31
31
|
env: "SHOPIFY_HYDROGEN_FLAG_TEMPLATE"
|
|
32
32
|
}),
|
|
33
|
-
|
|
33
|
+
...commonFlags.installDeps,
|
|
34
34
|
"mock-shop": Flags.boolean({
|
|
35
35
|
description: "Use mock.shop as the data source for the storefront.",
|
|
36
36
|
default: false,
|
|
37
37
|
env: "SHOPIFY_HYDROGEN_FLAG_MOCK_DATA"
|
|
38
38
|
}),
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
...commonFlags.styling,
|
|
40
|
+
...commonFlags.markets,
|
|
41
|
+
...commonFlags.shortcut,
|
|
42
42
|
routes: Flags.boolean({
|
|
43
43
|
description: "Generate routes for all pages.",
|
|
44
44
|
env: "SHOPIFY_HYDROGEN_FLAG_ROUTES",
|