@shopify/cli-hydrogen 4.1.1 → 4.2.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/codegen-unstable.d.ts +14 -0
- package/dist/commands/hydrogen/codegen-unstable.js +64 -0
- package/dist/commands/hydrogen/dev.d.ts +5 -0
- package/dist/commands/hydrogen/dev.js +48 -6
- package/dist/commands/hydrogen/env/list.d.ts +19 -0
- package/dist/commands/hydrogen/env/list.js +96 -0
- package/dist/commands/hydrogen/env/list.test.d.ts +1 -0
- package/dist/commands/hydrogen/env/list.test.js +151 -0
- package/dist/commands/hydrogen/env/pull.d.ts +23 -0
- package/dist/commands/hydrogen/env/pull.js +68 -0
- package/dist/commands/hydrogen/env/pull.test.d.ts +1 -0
- package/dist/commands/hydrogen/env/pull.test.js +112 -0
- package/dist/commands/hydrogen/init.js +3 -1
- package/dist/commands/hydrogen/link.d.ts +24 -0
- package/dist/commands/hydrogen/link.js +102 -0
- package/dist/commands/hydrogen/link.test.d.ts +1 -0
- package/dist/commands/hydrogen/link.test.js +137 -0
- package/dist/commands/hydrogen/list.d.ts +21 -0
- package/dist/commands/hydrogen/list.js +83 -0
- package/dist/commands/hydrogen/list.test.d.ts +1 -0
- package/dist/commands/hydrogen/list.test.js +116 -0
- package/dist/commands/hydrogen/unlink.d.ts +17 -0
- package/dist/commands/hydrogen/unlink.js +29 -0
- package/dist/commands/hydrogen/unlink.test.d.ts +1 -0
- package/dist/commands/hydrogen/unlink.test.js +36 -0
- package/dist/generator-templates/routes/[robots.txt].tsx +111 -19
- package/dist/generator-templates/routes/collections/index.tsx +102 -0
- package/dist/lib/admin-session.d.ts +5 -0
- package/dist/lib/admin-session.js +16 -0
- package/dist/lib/admin-session.test.d.ts +1 -0
- package/dist/lib/admin-session.test.js +27 -0
- package/dist/lib/admin-urls.d.ts +8 -0
- package/dist/lib/admin-urls.js +18 -0
- package/dist/lib/codegen.d.ts +25 -0
- package/dist/lib/codegen.js +128 -0
- package/dist/lib/colors.d.ts +3 -0
- package/dist/lib/colors.js +4 -1
- package/dist/lib/combined-environment-variables.d.ts +8 -0
- package/dist/lib/combined-environment-variables.js +74 -0
- package/dist/lib/combined-environment-variables.test.d.ts +1 -0
- package/dist/lib/combined-environment-variables.test.js +111 -0
- package/dist/lib/flags.d.ts +2 -0
- package/dist/lib/flags.js +13 -0
- package/dist/lib/graphql/admin/link-storefront.d.ts +11 -0
- package/dist/lib/graphql/admin/link-storefront.js +11 -0
- package/dist/lib/graphql/admin/list-environments.d.ts +20 -0
- package/dist/lib/graphql/admin/list-environments.js +18 -0
- package/dist/lib/graphql/admin/list-storefronts.d.ts +17 -0
- package/dist/lib/graphql/admin/list-storefronts.js +16 -0
- package/dist/lib/graphql/admin/pull-variables.d.ts +16 -0
- package/dist/lib/graphql/admin/pull-variables.js +15 -0
- package/dist/lib/graphql.d.ts +21 -0
- package/dist/lib/graphql.js +18 -0
- package/dist/lib/graphql.test.d.ts +1 -0
- package/dist/lib/graphql.test.js +15 -0
- package/dist/lib/mini-oxygen.d.ts +4 -1
- package/dist/lib/mini-oxygen.js +7 -3
- package/dist/lib/missing-storefronts.d.ts +5 -0
- package/dist/lib/missing-storefronts.js +18 -0
- package/dist/lib/pull-environment-variables.d.ts +19 -0
- package/dist/lib/pull-environment-variables.js +67 -0
- package/dist/lib/pull-environment-variables.test.d.ts +1 -0
- package/dist/lib/pull-environment-variables.test.js +174 -0
- package/dist/lib/render-errors.d.ts +16 -0
- package/dist/lib/render-errors.js +37 -0
- package/dist/lib/shop.d.ts +7 -0
- package/dist/lib/shop.js +32 -0
- package/dist/lib/shop.test.d.ts +1 -0
- package/dist/lib/shop.test.js +78 -0
- package/dist/lib/shopify-config.d.ts +35 -0
- package/dist/lib/shopify-config.js +86 -0
- package/dist/lib/shopify-config.test.d.ts +1 -0
- package/dist/lib/shopify-config.test.js +209 -0
- package/oclif.manifest.json +1 -1
- package/package.json +7 -5
|
@@ -0,0 +1,14 @@
|
|
|
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 class Codegen extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static flags: {
|
|
7
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
8
|
+
"codegen-config-path": _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
9
|
+
watch: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { Codegen as default };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
3
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
4
|
+
import { renderSuccess } from '@shopify/cli-kit/node/ui';
|
|
5
|
+
import { Flags } from '@oclif/core';
|
|
6
|
+
import { getProjectPaths, getRemixConfig } from '../../lib/config.js';
|
|
7
|
+
import { commonFlags, flagsToCamelObject } from '../../lib/flags.js';
|
|
8
|
+
import { patchGqlPluck, generateTypes, normalizeCodegenError } from '../../lib/codegen.js';
|
|
9
|
+
|
|
10
|
+
class Codegen extends Command {
|
|
11
|
+
static description = "Generate types for the Storefront API queries found in your project.";
|
|
12
|
+
static flags = {
|
|
13
|
+
path: commonFlags.path,
|
|
14
|
+
["codegen-config-path"]: Flags.string({
|
|
15
|
+
description: "Specify a path to a codegen configuration file. Defaults to `<root>/codegen.ts` if it exists.",
|
|
16
|
+
required: false
|
|
17
|
+
}),
|
|
18
|
+
watch: Flags.boolean({
|
|
19
|
+
description: "Watch the project for changes to update types on file save.",
|
|
20
|
+
required: false,
|
|
21
|
+
default: false
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { flags } = await this.parse(Codegen);
|
|
26
|
+
const directory = flags.path ? path.resolve(flags.path) : process.cwd();
|
|
27
|
+
await runCodegen({
|
|
28
|
+
...flagsToCamelObject(flags),
|
|
29
|
+
path: directory
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function runCodegen({
|
|
34
|
+
path: appPath,
|
|
35
|
+
codegenConfigPath,
|
|
36
|
+
watch
|
|
37
|
+
}) {
|
|
38
|
+
const { root } = getProjectPaths(appPath);
|
|
39
|
+
const remixConfig = await getRemixConfig(root);
|
|
40
|
+
await patchGqlPluck();
|
|
41
|
+
try {
|
|
42
|
+
const generatedFiles = await generateTypes({
|
|
43
|
+
...remixConfig,
|
|
44
|
+
configFilePath: codegenConfigPath,
|
|
45
|
+
watch
|
|
46
|
+
});
|
|
47
|
+
if (!watch) {
|
|
48
|
+
console.log("");
|
|
49
|
+
renderSuccess({
|
|
50
|
+
headline: "Generated types for GraphQL:",
|
|
51
|
+
body: generatedFiles.map((file) => `- ${file}`).join("\n")
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const { message, details } = normalizeCodegenError(
|
|
56
|
+
error.message,
|
|
57
|
+
remixConfig.rootDirectory
|
|
58
|
+
);
|
|
59
|
+
console.log("");
|
|
60
|
+
throw new AbortError(message, details);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { Codegen as default };
|
|
@@ -6,8 +6,13 @@ declare class Dev extends Command {
|
|
|
6
6
|
static flags: {
|
|
7
7
|
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
8
8
|
port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
9
|
+
"codegen-unstable": _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
10
|
+
"codegen-config-path": _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
9
11
|
"disable-virtual-routes": _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
12
|
+
shop: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
13
|
+
debug: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
10
14
|
host: _oclif_core_lib_interfaces_parser_js.OptionFlag<unknown, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
15
|
+
"env-branch": _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
11
16
|
};
|
|
12
17
|
run(): Promise<void>;
|
|
13
18
|
}
|
|
@@ -11,6 +11,9 @@ import { Flags } from '@oclif/core';
|
|
|
11
11
|
import { startMiniOxygen } from '../../lib/mini-oxygen.js';
|
|
12
12
|
import { checkHydrogenVersion } from '../../lib/check-version.js';
|
|
13
13
|
import { addVirtualRoutes } from '../../lib/virtual-routes.js';
|
|
14
|
+
import { spawnCodegenProcess } from '../../lib/codegen.js';
|
|
15
|
+
import { combinedEnvironmentVariables } from '../../lib/combined-environment-variables.js';
|
|
16
|
+
import { getConfig } from '../../lib/shopify-config.js';
|
|
14
17
|
|
|
15
18
|
const LOG_INITIAL_BUILD = "\n\u{1F3C1} Initial build";
|
|
16
19
|
const LOG_REBUILDING = "\u{1F9F1} Rebuilding...";
|
|
@@ -20,27 +23,55 @@ class Dev extends Command {
|
|
|
20
23
|
static flags = {
|
|
21
24
|
path: commonFlags.path,
|
|
22
25
|
port: commonFlags.port,
|
|
26
|
+
["codegen-unstable"]: Flags.boolean({
|
|
27
|
+
description: "Generate types for the Storefront API queries found in your project. It updates the types on file save.",
|
|
28
|
+
required: false,
|
|
29
|
+
default: false
|
|
30
|
+
}),
|
|
31
|
+
["codegen-config-path"]: Flags.string({
|
|
32
|
+
description: "Specify a path to a codegen configuration file. Defaults to `<root>/codegen.ts` if it exists.",
|
|
33
|
+
required: false,
|
|
34
|
+
dependsOn: ["codegen-unstable"]
|
|
35
|
+
}),
|
|
23
36
|
["disable-virtual-routes"]: Flags.boolean({
|
|
24
|
-
description: "Disable rendering fallback routes when a route file doesn't exist",
|
|
37
|
+
description: "Disable rendering fallback routes when a route file doesn't exist.",
|
|
25
38
|
env: "SHOPIFY_HYDROGEN_FLAG_DISABLE_VIRTUAL_ROUTES",
|
|
26
39
|
default: false
|
|
27
40
|
}),
|
|
28
|
-
|
|
41
|
+
shop: commonFlags.shop,
|
|
42
|
+
debug: Flags.boolean({
|
|
43
|
+
description: "Attaches a Node inspector",
|
|
44
|
+
env: "SHOPIFY_HYDROGEN_FLAG_DEBUG",
|
|
45
|
+
default: false
|
|
46
|
+
}),
|
|
47
|
+
host: deprecated("--host")(),
|
|
48
|
+
["env-branch"]: commonFlags["env-branch"]
|
|
29
49
|
};
|
|
30
50
|
async run() {
|
|
31
51
|
const { flags } = await this.parse(Dev);
|
|
32
52
|
const directory = flags.path ? path.resolve(flags.path) : process.cwd();
|
|
33
|
-
await runDev({
|
|
53
|
+
await runDev({
|
|
54
|
+
...flagsToCamelObject(flags),
|
|
55
|
+
codegen: flags["codegen-unstable"],
|
|
56
|
+
path: directory
|
|
57
|
+
});
|
|
34
58
|
}
|
|
35
59
|
}
|
|
36
60
|
async function runDev({
|
|
37
61
|
port,
|
|
38
62
|
path: appPath,
|
|
39
|
-
|
|
63
|
+
codegen = false,
|
|
64
|
+
codegenConfigPath,
|
|
65
|
+
disableVirtualRoutes,
|
|
66
|
+
shop,
|
|
67
|
+
envBranch,
|
|
68
|
+
debug = false
|
|
40
69
|
}) {
|
|
41
70
|
if (!process.env.NODE_ENV)
|
|
42
71
|
process.env.NODE_ENV = "development";
|
|
43
72
|
muteDevLogs();
|
|
73
|
+
if (debug)
|
|
74
|
+
(await import('node:inspector')).open();
|
|
44
75
|
console.time(LOG_INITIAL_BUILD);
|
|
45
76
|
const { root, publicPath, buildPathClient, buildPathWorkerFile } = getProjectPaths(appPath);
|
|
46
77
|
const checkingHydrogenVersion = checkHydrogenVersion(root);
|
|
@@ -54,6 +85,12 @@ async function runDev({
|
|
|
54
85
|
return [fileRelative, path.resolve(root, fileRelative)];
|
|
55
86
|
};
|
|
56
87
|
const serverBundleExists = () => fileExists(buildPathWorkerFile);
|
|
88
|
+
const hasLinkedStorefront = !!(await getConfig(root))?.storefront?.id;
|
|
89
|
+
const environmentVariables = hasLinkedStorefront ? await combinedEnvironmentVariables({
|
|
90
|
+
root,
|
|
91
|
+
shop,
|
|
92
|
+
envBranch
|
|
93
|
+
}) : void 0;
|
|
57
94
|
let miniOxygenStarted = false;
|
|
58
95
|
async function safeStartMiniOxygen() {
|
|
59
96
|
if (miniOxygenStarted)
|
|
@@ -63,7 +100,8 @@ async function runDev({
|
|
|
63
100
|
port,
|
|
64
101
|
watch: true,
|
|
65
102
|
buildPathWorkerFile,
|
|
66
|
-
buildPathClient
|
|
103
|
+
buildPathClient,
|
|
104
|
+
environmentVariables
|
|
67
105
|
});
|
|
68
106
|
miniOxygenStarted = true;
|
|
69
107
|
const showUpgrade = await checkingHydrogenVersion;
|
|
@@ -71,7 +109,11 @@ async function runDev({
|
|
|
71
109
|
showUpgrade();
|
|
72
110
|
}
|
|
73
111
|
const { watch } = await import('@remix-run/dev/dist/compiler/watch.js');
|
|
74
|
-
|
|
112
|
+
const remixConfig = await reloadConfig();
|
|
113
|
+
if (codegen) {
|
|
114
|
+
spawnCodegenProcess({ ...remixConfig, configFilePath: codegenConfigPath });
|
|
115
|
+
}
|
|
116
|
+
await watch(remixConfig, {
|
|
75
117
|
reloadConfig,
|
|
76
118
|
mode: process.env.NODE_ENV,
|
|
77
119
|
async onInitialBuild() {
|
|
@@ -0,0 +1,19 @@
|
|
|
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 class List extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static hidden: boolean;
|
|
7
|
+
static flags: {
|
|
8
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
9
|
+
shop: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
interface Flags {
|
|
14
|
+
path?: string;
|
|
15
|
+
shop?: string;
|
|
16
|
+
}
|
|
17
|
+
declare function listEnvironments({ path, shop: flagShop }: Flags): Promise<void>;
|
|
18
|
+
|
|
19
|
+
export { List as default, listEnvironments };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
|
+
import { renderConfirmationPrompt, renderTable } from '@shopify/cli-kit/node/ui';
|
|
3
|
+
import { outputContent, outputToken, outputNewline } from '@shopify/cli-kit/node/output';
|
|
4
|
+
import { linkStorefront } from '../link.js';
|
|
5
|
+
import { adminRequest } from '../../../lib/graphql.js';
|
|
6
|
+
import { commonFlags } from '../../../lib/flags.js';
|
|
7
|
+
import { getHydrogenShop } from '../../../lib/shop.js';
|
|
8
|
+
import { getAdminSession } from '../../../lib/admin-session.js';
|
|
9
|
+
import { ListEnvironmentsQuery } from '../../../lib/graphql/admin/list-environments.js';
|
|
10
|
+
import { getConfig } from '../../../lib/shopify-config.js';
|
|
11
|
+
import { renderMissingLink, renderMissingStorefront } from '../../../lib/render-errors.js';
|
|
12
|
+
|
|
13
|
+
class List extends Command {
|
|
14
|
+
static description = "List the environments on your Hydrogen storefront.";
|
|
15
|
+
static hidden = true;
|
|
16
|
+
static flags = {
|
|
17
|
+
path: commonFlags.path,
|
|
18
|
+
shop: commonFlags.shop
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { flags } = await this.parse(List);
|
|
22
|
+
await listEnvironments(flags);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function listEnvironments({ path, shop: flagShop }) {
|
|
26
|
+
const shop = await getHydrogenShop({ path, shop: flagShop });
|
|
27
|
+
const adminSession = await getAdminSession(shop);
|
|
28
|
+
const actualPath = path ?? process.cwd();
|
|
29
|
+
let configStorefront = (await getConfig(actualPath)).storefront;
|
|
30
|
+
if (!configStorefront?.id) {
|
|
31
|
+
renderMissingLink({ adminSession });
|
|
32
|
+
const runLink = await renderConfirmationPrompt({
|
|
33
|
+
message: outputContent`Run ${outputToken.genericShellCommand(
|
|
34
|
+
`npx shopify hydrogen link`
|
|
35
|
+
)}?`.value
|
|
36
|
+
});
|
|
37
|
+
if (!runLink) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await linkStorefront({ path, shop: flagShop, silent: true });
|
|
41
|
+
}
|
|
42
|
+
configStorefront = (await getConfig(actualPath)).storefront;
|
|
43
|
+
if (!configStorefront) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const result = await adminRequest(
|
|
47
|
+
ListEnvironmentsQuery,
|
|
48
|
+
adminSession,
|
|
49
|
+
{
|
|
50
|
+
id: configStorefront.id
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
const hydrogenStorefront = result.hydrogenStorefront;
|
|
54
|
+
if (!hydrogenStorefront) {
|
|
55
|
+
renderMissingStorefront({ adminSession, storefront: configStorefront });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const previewEnvironmentIndex = hydrogenStorefront.environments.findIndex(
|
|
59
|
+
(env) => env.type === "PREVIEW"
|
|
60
|
+
);
|
|
61
|
+
const previewEnvironment = hydrogenStorefront.environments.splice(
|
|
62
|
+
previewEnvironmentIndex,
|
|
63
|
+
1
|
|
64
|
+
);
|
|
65
|
+
hydrogenStorefront.environments.push(previewEnvironment[0]);
|
|
66
|
+
const rows = hydrogenStorefront.environments.map(
|
|
67
|
+
({ branch, name, url, type }) => {
|
|
68
|
+
const environmentUrl = type === "PRODUCTION" ? hydrogenStorefront.productionUrl : url;
|
|
69
|
+
return {
|
|
70
|
+
name,
|
|
71
|
+
branch: branch ? branch : "-",
|
|
72
|
+
url: environmentUrl ? environmentUrl : "-"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
outputNewline();
|
|
77
|
+
renderTable({
|
|
78
|
+
rows,
|
|
79
|
+
columns: {
|
|
80
|
+
name: {
|
|
81
|
+
header: "Name",
|
|
82
|
+
color: "whiteBright"
|
|
83
|
+
},
|
|
84
|
+
branch: {
|
|
85
|
+
header: "Branch",
|
|
86
|
+
color: "yellow"
|
|
87
|
+
},
|
|
88
|
+
url: {
|
|
89
|
+
header: "URL",
|
|
90
|
+
color: "green"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { List as default, listEnvironments };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
|
|
2
|
+
import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
|
|
3
|
+
import { inTemporaryDirectory } from '@shopify/cli-kit/node/fs';
|
|
4
|
+
import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
|
|
5
|
+
import { ListEnvironmentsQuery } from '../../../lib/graphql/admin/list-environments.js';
|
|
6
|
+
import { getAdminSession } from '../../../lib/admin-session.js';
|
|
7
|
+
import { adminRequest } from '../../../lib/graphql.js';
|
|
8
|
+
import { getConfig } from '../../../lib/shopify-config.js';
|
|
9
|
+
import { renderMissingLink, renderMissingStorefront } from '../../../lib/render-errors.js';
|
|
10
|
+
import { linkStorefront } from '../link.js';
|
|
11
|
+
import { listEnvironments } from './list.js';
|
|
12
|
+
|
|
13
|
+
vi.mock("@shopify/cli-kit/node/ui", async () => {
|
|
14
|
+
const original = await vi.importActual("@shopify/cli-kit/node/ui");
|
|
15
|
+
return {
|
|
16
|
+
...original,
|
|
17
|
+
renderConfirmationPrompt: vi.fn()
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
vi.mock("../link.js");
|
|
21
|
+
vi.mock("../../../lib/admin-session.js");
|
|
22
|
+
vi.mock("../../../lib/shopify-config.js");
|
|
23
|
+
vi.mock("../../../lib/render-errors.js");
|
|
24
|
+
vi.mock("../../../lib/graphql.js", async () => {
|
|
25
|
+
const original = await vi.importActual("../../../lib/graphql.js");
|
|
26
|
+
return {
|
|
27
|
+
...original,
|
|
28
|
+
adminRequest: vi.fn()
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
vi.mock("../../../lib/shop.js", () => ({
|
|
32
|
+
getHydrogenShop: () => "my-shop"
|
|
33
|
+
}));
|
|
34
|
+
describe("listEnvironments", () => {
|
|
35
|
+
const ADMIN_SESSION = {
|
|
36
|
+
token: "abc123",
|
|
37
|
+
storeFqdn: "my-shop"
|
|
38
|
+
};
|
|
39
|
+
const PRODUCTION_ENVIRONMENT = {
|
|
40
|
+
id: "gid://shopify/HydrogenStorefrontEnvironment/1",
|
|
41
|
+
branch: "main",
|
|
42
|
+
type: "PRODUCTION",
|
|
43
|
+
name: "Production",
|
|
44
|
+
createdAt: "2023-02-16T22:35:42Z",
|
|
45
|
+
url: "https://oxygen-123.example.com"
|
|
46
|
+
};
|
|
47
|
+
const CUSTOM_ENVIRONMENT = {
|
|
48
|
+
id: "gid://shopify/HydrogenStorefrontEnvironment/3",
|
|
49
|
+
branch: "staging",
|
|
50
|
+
type: "CUSTOM",
|
|
51
|
+
name: "Staging",
|
|
52
|
+
createdAt: "2023-05-08T20:52:29Z",
|
|
53
|
+
url: "https://oxygen-456.example.com"
|
|
54
|
+
};
|
|
55
|
+
const PREVIEW_ENVIRONMENT = {
|
|
56
|
+
id: "gid://shopify/HydrogenStorefrontEnvironment/2",
|
|
57
|
+
branch: null,
|
|
58
|
+
type: "PREVIEW",
|
|
59
|
+
name: "Preview",
|
|
60
|
+
createdAt: "2023-02-16T22:35:42Z",
|
|
61
|
+
url: null
|
|
62
|
+
};
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
vi.mocked(getAdminSession).mockResolvedValue(ADMIN_SESSION);
|
|
65
|
+
vi.mocked(getConfig).mockResolvedValue({
|
|
66
|
+
storefront: {
|
|
67
|
+
id: "gid://shopify/HydrogenStorefront/1",
|
|
68
|
+
title: "Existing Link"
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
vi.mocked(adminRequest).mockResolvedValue({
|
|
72
|
+
hydrogenStorefront: {
|
|
73
|
+
id: "gid://shopify/HydrogenStorefront/1",
|
|
74
|
+
productionUrl: "https://example.com",
|
|
75
|
+
environments: [
|
|
76
|
+
PRODUCTION_ENVIRONMENT,
|
|
77
|
+
CUSTOM_ENVIRONMENT,
|
|
78
|
+
PREVIEW_ENVIRONMENT
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
vi.resetAllMocks();
|
|
85
|
+
mockAndCaptureOutput().clear();
|
|
86
|
+
});
|
|
87
|
+
it("makes a GraphQL call to fetch environment variables", async () => {
|
|
88
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
89
|
+
await listEnvironments({ path: tmpDir });
|
|
90
|
+
expect(adminRequest).toHaveBeenCalledWith(
|
|
91
|
+
ListEnvironmentsQuery,
|
|
92
|
+
ADMIN_SESSION,
|
|
93
|
+
{
|
|
94
|
+
id: "gid://shopify/HydrogenStorefront/1"
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it("lists the environments", async () => {
|
|
100
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
101
|
+
const output = mockAndCaptureOutput();
|
|
102
|
+
await listEnvironments({ path: tmpDir });
|
|
103
|
+
expect(output.info()).toMatch(
|
|
104
|
+
/Production\s*main\s*https:\/\/example\.com/
|
|
105
|
+
);
|
|
106
|
+
expect(output.info()).toMatch(
|
|
107
|
+
/Staging\s*staging\s*https:\/\/oxygen-456\.example\.com/
|
|
108
|
+
);
|
|
109
|
+
expect(output.info()).toMatch(/Preview\s*-\s*-/);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("when there is no linked storefront", () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
vi.mocked(getConfig).mockResolvedValue({
|
|
115
|
+
storefront: void 0
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
it("calls renderMissingLink", async () => {
|
|
119
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
120
|
+
await listEnvironments({ path: tmpDir });
|
|
121
|
+
expect(renderMissingLink).toHaveBeenCalledOnce();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
it("prompts the user to create a link", async () => {
|
|
125
|
+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
|
|
126
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
127
|
+
await listEnvironments({ path: tmpDir });
|
|
128
|
+
expect(renderConfirmationPrompt).toHaveBeenCalledWith({
|
|
129
|
+
message: expect.stringMatching(/Run .*npx shopify hydrogen link.*\?/)
|
|
130
|
+
});
|
|
131
|
+
expect(linkStorefront).toHaveBeenCalledWith({
|
|
132
|
+
path: tmpDir,
|
|
133
|
+
silent: true
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe("when there is no matching storefront in the shop", () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
vi.mocked(adminRequest).mockResolvedValue({
|
|
141
|
+
hydrogenStorefront: null
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
it("calls renderMissingStorefront", async () => {
|
|
145
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
146
|
+
await listEnvironments({ path: tmpDir });
|
|
147
|
+
expect(renderMissingStorefront).toHaveBeenCalledOnce();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
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 class Pull extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static hidden: boolean;
|
|
7
|
+
static flags: {
|
|
8
|
+
"env-branch": _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
9
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
10
|
+
shop: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
11
|
+
force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
interface Flags {
|
|
16
|
+
envBranch?: string;
|
|
17
|
+
force?: boolean;
|
|
18
|
+
path?: string;
|
|
19
|
+
shop?: string;
|
|
20
|
+
}
|
|
21
|
+
declare function pullVariables({ envBranch, force, path, shop: flagShop, }: Flags): Promise<void>;
|
|
22
|
+
|
|
23
|
+
export { Pull as default, pullVariables };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
|
+
import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
|
|
3
|
+
import { outputWarn, outputSuccess } from '@shopify/cli-kit/node/output';
|
|
4
|
+
import { fileExists, writeFile } from '@shopify/cli-kit/node/fs';
|
|
5
|
+
import { resolvePath } from '@shopify/cli-kit/node/path';
|
|
6
|
+
import { commonFlags, flagsToCamelObject } from '../../../lib/flags.js';
|
|
7
|
+
import { pullRemoteEnvironmentVariables } from '../../../lib/pull-environment-variables.js';
|
|
8
|
+
import { getConfig } from '../../../lib/shopify-config.js';
|
|
9
|
+
|
|
10
|
+
class Pull extends Command {
|
|
11
|
+
static description = "Populate your .env with variables from your Hydrogen storefront.";
|
|
12
|
+
static hidden = true;
|
|
13
|
+
static flags = {
|
|
14
|
+
["env-branch"]: commonFlags["env-branch"],
|
|
15
|
+
path: commonFlags.path,
|
|
16
|
+
shop: commonFlags.shop,
|
|
17
|
+
force: commonFlags.force
|
|
18
|
+
};
|
|
19
|
+
async run() {
|
|
20
|
+
const { flags } = await this.parse(Pull);
|
|
21
|
+
await pullVariables({ ...flagsToCamelObject(flags) });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function pullVariables({
|
|
25
|
+
envBranch,
|
|
26
|
+
force,
|
|
27
|
+
path,
|
|
28
|
+
shop: flagShop
|
|
29
|
+
}) {
|
|
30
|
+
const actualPath = path ?? process.cwd();
|
|
31
|
+
const environmentVariables = await pullRemoteEnvironmentVariables({
|
|
32
|
+
root: actualPath,
|
|
33
|
+
flagShop,
|
|
34
|
+
envBranch
|
|
35
|
+
});
|
|
36
|
+
if (!environmentVariables.length) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const dotEnvPath = resolvePath(actualPath, ".env");
|
|
40
|
+
if (await fileExists(dotEnvPath) && !force) {
|
|
41
|
+
const overwrite = await renderConfirmationPrompt({
|
|
42
|
+
message: "Warning: .env file already exists. Do you want to overwrite it?"
|
|
43
|
+
});
|
|
44
|
+
if (!overwrite) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let hasSecretVariables = false;
|
|
49
|
+
const contents = environmentVariables.map(({ key, value, isSecret }) => {
|
|
50
|
+
let line = `${key}="${value}"`;
|
|
51
|
+
if (isSecret) {
|
|
52
|
+
hasSecretVariables = true;
|
|
53
|
+
line = `# ${key} is marked as secret and its value is hidden
|
|
54
|
+
` + line;
|
|
55
|
+
}
|
|
56
|
+
return line;
|
|
57
|
+
}).join("\n") + "\n";
|
|
58
|
+
if (hasSecretVariables) {
|
|
59
|
+
const { storefront: configStorefront } = await getConfig(actualPath);
|
|
60
|
+
outputWarn(
|
|
61
|
+
`${configStorefront.title} contains environment variables marked as secret, so their values weren\u2019t pulled.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
await writeFile(dotEnvPath, contents);
|
|
65
|
+
outputSuccess("Updated .env");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { Pull as default, pullVariables };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
|
|
2
|
+
import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
|
|
3
|
+
import { inTemporaryDirectory, fileExists, readFile, writeFile } from '@shopify/cli-kit/node/fs';
|
|
4
|
+
import { joinPath } from '@shopify/cli-kit/node/path';
|
|
5
|
+
import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
|
|
6
|
+
import { getAdminSession } from '../../../lib/admin-session.js';
|
|
7
|
+
import { pullRemoteEnvironmentVariables } from '../../../lib/pull-environment-variables.js';
|
|
8
|
+
import { getConfig } from '../../../lib/shopify-config.js';
|
|
9
|
+
import { pullVariables } from './pull.js';
|
|
10
|
+
|
|
11
|
+
vi.mock("@shopify/cli-kit/node/ui", async () => {
|
|
12
|
+
const original = await vi.importActual("@shopify/cli-kit/node/ui");
|
|
13
|
+
return {
|
|
14
|
+
...original,
|
|
15
|
+
renderConfirmationPrompt: vi.fn()
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
vi.mock("../link.js");
|
|
19
|
+
vi.mock("../../../lib/admin-session.js");
|
|
20
|
+
vi.mock("../../../lib/shopify-config.js");
|
|
21
|
+
vi.mock("../../../lib/pull-environment-variables.js");
|
|
22
|
+
vi.mock("../../../lib/shop.js", () => ({
|
|
23
|
+
getHydrogenShop: () => "my-shop"
|
|
24
|
+
}));
|
|
25
|
+
describe("pullVariables", () => {
|
|
26
|
+
const ADMIN_SESSION = {
|
|
27
|
+
token: "abc123",
|
|
28
|
+
storeFqdn: "my-shop"
|
|
29
|
+
};
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
vi.mocked(getAdminSession).mockResolvedValue(ADMIN_SESSION);
|
|
32
|
+
vi.mocked(getConfig).mockResolvedValue({
|
|
33
|
+
storefront: {
|
|
34
|
+
id: "gid://shopify/HydrogenStorefront/2",
|
|
35
|
+
title: "Existing Link"
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
vi.mocked(pullRemoteEnvironmentVariables).mockResolvedValue([
|
|
39
|
+
{
|
|
40
|
+
id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
|
|
41
|
+
key: "PUBLIC_API_TOKEN",
|
|
42
|
+
value: "abc123",
|
|
43
|
+
isSecret: false
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/2",
|
|
47
|
+
key: "PRIVATE_API_TOKEN",
|
|
48
|
+
value: "",
|
|
49
|
+
isSecret: true
|
|
50
|
+
}
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.resetAllMocks();
|
|
55
|
+
mockAndCaptureOutput().clear();
|
|
56
|
+
});
|
|
57
|
+
it("calls pullRemoteEnvironmentVariables", async () => {
|
|
58
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
59
|
+
await pullVariables({ path: tmpDir, envBranch: "staging" });
|
|
60
|
+
expect(pullRemoteEnvironmentVariables).toHaveBeenCalledWith({
|
|
61
|
+
root: tmpDir,
|
|
62
|
+
envBranch: "staging"
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it("writes environment variables to a .env file", async () => {
|
|
67
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
68
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
69
|
+
expect(await fileExists(filePath)).toBeFalsy();
|
|
70
|
+
await pullVariables({ path: tmpDir });
|
|
71
|
+
expect(await readFile(filePath)).toStrictEqual(
|
|
72
|
+
'PUBLIC_API_TOKEN="abc123"\n# PRIVATE_API_TOKEN is marked as secret and its value is hidden\nPRIVATE_API_TOKEN=""\n'
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it("warns about secret environment variables", async () => {
|
|
77
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
78
|
+
const outputMock = mockAndCaptureOutput();
|
|
79
|
+
await pullVariables({ path: tmpDir });
|
|
80
|
+
expect(outputMock.warn()).toStrictEqual(
|
|
81
|
+
"Existing Link contains environment variables marked as secret, so their values weren\u2019t pulled."
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("when a .env file already exists", () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
|
|
88
|
+
});
|
|
89
|
+
it("prompts the user to confirm", async () => {
|
|
90
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
91
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
92
|
+
await writeFile(filePath, "EXISTING_TOKEN=1");
|
|
93
|
+
await pullVariables({ path: tmpDir });
|
|
94
|
+
expect(renderConfirmationPrompt).toHaveBeenCalledWith({
|
|
95
|
+
message: expect.stringMatching(
|
|
96
|
+
/Warning: \.env file already exists\. Do you want to overwrite it\?/
|
|
97
|
+
)
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("and --force is enabled", () => {
|
|
102
|
+
it("does not prompt the user to confirm", async () => {
|
|
103
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
104
|
+
const filePath = joinPath(tmpDir, ".env");
|
|
105
|
+
await writeFile(filePath, "EXISTING_TOKEN=1");
|
|
106
|
+
await pullVariables({ path: tmpDir, force: true });
|
|
107
|
+
expect(renderConfirmationPrompt).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|