@shopify/cli-hydrogen 4.0.8 → 4.1.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/README.md +9 -0
- package/dist/commands/hydrogen/build.d.ts +4 -1
- package/dist/commands/hydrogen/build.js +21 -17
- package/dist/commands/hydrogen/check.d.ts +3 -6
- package/dist/commands/hydrogen/check.js +10 -9
- package/dist/commands/hydrogen/dev.d.ts +3 -2
- package/dist/commands/hydrogen/dev.js +24 -22
- package/dist/commands/hydrogen/g.d.ts +10 -0
- package/dist/commands/hydrogen/g.js +17 -0
- package/dist/commands/hydrogen/generate/route.d.ts +7 -9
- package/dist/commands/hydrogen/generate/route.js +49 -47
- package/dist/commands/hydrogen/generate/route.test.js +48 -40
- package/dist/commands/hydrogen/generate/routes.d.ts +2 -2
- package/dist/commands/hydrogen/init.d.ts +3 -3
- package/dist/commands/hydrogen/init.js +76 -95
- package/dist/commands/hydrogen/init.test.js +126 -0
- package/dist/commands/hydrogen/preview.d.ts +2 -2
- package/dist/commands/hydrogen/preview.js +4 -4
- package/dist/commands/hydrogen/shortcut.d.ts +9 -0
- package/dist/commands/hydrogen/shortcut.js +74 -0
- package/dist/commands/hydrogen/shortcut.test.js +58 -0
- package/dist/generator-templates/routes/[robots.txt].tsx +35 -1
- package/dist/generator-templates/routes/[sitemap.xml].tsx +45 -10
- package/dist/generator-templates/routes/account/login.tsx +42 -13
- package/dist/generator-templates/routes/account/register.tsx +42 -13
- package/dist/generator-templates/routes/cart.tsx +42 -2
- package/dist/generator-templates/routes/collections/$collectionHandle.tsx +44 -5
- package/dist/generator-templates/routes/index.tsx +33 -0
- package/dist/generator-templates/routes/pages/$pageHandle.tsx +48 -10
- package/dist/generator-templates/routes/policies/$policyHandle.tsx +67 -14
- package/dist/generator-templates/routes/policies/index.tsx +54 -4
- package/dist/generator-templates/routes/products/$productHandle.tsx +44 -9
- package/dist/hooks/init.js +2 -2
- package/dist/{utils → lib}/check-lockfile.js +7 -4
- package/dist/{utils → lib}/check-lockfile.test.js +19 -28
- package/dist/{utils → lib}/check-version.test.js +3 -2
- package/dist/lib/colors.d.ts +8 -0
- package/dist/lib/colors.js +8 -0
- package/dist/{utils → lib}/config.js +10 -19
- package/dist/{utils → lib}/flags.d.ts +9 -3
- package/dist/{utils → lib}/flags.js +19 -4
- package/dist/lib/flags.test.d.ts +1 -0
- package/dist/{utils → lib}/mini-oxygen.js +14 -12
- package/dist/{utils → lib}/missing-routes.js +1 -1
- package/dist/lib/remix-version-interop.d.ts +11 -0
- package/dist/lib/remix-version-interop.js +54 -0
- package/dist/lib/remix-version-interop.test.d.ts +1 -0
- package/dist/lib/remix-version-interop.test.js +93 -0
- package/dist/lib/shell.d.ts +12 -0
- package/dist/lib/shell.js +73 -0
- package/dist/lib/template-downloader.d.ts +6 -0
- package/dist/{utils → lib}/template-downloader.js +21 -16
- package/dist/{utils → lib}/transpile-ts.js +5 -5
- package/dist/lib/virtual-routes.test.d.ts +1 -0
- package/dist/virtual-routes/routes/index.jsx +2 -15
- package/dist/virtual-routes/virtual-root.jsx +5 -6
- package/oclif.manifest.json +1 -1
- package/package.json +11 -10
- package/dist/utils/template-downloader.d.ts +0 -11
- /package/dist/{utils/check-lockfile.test.d.ts → commands/hydrogen/init.test.d.ts} +0 -0
- /package/dist/{utils/check-version.test.d.ts → commands/hydrogen/shortcut.test.d.ts} +0 -0
- /package/dist/{utils → lib}/check-lockfile.d.ts +0 -0
- /package/dist/{utils/flags.test.d.ts → lib/check-lockfile.test.d.ts} +0 -0
- /package/dist/{utils → lib}/check-version.d.ts +0 -0
- /package/dist/{utils → lib}/check-version.js +0 -0
- /package/dist/{utils/virtual-routes.test.d.ts → lib/check-version.test.d.ts} +0 -0
- /package/dist/{utils → lib}/config.d.ts +0 -0
- /package/dist/{utils → lib}/flags.test.js +0 -0
- /package/dist/{utils → lib}/log.d.ts +0 -0
- /package/dist/{utils → lib}/log.js +0 -0
- /package/dist/{utils → lib}/mini-oxygen.d.ts +0 -0
- /package/dist/{utils → lib}/missing-routes.d.ts +0 -0
- /package/dist/{utils → lib}/transpile-ts.d.ts +0 -0
- /package/dist/{utils → lib}/virtual-routes.d.ts +0 -0
- /package/dist/{utils → lib}/virtual-routes.js +0 -0
- /package/dist/{utils → lib}/virtual-routes.test.js +0 -0
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import { describe, beforeEach, vi, it, expect } from 'vitest';
|
|
2
2
|
import { temporaryDirectoryTask } from 'tempy';
|
|
3
3
|
import { runGenerate, GENERATOR_TEMPLATES_DIR } from './route.js';
|
|
4
|
-
import {
|
|
4
|
+
import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
|
|
5
|
+
import { readFile, mkdir, writeFile } from '@shopify/cli-kit/node/fs';
|
|
6
|
+
import { joinPath, dirname } from '@shopify/cli-kit/node/path';
|
|
7
|
+
import { convertRouteToV2 } from '../../../lib/remix-version-interop.js';
|
|
5
8
|
|
|
6
9
|
describe("generate/route", () => {
|
|
7
10
|
beforeEach(() => {
|
|
8
11
|
vi.resetAllMocks();
|
|
9
|
-
vi.mock("@shopify/cli-kit"
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
...cliKit,
|
|
13
|
-
output: {
|
|
14
|
-
...cliKit.output,
|
|
15
|
-
success: vi.fn()
|
|
16
|
-
},
|
|
17
|
-
ui: {
|
|
18
|
-
prompt: vi.fn()
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
});
|
|
12
|
+
vi.mock("@shopify/cli-kit/node/output");
|
|
13
|
+
vi.mock("@shopify/cli-kit/node/ui");
|
|
22
14
|
});
|
|
23
15
|
it("generates a route file", async () => {
|
|
24
16
|
await temporaryDirectoryTask(async (tmpDir) => {
|
|
@@ -27,12 +19,30 @@ describe("generate/route", () => {
|
|
|
27
19
|
files: [],
|
|
28
20
|
templates: [[route, `const str = "hello world"`]]
|
|
29
21
|
});
|
|
30
|
-
await runGenerate(route, {
|
|
22
|
+
await runGenerate(route, route, {
|
|
31
23
|
directory: appRoot,
|
|
32
24
|
templatesRoot
|
|
33
25
|
});
|
|
34
26
|
expect(
|
|
35
|
-
await
|
|
27
|
+
await readFile(joinPath(appRoot, "app/routes", `${route}.jsx`))
|
|
28
|
+
).toContain(`const str = 'hello world'`);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
it("generates a route file for Remix v2", async () => {
|
|
32
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
33
|
+
const route = "custom/path/$handle/index";
|
|
34
|
+
const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
|
|
35
|
+
files: [],
|
|
36
|
+
templates: [[route, `const str = "hello world"`]]
|
|
37
|
+
});
|
|
38
|
+
await runGenerate(route, convertRouteToV2(route), {
|
|
39
|
+
directory: appRoot,
|
|
40
|
+
templatesRoot
|
|
41
|
+
});
|
|
42
|
+
expect(
|
|
43
|
+
await readFile(
|
|
44
|
+
joinPath(appRoot, "app/routes", `custom.path.$handle._index.jsx`)
|
|
45
|
+
)
|
|
36
46
|
).toContain(`const str = 'hello world'`);
|
|
37
47
|
});
|
|
38
48
|
});
|
|
@@ -43,55 +53,53 @@ describe("generate/route", () => {
|
|
|
43
53
|
files: [],
|
|
44
54
|
templates: [[route, 'const str = "hello typescript"']]
|
|
45
55
|
});
|
|
46
|
-
await runGenerate(route, {
|
|
56
|
+
await runGenerate(route, route, {
|
|
47
57
|
directory: appRoot,
|
|
48
58
|
templatesRoot,
|
|
49
59
|
typescript: true
|
|
50
60
|
});
|
|
51
61
|
expect(
|
|
52
|
-
await
|
|
62
|
+
await readFile(joinPath(appRoot, "app/routes", `${route}.tsx`))
|
|
53
63
|
).toContain(`const str = 'hello typescript'`);
|
|
54
64
|
});
|
|
55
65
|
});
|
|
56
66
|
it("prompts the user if there the file already exists", async () => {
|
|
57
67
|
await temporaryDirectoryTask(async (tmpDir) => {
|
|
58
|
-
vi.mocked(
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
|
|
69
|
+
async () => true
|
|
70
|
+
);
|
|
61
71
|
const route = "page/$pageHandle";
|
|
62
72
|
const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
|
|
63
73
|
files: [[`app/routes/${route}.jsx`, 'const str = "I exist"']],
|
|
64
74
|
templates: [[route, 'const str = "hello world"']]
|
|
65
75
|
});
|
|
66
|
-
await runGenerate(route, {
|
|
76
|
+
await runGenerate(route, route, {
|
|
67
77
|
directory: appRoot,
|
|
68
78
|
templatesRoot
|
|
69
79
|
});
|
|
70
|
-
expect(
|
|
71
|
-
expect.
|
|
72
|
-
expect.
|
|
73
|
-
|
|
74
|
-
})
|
|
75
|
-
])
|
|
80
|
+
expect(renderConfirmationPrompt).toHaveBeenCalledWith(
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
message: expect.stringContaining("already exists")
|
|
83
|
+
})
|
|
76
84
|
);
|
|
77
85
|
});
|
|
78
86
|
});
|
|
79
87
|
it("does not prompt the user if the force property is true", async () => {
|
|
80
88
|
await temporaryDirectoryTask(async (tmpDir) => {
|
|
81
|
-
vi.mocked(
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
vi.mocked(renderConfirmationPrompt).mockImplementationOnce(
|
|
90
|
+
async () => true
|
|
91
|
+
);
|
|
84
92
|
const route = "page/$pageHandle";
|
|
85
93
|
const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
|
|
86
94
|
files: [[`app/routes/${route}.jsx`, 'const str = "I exist"']],
|
|
87
95
|
templates: [[route, 'const str = "hello world"']]
|
|
88
96
|
});
|
|
89
|
-
await runGenerate(route, {
|
|
97
|
+
await runGenerate(route, route, {
|
|
90
98
|
directory: appRoot,
|
|
91
99
|
templatesRoot,
|
|
92
100
|
force: true
|
|
93
101
|
});
|
|
94
|
-
expect(
|
|
102
|
+
expect(renderConfirmationPrompt).not.toHaveBeenCalled();
|
|
95
103
|
});
|
|
96
104
|
});
|
|
97
105
|
});
|
|
@@ -104,23 +112,23 @@ async function createHydrogen(directory, {
|
|
|
104
112
|
}) {
|
|
105
113
|
for (const item of files) {
|
|
106
114
|
const [filePath, fileContent] = item;
|
|
107
|
-
const fullFilePath =
|
|
108
|
-
await
|
|
109
|
-
await
|
|
115
|
+
const fullFilePath = joinPath(directory, "app", filePath);
|
|
116
|
+
await mkdir(dirname(fullFilePath));
|
|
117
|
+
await writeFile(fullFilePath, fileContent);
|
|
110
118
|
}
|
|
111
119
|
for (const item of templates) {
|
|
112
120
|
const [filePath, fileContent] = item;
|
|
113
|
-
const fullFilePath =
|
|
121
|
+
const fullFilePath = joinPath(
|
|
114
122
|
directory,
|
|
115
123
|
GENERATOR_TEMPLATES_DIR,
|
|
116
124
|
"routes",
|
|
117
125
|
`${filePath}.tsx`
|
|
118
126
|
);
|
|
119
|
-
await
|
|
120
|
-
await
|
|
127
|
+
await mkdir(dirname(fullFilePath));
|
|
128
|
+
await writeFile(fullFilePath, fileContent);
|
|
121
129
|
}
|
|
122
130
|
return {
|
|
123
|
-
appRoot:
|
|
131
|
+
appRoot: joinPath(directory, "app"),
|
|
124
132
|
templatesRoot: directory
|
|
125
133
|
};
|
|
126
134
|
}
|
|
@@ -5,10 +5,10 @@ declare class GenerateRoutes extends Command {
|
|
|
5
5
|
static description: string;
|
|
6
6
|
static hidden: true;
|
|
7
7
|
static flags: {
|
|
8
|
-
adapter: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
|
|
8
|
+
adapter: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
9
9
|
typescript: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
10
10
|
force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
11
|
-
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
|
|
11
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
12
12
|
};
|
|
13
13
|
run(): Promise<void>;
|
|
14
14
|
}
|
|
@@ -5,9 +5,9 @@ declare class Init extends Command {
|
|
|
5
5
|
static description: string;
|
|
6
6
|
static flags: {
|
|
7
7
|
force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
8
|
-
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
|
|
9
|
-
language: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
|
|
10
|
-
template: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
|
|
8
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
9
|
+
language: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
10
|
+
template: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
11
11
|
'install-deps': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
12
12
|
};
|
|
13
13
|
run(): Promise<void>;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
2
|
import { packageManagerUsedForCreating, installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
|
|
3
|
-
import { renderFatalError } from '@shopify/cli-kit/node/ui';
|
|
4
|
-
import Flags from '@oclif/core
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
3
|
+
import { renderFatalError, renderSelectPrompt, renderTextPrompt, renderConfirmationPrompt, renderInfo, renderTasks, renderSuccess } from '@shopify/cli-kit/node/ui';
|
|
4
|
+
import { Flags } from '@oclif/core';
|
|
5
|
+
import { basename, resolvePath, joinPath } from '@shopify/cli-kit/node/path';
|
|
6
|
+
import { rmdir, copyFile, fileExists, isDirectory } from '@shopify/cli-kit/node/fs';
|
|
7
|
+
import { outputContent, outputToken } from '@shopify/cli-kit/node/output';
|
|
8
|
+
import { commonFlags, flagsToCamelObject, parseProcessFlags } from '../../lib/flags.js';
|
|
9
|
+
import { transpileProject } from '../../lib/transpile-ts.js';
|
|
10
|
+
import { getLatestTemplates } from '../../lib/template-downloader.js';
|
|
11
|
+
import { checkHydrogenVersion } from '../../lib/check-version.js';
|
|
10
12
|
import { readdir } from 'fs/promises';
|
|
11
13
|
import { fileURLToPath } from 'url';
|
|
12
14
|
|
|
@@ -32,13 +34,13 @@ class Init extends Command {
|
|
|
32
34
|
}),
|
|
33
35
|
"install-deps": Flags.boolean({
|
|
34
36
|
description: "Auto install dependencies using the active package manager",
|
|
35
|
-
env: "
|
|
37
|
+
env: "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS",
|
|
36
38
|
allowNo: true
|
|
37
39
|
})
|
|
38
40
|
};
|
|
39
41
|
async run() {
|
|
40
42
|
const { flags } = await this.parse(Init);
|
|
41
|
-
await runInit(
|
|
43
|
+
await runInit(flagsToCamelObject(flags));
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
|
|
@@ -53,106 +55,77 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
|
|
|
53
55
|
packageManager2 === "unknown" ? "" : `Please use the latest version with \`${packageManager2} create @shopify/hydrogen@latest\``
|
|
54
56
|
);
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
let templatesDownloaded = false;
|
|
59
|
+
const templatesPromise = getLatestTemplates().then((result) => {
|
|
60
|
+
templatesDownloaded = true;
|
|
61
|
+
return result;
|
|
62
|
+
}).catch((error) => {
|
|
58
63
|
renderFatalError(error);
|
|
59
64
|
process.exit(1);
|
|
60
65
|
});
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
],
|
|
84
|
-
default: "js"
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
if (!options.path) {
|
|
88
|
-
prompts.push({
|
|
89
|
-
type: "input",
|
|
90
|
-
name: "path",
|
|
91
|
-
message: "Where would you like to create your app?",
|
|
92
|
-
default: "hydrogen-storefront"
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
const {
|
|
96
|
-
path: location = options.path,
|
|
97
|
-
template: appTemplate = options.template,
|
|
98
|
-
language = options.language ?? "js"
|
|
99
|
-
} = prompts.length > 0 ? await ui.prompt(prompts) : options;
|
|
100
|
-
const projectName = path.basename(location);
|
|
101
|
-
const projectDir = path.resolve(process.cwd(), location);
|
|
66
|
+
const appTemplate = options.template ?? await renderSelectPrompt({
|
|
67
|
+
message: "Choose a template",
|
|
68
|
+
defaultValue: "hello-world",
|
|
69
|
+
choices: STARTER_TEMPLATES.map((value) => ({
|
|
70
|
+
label: value.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
|
|
71
|
+
value
|
|
72
|
+
}))
|
|
73
|
+
});
|
|
74
|
+
const language = options.language ?? await renderSelectPrompt({
|
|
75
|
+
message: "Choose a language",
|
|
76
|
+
choices: [
|
|
77
|
+
{ label: "JavaScript", value: "js" },
|
|
78
|
+
{ label: "TypeScript", value: "ts" }
|
|
79
|
+
],
|
|
80
|
+
defaultValue: "js"
|
|
81
|
+
});
|
|
82
|
+
const location = options.path ?? await renderTextPrompt({
|
|
83
|
+
message: "Where would you like to create your app?",
|
|
84
|
+
defaultValue: "hydrogen-storefront"
|
|
85
|
+
});
|
|
86
|
+
const projectName = basename(location);
|
|
87
|
+
const projectDir = resolvePath(process.cwd(), location);
|
|
102
88
|
if (await projectExists(projectDir)) {
|
|
103
89
|
if (!options.force) {
|
|
104
|
-
const
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
choices: [
|
|
110
|
-
{ name: "Yes, delete the files", value: "true" },
|
|
111
|
-
{ name: "No, do not delete the files", value: "false" }
|
|
112
|
-
],
|
|
113
|
-
default: "false"
|
|
114
|
-
}
|
|
115
|
-
]);
|
|
116
|
-
if (deleteFiles === "false") {
|
|
90
|
+
const deleteFiles = await renderConfirmationPrompt({
|
|
91
|
+
message: `${location} is not an empty directory. Do you want to delete the existing files and continue?`,
|
|
92
|
+
defaultValue: false
|
|
93
|
+
});
|
|
94
|
+
if (!deleteFiles) {
|
|
117
95
|
renderInfo({
|
|
118
96
|
headline: `Destination path ${location} already exists and is not an empty directory. You may use \`--force\` or \`-f\` to override it.`
|
|
119
97
|
});
|
|
120
98
|
return;
|
|
121
99
|
}
|
|
122
100
|
}
|
|
123
|
-
await
|
|
101
|
+
await rmdir(projectDir, { force: true });
|
|
102
|
+
}
|
|
103
|
+
if (!templatesDownloaded) {
|
|
104
|
+
await renderTasks([
|
|
105
|
+
{
|
|
106
|
+
title: "Downloading templates",
|
|
107
|
+
task: async () => {
|
|
108
|
+
await templatesPromise;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
]);
|
|
124
112
|
}
|
|
125
|
-
let downloaded = false;
|
|
126
|
-
setTimeout(
|
|
127
|
-
() => !downloaded && output.info("\n\u{1F4E5} Downloading templates..."),
|
|
128
|
-
150
|
|
129
|
-
);
|
|
130
113
|
const { templatesDir } = await templatesPromise;
|
|
131
|
-
|
|
132
|
-
await file.copy(path.join(templatesDir, appTemplate), projectDir);
|
|
114
|
+
await copyFile(joinPath(templatesDir, appTemplate), projectDir);
|
|
133
115
|
if (language === "js") {
|
|
134
116
|
try {
|
|
135
117
|
await transpileProject(projectDir);
|
|
136
118
|
} catch (error) {
|
|
137
|
-
await
|
|
119
|
+
await rmdir(projectDir, { force: true });
|
|
138
120
|
throw error;
|
|
139
121
|
}
|
|
140
122
|
}
|
|
141
123
|
let depsInstalled = false;
|
|
142
124
|
let packageManager = await packageManagerUsedForCreating();
|
|
143
125
|
if (packageManager !== "unknown") {
|
|
144
|
-
const installDeps = options.installDeps ??
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
name: "installDeps",
|
|
148
|
-
message: `Install dependencies with ${packageManager}?`,
|
|
149
|
-
choices: [
|
|
150
|
-
{ name: "Yes", value: "true" },
|
|
151
|
-
{ name: "No", value: "false" }
|
|
152
|
-
],
|
|
153
|
-
default: "true"
|
|
154
|
-
}
|
|
155
|
-
])).installDeps === "true";
|
|
126
|
+
const installDeps = options.installDeps ?? await renderConfirmationPrompt({
|
|
127
|
+
message: `Install dependencies with ${packageManager}?`
|
|
128
|
+
});
|
|
156
129
|
if (installDeps) {
|
|
157
130
|
await installNodeModules({
|
|
158
131
|
directory: projectDir,
|
|
@@ -169,20 +142,28 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
|
|
|
169
142
|
renderSuccess({
|
|
170
143
|
headline: `${projectName} is ready to build.`,
|
|
171
144
|
nextSteps: [
|
|
172
|
-
`Run
|
|
173
|
-
|
|
145
|
+
outputContent`Run ${outputToken.genericShellCommand(`cd ${location}`)}`.value,
|
|
146
|
+
depsInstalled ? void 0 : outputContent`Run ${outputToken.genericShellCommand(
|
|
147
|
+
`${packageManager} install`
|
|
148
|
+
)} to install the dependencies`.value,
|
|
149
|
+
outputContent`Run ${outputToken.packagejsonScript(
|
|
150
|
+
packageManager,
|
|
151
|
+
"dev"
|
|
152
|
+
)} to start your local development server and start building`.value
|
|
153
|
+
].filter((step) => Boolean(step)),
|
|
174
154
|
reference: [
|
|
175
|
-
"Building with Hydrogen: https://shopify.dev/custom-storefronts/hydrogen"
|
|
155
|
+
"Building with Hydrogen: https://shopify.dev/docs/custom-storefronts/hydrogen/building/begin-development"
|
|
176
156
|
]
|
|
177
157
|
});
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
158
|
+
if (appTemplate === "demo-store") {
|
|
159
|
+
renderInfo({
|
|
160
|
+
headline: `Your project will display inventory from the Hydrogen Demo Store.`,
|
|
161
|
+
body: `To connect this project to your Shopify store\u2019s inventory, update \`${projectName}/.env\` with your store ID and Storefront API key.`
|
|
162
|
+
});
|
|
163
|
+
}
|
|
182
164
|
}
|
|
183
165
|
async function projectExists(projectDir) {
|
|
184
|
-
|
|
185
|
-
return await file.exists(projectDir) && await file.isDirectory(projectDir) && (await readdir(projectDir)).length > 0;
|
|
166
|
+
return await fileExists(projectDir) && await isDirectory(projectDir) && (await readdir(projectDir)).length > 0;
|
|
186
167
|
}
|
|
187
168
|
function supressNodeExperimentalWarnings() {
|
|
188
169
|
const warningListener = process.listeners("warning")[0];
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, beforeEach, vi, it, expect } from 'vitest';
|
|
2
|
+
import { temporaryDirectoryTask } from 'tempy';
|
|
3
|
+
import { runInit } from './init.js';
|
|
4
|
+
import { renderSelectPrompt, renderConfirmationPrompt, renderTextPrompt, renderInfo } from '@shopify/cli-kit/node/ui';
|
|
5
|
+
import { outputContent } from '@shopify/cli-kit/node/output';
|
|
6
|
+
import { installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
|
|
7
|
+
|
|
8
|
+
describe("init", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.resetAllMocks();
|
|
11
|
+
vi.mock("@shopify/cli-kit/node/output");
|
|
12
|
+
vi.mock("../../lib/transpile-ts.js");
|
|
13
|
+
vi.mock("../../lib/template-downloader.js", async () => ({
|
|
14
|
+
getLatestTemplates: () => Promise.resolve({})
|
|
15
|
+
}));
|
|
16
|
+
vi.mock("@shopify/cli-kit/node/node-package-manager");
|
|
17
|
+
vi.mocked(outputContent).mockImplementation(() => ({
|
|
18
|
+
value: ""
|
|
19
|
+
}));
|
|
20
|
+
vi.mock("@shopify/cli-kit/node/ui");
|
|
21
|
+
vi.mock("@shopify/cli-kit/node/fs");
|
|
22
|
+
});
|
|
23
|
+
const defaultOptions = (stubs) => ({
|
|
24
|
+
template: "hello-world",
|
|
25
|
+
language: "js",
|
|
26
|
+
path: "path/to/project",
|
|
27
|
+
...stubs
|
|
28
|
+
});
|
|
29
|
+
describe.each([
|
|
30
|
+
{
|
|
31
|
+
flag: "template",
|
|
32
|
+
value: "hello-world",
|
|
33
|
+
condition: { fn: renderSelectPrompt, match: /template/i }
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
flag: "installDeps",
|
|
37
|
+
value: true,
|
|
38
|
+
condition: { fn: renderConfirmationPrompt, match: /install dependencies/i }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
flag: "language",
|
|
42
|
+
value: "ts",
|
|
43
|
+
condition: { fn: renderSelectPrompt, match: /language/i }
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
flag: "path",
|
|
47
|
+
value: "./my-app",
|
|
48
|
+
condition: { fn: renderTextPrompt, match: /where/i }
|
|
49
|
+
}
|
|
50
|
+
])("flag $flag", ({ flag, value, condition }) => {
|
|
51
|
+
it(`does not prompt the user for ${flag} when a value is passed in options`, async () => {
|
|
52
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
53
|
+
const options = defaultOptions({
|
|
54
|
+
path: tmpDir,
|
|
55
|
+
[flag]: value
|
|
56
|
+
});
|
|
57
|
+
await runInit(options);
|
|
58
|
+
expect(condition.fn).not.toHaveBeenCalledWith(
|
|
59
|
+
expect.objectContaining({
|
|
60
|
+
message: expect.stringMatching(condition.match)
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it(`prompts the user for ${flag} when no value is passed in options`, async () => {
|
|
66
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
67
|
+
const options = defaultOptions({
|
|
68
|
+
path: tmpDir,
|
|
69
|
+
[flag]: void 0
|
|
70
|
+
});
|
|
71
|
+
await runInit(options);
|
|
72
|
+
expect(condition.fn).toHaveBeenCalledWith(
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
message: expect.stringMatching(condition.match)
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it("installs dependencies when installDeps is true", async () => {
|
|
81
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
82
|
+
const options = defaultOptions({ installDeps: true, path: tmpDir });
|
|
83
|
+
await runInit(options);
|
|
84
|
+
expect(installNodeModules).toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it("does not install dependencies when installDeps is false", async () => {
|
|
88
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
89
|
+
const options = defaultOptions({ installDeps: false, path: tmpDir });
|
|
90
|
+
await runInit(options);
|
|
91
|
+
expect(installNodeModules).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
it("displays inventory information when using the demo-store template", async () => {
|
|
95
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
96
|
+
const options = defaultOptions({
|
|
97
|
+
installDeps: false,
|
|
98
|
+
path: tmpDir,
|
|
99
|
+
template: "demo-store"
|
|
100
|
+
});
|
|
101
|
+
await runInit(options);
|
|
102
|
+
expect(renderInfo).toHaveBeenCalledTimes(1);
|
|
103
|
+
expect(renderInfo).toHaveBeenCalledWith(
|
|
104
|
+
expect.objectContaining({
|
|
105
|
+
body: expect.stringContaining(
|
|
106
|
+
"To connect this project to your Shopify store\u2019s inventory"
|
|
107
|
+
),
|
|
108
|
+
headline: expect.stringContaining(
|
|
109
|
+
"Your project will display inventory from the Hydrogen Demo Store"
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
it("does not display inventory information when using non-demo-store templates", async () => {
|
|
116
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
117
|
+
const options = defaultOptions({
|
|
118
|
+
installDeps: false,
|
|
119
|
+
path: tmpDir,
|
|
120
|
+
template: "pizza-store"
|
|
121
|
+
});
|
|
122
|
+
await runInit(options);
|
|
123
|
+
expect(renderInfo).toHaveBeenCalledTimes(0);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -4,8 +4,8 @@ import Command from '@shopify/cli-kit/node/base-command';
|
|
|
4
4
|
declare class Preview extends Command {
|
|
5
5
|
static description: string;
|
|
6
6
|
static flags: {
|
|
7
|
-
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
|
|
8
|
-
port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number>;
|
|
7
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
8
|
+
port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
|
|
9
9
|
};
|
|
10
10
|
run(): Promise<void>;
|
|
11
11
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
|
-
import { muteDevLogs } from '../../
|
|
3
|
-
import { getProjectPaths } from '../../
|
|
4
|
-
import { commonFlags } from '../../
|
|
5
|
-
import { startMiniOxygen } from '../../
|
|
2
|
+
import { muteDevLogs } from '../../lib/log.js';
|
|
3
|
+
import { getProjectPaths } from '../../lib/config.js';
|
|
4
|
+
import { commonFlags } from '../../lib/flags.js';
|
|
5
|
+
import { startMiniOxygen } from '../../lib/mini-oxygen.js';
|
|
6
6
|
|
|
7
7
|
class Preview extends Command {
|
|
8
8
|
static description = "Runs a Hydrogen storefront in an Oxygen worker for production.";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
|
+
|
|
3
|
+
declare class Shortcut extends Command {
|
|
4
|
+
static description: string;
|
|
5
|
+
run(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
declare function runCreateShortcut(): Promise<void>;
|
|
8
|
+
|
|
9
|
+
export { Shortcut as default, runCreateShortcut };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
|
+
import { renderSuccess, renderFatalError } from '@shopify/cli-kit/node/ui';
|
|
3
|
+
import { isWindows, isGitBash, supportsShell, hasAlias, shellWriteFile, homeFileExists, shellRunScript } from '../../lib/shell.js';
|
|
4
|
+
|
|
5
|
+
const ALIAS_NAME = "h2";
|
|
6
|
+
class Shortcut extends Command {
|
|
7
|
+
static description = `Creates a global \`${ALIAS_NAME}\` shortcut for the Hydrogen CLI`;
|
|
8
|
+
async run() {
|
|
9
|
+
await runCreateShortcut();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function runCreateShortcut() {
|
|
13
|
+
const shortcuts = isWindows() && !isGitBash() ? await createShortcutsForWindows() : await createShortcutsForUnix();
|
|
14
|
+
if (shortcuts.length > 0) {
|
|
15
|
+
renderSuccess({
|
|
16
|
+
headline: `Shortcut ready for the following shells: ${shortcuts.join(
|
|
17
|
+
", "
|
|
18
|
+
)}.
|
|
19
|
+
Restart your terminal session and run \`${ALIAS_NAME}\` from your local project.`
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
renderFatalError({
|
|
23
|
+
name: "error",
|
|
24
|
+
type: 0,
|
|
25
|
+
message: "No supported shell found.",
|
|
26
|
+
tryMessage: "Please create a shortcut manually."
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const BASH_ZSH_COMMAND = `
|
|
31
|
+
# Shopify Hydrogen alias to local projects
|
|
32
|
+
alias ${ALIAS_NAME}='$(npm prefix -s)/node_modules/.bin/shopify hydrogen'`;
|
|
33
|
+
const FISH_FUNCTION = `
|
|
34
|
+
function ${ALIAS_NAME} --wraps='shopify hydrogen' --description 'Shortcut for the Hydrogen CLI'
|
|
35
|
+
set npmPrefix (npm prefix -s)
|
|
36
|
+
$npmPrefix/node_modules/.bin/shopify hydrogen $argv
|
|
37
|
+
end
|
|
38
|
+
`;
|
|
39
|
+
async function createShortcutsForUnix() {
|
|
40
|
+
const shells = [];
|
|
41
|
+
if (supportsShell("zsh") && (hasAlias(ALIAS_NAME, "~/.zshrc") || shellWriteFile("~/.zshrc", BASH_ZSH_COMMAND, true))) {
|
|
42
|
+
shells.push("zsh");
|
|
43
|
+
}
|
|
44
|
+
if (supportsShell("bash") && (hasAlias(ALIAS_NAME, "~/.bashrc") || shellWriteFile("~/.bashrc", BASH_ZSH_COMMAND, true))) {
|
|
45
|
+
shells.push("bash");
|
|
46
|
+
}
|
|
47
|
+
if (supportsShell("fish") && await homeFileExists("~/.config/fish/functions") && shellWriteFile(`~/.config/fish/functions/${ALIAS_NAME}.fish`, FISH_FUNCTION)) {
|
|
48
|
+
shells.push("fish");
|
|
49
|
+
}
|
|
50
|
+
return shells;
|
|
51
|
+
}
|
|
52
|
+
const PS_FUNCTION = `function Invoke-Local-H2 {$npmPrefix = npm prefix -s; Invoke-Expression "$npmPrefix\\node_modules\\.bin\\shopify.ps1 hydrogen $Args"}; Set-Alias -Name ${ALIAS_NAME} -Value Invoke-Local-H2`;
|
|
53
|
+
const PS_APPEND_PROFILE_COMMAND = `
|
|
54
|
+
if (!(Test-Path -Path $PROFILE)) {
|
|
55
|
+
New-Item -ItemType File -Path $PROFILE -Force
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
$profileContent = Get-Content -Path $PROFILE
|
|
59
|
+
if (!$profileContent -or $profileContent -NotLike '*Invoke-Local-H2*') {
|
|
60
|
+
Add-Content -Path $PROFILE -Value '${PS_FUNCTION}'
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
async function createShortcutsForWindows() {
|
|
64
|
+
const shells = [];
|
|
65
|
+
if (shellRunScript(PS_APPEND_PROFILE_COMMAND, "powershell.exe")) {
|
|
66
|
+
shells.push("PowerShell");
|
|
67
|
+
}
|
|
68
|
+
if (shellRunScript(PS_APPEND_PROFILE_COMMAND, "pwsh.exe")) {
|
|
69
|
+
shells.push("PowerShell 7+");
|
|
70
|
+
}
|
|
71
|
+
return shells;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { Shortcut as default, runCreateShortcut };
|