@shopify/cli-hydrogen 0.0.0-next-744c490-20230315110711
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/LICENSE.md +7 -0
- package/README.md +14 -0
- package/dist/commands/hydrogen/build.d.ts +23 -0
- package/dist/commands/hydrogen/build.js +100 -0
- package/dist/commands/hydrogen/check.d.ts +18 -0
- package/dist/commands/hydrogen/check.js +35 -0
- package/dist/commands/hydrogen/dev.d.ts +15 -0
- package/dist/commands/hydrogen/dev.js +134 -0
- package/dist/commands/hydrogen/generate/route.d.ts +34 -0
- package/dist/commands/hydrogen/generate/route.js +166 -0
- package/dist/commands/hydrogen/generate/route.test.d.ts +1 -0
- package/dist/commands/hydrogen/generate/route.test.js +126 -0
- package/dist/commands/hydrogen/generate/routes.d.ts +16 -0
- package/dist/commands/hydrogen/generate/routes.js +22 -0
- package/dist/commands/hydrogen/init.d.ts +24 -0
- package/dist/commands/hydrogen/init.js +199 -0
- package/dist/commands/hydrogen/init.test.d.ts +1 -0
- package/dist/commands/hydrogen/init.test.js +79 -0
- package/dist/commands/hydrogen/preview.d.ts +17 -0
- package/dist/commands/hydrogen/preview.js +34 -0
- package/dist/create-app.d.ts +1 -0
- package/dist/create-app.js +4 -0
- package/dist/generator-templates/routes/[robots.txt].tsx +41 -0
- package/dist/generator-templates/routes/[sitemap.xml].tsx +204 -0
- package/dist/generator-templates/routes/account/login.tsx +74 -0
- package/dist/generator-templates/routes/account/register.tsx +74 -0
- package/dist/generator-templates/routes/cart.tsx +41 -0
- package/dist/generator-templates/routes/collections/$collectionHandle.tsx +65 -0
- package/dist/generator-templates/routes/graphiql.tsx +10 -0
- package/dist/generator-templates/routes/index.tsx +7 -0
- package/dist/generator-templates/routes/pages/$pageHandle.tsx +74 -0
- package/dist/generator-templates/routes/policies/$policyHandle.tsx +87 -0
- package/dist/generator-templates/routes/policies/index.tsx +67 -0
- package/dist/generator-templates/routes/products/$productHandle.tsx +57 -0
- package/dist/hooks/init.d.ts +5 -0
- package/dist/hooks/init.js +18 -0
- package/dist/utils/check-lockfile.d.ts +3 -0
- package/dist/utils/check-lockfile.js +78 -0
- package/dist/utils/check-lockfile.test.d.ts +1 -0
- package/dist/utils/check-lockfile.test.js +74 -0
- package/dist/utils/check-version.d.ts +16 -0
- package/dist/utils/check-version.js +55 -0
- package/dist/utils/check-version.test.d.ts +1 -0
- package/dist/utils/check-version.test.js +60 -0
- package/dist/utils/config.d.ts +18 -0
- package/dist/utils/config.js +146 -0
- package/dist/utils/flags.d.ts +24 -0
- package/dist/utils/flags.js +60 -0
- package/dist/utils/flags.test.d.ts +1 -0
- package/dist/utils/flags.test.js +43 -0
- package/dist/utils/log.d.ts +5 -0
- package/dist/utils/log.js +20 -0
- package/dist/utils/mini-oxygen.d.ts +11 -0
- package/dist/utils/mini-oxygen.js +73 -0
- package/dist/utils/missing-routes.d.ts +6 -0
- package/dist/utils/missing-routes.js +71 -0
- package/dist/utils/template-downloader.d.ts +11 -0
- package/dist/utils/template-downloader.js +70 -0
- package/dist/utils/transpile-ts.d.ts +16 -0
- package/dist/utils/transpile-ts.js +169 -0
- package/dist/utils/virtual-routes.d.ts +7 -0
- package/dist/utils/virtual-routes.js +44 -0
- package/dist/utils/virtual-routes.test.d.ts +1 -0
- package/dist/utils/virtual-routes.test.js +49 -0
- package/dist/virtual-routes/assets/favicon.svg +28 -0
- package/dist/virtual-routes/assets/inter-variable-font.woff2 +0 -0
- package/dist/virtual-routes/assets/jetbrainsmono-variable-font.woff2 +0 -0
- package/dist/virtual-routes/assets/styles.css +238 -0
- package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +7 -0
- package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +13 -0
- package/dist/virtual-routes/components/IconBanner.jsx +47 -0
- package/dist/virtual-routes/components/IconDiscord.jsx +4 -0
- package/dist/virtual-routes/components/IconError.jsx +20 -0
- package/dist/virtual-routes/components/IconGithub.jsx +4 -0
- package/dist/virtual-routes/components/IconTwitter.jsx +4 -0
- package/dist/virtual-routes/components/Layout.jsx +6 -0
- package/dist/virtual-routes/routes/graphiql.jsx +5 -0
- package/dist/virtual-routes/routes/index.jsx +163 -0
- package/dist/virtual-routes/virtual-root.jsx +38 -0
- package/oclif.manifest.json +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,16 @@
|
|
|
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 GenerateRoutes extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static hidden: true;
|
|
7
|
+
static flags: {
|
|
8
|
+
adapter: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
|
|
9
|
+
typescript: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
10
|
+
force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
11
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { GenerateRoutes as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import GenerateRoute from './route.js';
|
|
2
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
3
|
+
|
|
4
|
+
class GenerateRoutes extends Command {
|
|
5
|
+
static description = "Generates all supported standard shopify routes.";
|
|
6
|
+
static hidden;
|
|
7
|
+
static flags = GenerateRoute.flags;
|
|
8
|
+
async run() {
|
|
9
|
+
const { flags } = await this.parse(GenerateRoutes);
|
|
10
|
+
await GenerateRoute.run([
|
|
11
|
+
"all",
|
|
12
|
+
...Object.entries(flags).map(([name, value]) => {
|
|
13
|
+
if (value === true) {
|
|
14
|
+
return `--${name}`;
|
|
15
|
+
}
|
|
16
|
+
return `--${name}=${value}`;
|
|
17
|
+
})
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { GenerateRoutes as default };
|
|
@@ -0,0 +1,24 @@
|
|
|
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 Init extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static flags: {
|
|
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>;
|
|
11
|
+
'install-deps': _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
declare function runInit(options?: {
|
|
16
|
+
path?: string;
|
|
17
|
+
template?: string;
|
|
18
|
+
language?: string;
|
|
19
|
+
token?: string;
|
|
20
|
+
force?: boolean;
|
|
21
|
+
installDeps?: boolean;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
|
|
24
|
+
export { Init as default, runInit };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
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/lib/flags.js';
|
|
5
|
+
import { output, path } from '@shopify/cli-kit';
|
|
6
|
+
import { commonFlags, flagsToCamelObject, parseProcessFlags } from '../../utils/flags.js';
|
|
7
|
+
import { transpileProject } from '../../utils/transpile-ts.js';
|
|
8
|
+
import { getLatestTemplates } from '../../utils/template-downloader.js';
|
|
9
|
+
import { checkHydrogenVersion } from '../../utils/check-version.js';
|
|
10
|
+
import { readdir } from 'fs/promises';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const STARTER_TEMPLATES = ["hello-world", "demo-store"];
|
|
14
|
+
const FLAG_MAP = { f: "force" };
|
|
15
|
+
class Init extends Command {
|
|
16
|
+
static description = "Creates a new Hydrogen storefront.";
|
|
17
|
+
static flags = {
|
|
18
|
+
force: commonFlags.force,
|
|
19
|
+
path: Flags.string({
|
|
20
|
+
description: "The path to the directory of the new Hydrogen storefront.",
|
|
21
|
+
env: "SHOPIFY_HYDROGEN_FLAG_PATH"
|
|
22
|
+
}),
|
|
23
|
+
language: Flags.string({
|
|
24
|
+
description: "Sets the template language to use. One of `js` or `ts`.",
|
|
25
|
+
choices: ["js", "ts"],
|
|
26
|
+
env: "SHOPIFY_HYDROGEN_FLAG_LANGUAGE"
|
|
27
|
+
}),
|
|
28
|
+
template: Flags.string({
|
|
29
|
+
description: "Sets the template to use. One of `demo-store` or `hello-world`.",
|
|
30
|
+
choices: STARTER_TEMPLATES,
|
|
31
|
+
env: "SHOPIFY_HYDROGEN_FLAG_TEMPLATE"
|
|
32
|
+
}),
|
|
33
|
+
"install-deps": Flags.boolean({
|
|
34
|
+
description: "Auto install dependencies using the active package manager",
|
|
35
|
+
env: "SHOPIFY_HYDROGEN_FLAG_INSTALL_DEPS",
|
|
36
|
+
allowNo: true
|
|
37
|
+
})
|
|
38
|
+
};
|
|
39
|
+
async run() {
|
|
40
|
+
const { flags } = await this.parse(Init);
|
|
41
|
+
await runInit(flagsToCamelObject(flags));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
|
|
45
|
+
supressNodeExperimentalWarnings();
|
|
46
|
+
const showUpgrade = await checkHydrogenVersion(
|
|
47
|
+
fileURLToPath(new URL("../../../package.json", import.meta.url)),
|
|
48
|
+
"cli"
|
|
49
|
+
);
|
|
50
|
+
if (showUpgrade) {
|
|
51
|
+
const packageManager2 = await packageManagerUsedForCreating();
|
|
52
|
+
showUpgrade(
|
|
53
|
+
packageManager2 === "unknown" ? "" : `Please use the latest version with \`${packageManager2} create @shopify/hydrogen@latest\``
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const templatesPromise = getLatestTemplates().catch((error) => {
|
|
57
|
+
output.info("\n\n\n");
|
|
58
|
+
renderFatalError(error);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
});
|
|
61
|
+
const { ui, file } = await import('@shopify/cli-kit');
|
|
62
|
+
const { renderSuccess, renderInfo } = await import('@shopify/cli-kit/node/ui');
|
|
63
|
+
const prompts = [];
|
|
64
|
+
if (!options.template) {
|
|
65
|
+
prompts.push({
|
|
66
|
+
type: "select",
|
|
67
|
+
name: "template",
|
|
68
|
+
message: "Choose a template",
|
|
69
|
+
choices: STARTER_TEMPLATES.map((value) => ({
|
|
70
|
+
name: value.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
|
|
71
|
+
value
|
|
72
|
+
}))
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!options.language) {
|
|
76
|
+
prompts.push({
|
|
77
|
+
type: "select",
|
|
78
|
+
name: "language",
|
|
79
|
+
message: "Choose a language",
|
|
80
|
+
choices: [
|
|
81
|
+
{ name: "JavaScript", value: "js" },
|
|
82
|
+
{ name: "TypeScript", value: "ts" }
|
|
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);
|
|
102
|
+
if (await projectExists(projectDir)) {
|
|
103
|
+
if (!options.force) {
|
|
104
|
+
const { deleteFiles } = await ui.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: "select",
|
|
107
|
+
name: "deleteFiles",
|
|
108
|
+
message: `${location} is not an empty directory. Do you want to delete the existing files and continue?`,
|
|
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") {
|
|
117
|
+
renderInfo({
|
|
118
|
+
headline: `Destination path ${location} already exists and is not an empty directory. You may use \`--force\` or \`-f\` to override it.`
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
await file.rmdir(projectDir, { force: true });
|
|
124
|
+
}
|
|
125
|
+
let downloaded = false;
|
|
126
|
+
setTimeout(
|
|
127
|
+
() => !downloaded && output.info("\n\u{1F4E5} Downloading templates..."),
|
|
128
|
+
150
|
|
129
|
+
);
|
|
130
|
+
const { templatesDir } = await templatesPromise;
|
|
131
|
+
downloaded = true;
|
|
132
|
+
await file.copy(path.join(templatesDir, appTemplate), projectDir);
|
|
133
|
+
if (language === "js") {
|
|
134
|
+
try {
|
|
135
|
+
await transpileProject(projectDir);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await file.rmdir(projectDir, { force: true });
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let depsInstalled = false;
|
|
142
|
+
let packageManager = await packageManagerUsedForCreating();
|
|
143
|
+
if (packageManager !== "unknown") {
|
|
144
|
+
const installDeps = options.installDeps ?? (await ui.prompt([
|
|
145
|
+
{
|
|
146
|
+
type: "select",
|
|
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";
|
|
156
|
+
if (installDeps) {
|
|
157
|
+
await installNodeModules({
|
|
158
|
+
directory: projectDir,
|
|
159
|
+
packageManager,
|
|
160
|
+
args: [],
|
|
161
|
+
stdout: process.stdout,
|
|
162
|
+
stderr: process.stderr
|
|
163
|
+
});
|
|
164
|
+
depsInstalled = true;
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
packageManager = "npm";
|
|
168
|
+
}
|
|
169
|
+
renderSuccess({
|
|
170
|
+
headline: `${projectName} is ready to build.`,
|
|
171
|
+
nextSteps: [
|
|
172
|
+
`Run \`cd ${location}\`${depsInstalled ? "" : `, \`${packageManager} install\`,`} and \`${packageManager + (packageManager === "npm" ? " run" : "")} dev\` to start your local development server and start building.`
|
|
173
|
+
],
|
|
174
|
+
reference: [
|
|
175
|
+
"Building with Hydrogen: https://shopify.dev/custom-storefronts/hydrogen"
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
renderInfo({
|
|
179
|
+
headline: `Your project will display inventory from the Hydrogen Demo Store.`,
|
|
180
|
+
body: `To connect this project to your Shopify store\u2019s inventory, update \`${projectName}/.env\` with your store ID and Storefront API key.`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async function projectExists(projectDir) {
|
|
184
|
+
const { file } = await import('@shopify/cli-kit');
|
|
185
|
+
return await file.exists(projectDir) && await file.isDirectory(projectDir) && (await readdir(projectDir)).length > 0;
|
|
186
|
+
}
|
|
187
|
+
function supressNodeExperimentalWarnings() {
|
|
188
|
+
const warningListener = process.listeners("warning")[0];
|
|
189
|
+
if (warningListener) {
|
|
190
|
+
process.removeAllListeners("warning");
|
|
191
|
+
process.prependListener("warning", (warning) => {
|
|
192
|
+
if (warning.name != "ExperimentalWarning") {
|
|
193
|
+
warningListener(warning);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { Init as default, runInit };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, beforeEach, vi, it, expect } from 'vitest';
|
|
2
|
+
import { temporaryDirectoryTask } from 'tempy';
|
|
3
|
+
import { runInit } from './init.js';
|
|
4
|
+
import { ui } from '@shopify/cli-kit';
|
|
5
|
+
import { installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
|
|
6
|
+
|
|
7
|
+
describe("init", () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.resetAllMocks();
|
|
10
|
+
vi.mock("@shopify/cli-kit");
|
|
11
|
+
vi.mock("../../utils/transpile-ts.js");
|
|
12
|
+
vi.mock("../../utils/template-downloader.js", async () => ({
|
|
13
|
+
getLatestTemplates: () => Promise.resolve({})
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("@shopify/cli-kit/node/node-package-manager");
|
|
16
|
+
vi.mocked(ui.prompt).mockImplementation(
|
|
17
|
+
() => Promise.resolve({ installDeps: "false" })
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
const defaultOptions = (stubs) => ({
|
|
21
|
+
template: "hello-world",
|
|
22
|
+
language: "js",
|
|
23
|
+
path: "path/to/project",
|
|
24
|
+
...stubs
|
|
25
|
+
});
|
|
26
|
+
describe.each([
|
|
27
|
+
{ flag: "template", value: "hello-world" },
|
|
28
|
+
{ flag: "installDeps", value: true },
|
|
29
|
+
{ flag: "language", value: "ts" },
|
|
30
|
+
{ flag: "path", value: "./my-app" }
|
|
31
|
+
])("flag $flag", ({ flag, value }) => {
|
|
32
|
+
it(`does not prompt the user for ${flag} when a value is passed in options`, async () => {
|
|
33
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
34
|
+
const options = defaultOptions({
|
|
35
|
+
path: tmpDir,
|
|
36
|
+
[flag]: value
|
|
37
|
+
});
|
|
38
|
+
await runInit(options);
|
|
39
|
+
expect(ui.prompt).not.toHaveBeenCalledWith(
|
|
40
|
+
expect.arrayContaining([
|
|
41
|
+
expect.objectContaining({
|
|
42
|
+
name: flag
|
|
43
|
+
})
|
|
44
|
+
])
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it(`prompts the user for ${flag} when no value is passed in options`, async () => {
|
|
49
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
50
|
+
const options = defaultOptions({
|
|
51
|
+
path: tmpDir,
|
|
52
|
+
[flag]: void 0
|
|
53
|
+
});
|
|
54
|
+
await runInit(options);
|
|
55
|
+
expect(ui.prompt).toHaveBeenCalledWith(
|
|
56
|
+
expect.arrayContaining([
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
name: flag
|
|
59
|
+
})
|
|
60
|
+
])
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it("installs dependencies when installDeps is true", async () => {
|
|
66
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
67
|
+
const options = defaultOptions({ installDeps: true, path: tmpDir });
|
|
68
|
+
await runInit(options);
|
|
69
|
+
expect(installNodeModules).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it("does not install dependencies when installDeps is false", async () => {
|
|
73
|
+
await temporaryDirectoryTask(async (tmpDir) => {
|
|
74
|
+
const options = defaultOptions({ installDeps: false, path: tmpDir });
|
|
75
|
+
await runInit(options);
|
|
76
|
+
expect(installNodeModules).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
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 Preview extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static flags: {
|
|
7
|
+
path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
|
|
8
|
+
port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
declare function runPreview({ port, path: appPath, }: {
|
|
13
|
+
port?: number;
|
|
14
|
+
path?: string;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
|
|
17
|
+
export { Preview as default, runPreview };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
|
+
import { muteDevLogs } from '../../utils/log.js';
|
|
3
|
+
import { getProjectPaths } from '../../utils/config.js';
|
|
4
|
+
import { commonFlags } from '../../utils/flags.js';
|
|
5
|
+
import { startMiniOxygen } from '../../utils/mini-oxygen.js';
|
|
6
|
+
|
|
7
|
+
class Preview extends Command {
|
|
8
|
+
static description = "Runs a Hydrogen storefront in an Oxygen worker for production.";
|
|
9
|
+
static flags = {
|
|
10
|
+
path: commonFlags.path,
|
|
11
|
+
port: commonFlags.port
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(Preview);
|
|
15
|
+
await runPreview({ ...flags });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function runPreview({
|
|
19
|
+
port,
|
|
20
|
+
path: appPath
|
|
21
|
+
}) {
|
|
22
|
+
if (!process.env.NODE_ENV)
|
|
23
|
+
process.env.NODE_ENV = "production";
|
|
24
|
+
muteDevLogs({ workerReload: false });
|
|
25
|
+
const { root, buildPathWorkerFile, buildPathClient } = getProjectPaths(appPath);
|
|
26
|
+
await startMiniOxygen({
|
|
27
|
+
root,
|
|
28
|
+
port,
|
|
29
|
+
buildPathClient,
|
|
30
|
+
buildPathWorkerFile
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { Preview as default, runPreview };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {type LoaderArgs} from '@shopify/remix-oxygen';
|
|
2
|
+
|
|
3
|
+
export const loader = ({request}: LoaderArgs) => {
|
|
4
|
+
const url = new URL(request.url);
|
|
5
|
+
|
|
6
|
+
return new Response(robotsTxtData({url: url.origin}), {
|
|
7
|
+
status: 200,
|
|
8
|
+
headers: {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
10
|
+
'content-type': 'text/plain',
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
12
|
+
'cache-control': `max-age=${60 * 60 * 24}`,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function robotsTxtData({url}: {url: string}) {
|
|
18
|
+
const sitemapUrl = url ? `${url}/sitemap.xml` : undefined;
|
|
19
|
+
|
|
20
|
+
return `
|
|
21
|
+
User-agent: *
|
|
22
|
+
Disallow: /admin
|
|
23
|
+
Disallow: /cart
|
|
24
|
+
Disallow: /orders
|
|
25
|
+
Disallow: /checkouts/
|
|
26
|
+
Disallow: /checkout
|
|
27
|
+
Disallow: /carts
|
|
28
|
+
Disallow: /account
|
|
29
|
+
${sitemapUrl ? `Sitemap: ${sitemapUrl}` : ''}
|
|
30
|
+
|
|
31
|
+
# Google adsbot ignores robots.txt unless specifically named!
|
|
32
|
+
User-agent: adsbot-google
|
|
33
|
+
Disallow: /checkouts/
|
|
34
|
+
Disallow: /checkout
|
|
35
|
+
Disallow: /carts
|
|
36
|
+
Disallow: /orders
|
|
37
|
+
|
|
38
|
+
User-agent: Pinterest
|
|
39
|
+
Crawl-delay: 1
|
|
40
|
+
`.trim();
|
|
41
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {flattenConnection} from '@shopify/hydrogen';
|
|
2
|
+
import type {LoaderArgs} from '@shopify/remix-oxygen';
|
|
3
|
+
import {
|
|
4
|
+
CollectionConnection,
|
|
5
|
+
PageConnection,
|
|
6
|
+
ProductConnection,
|
|
7
|
+
} from '@shopify/hydrogen/storefront-api-types';
|
|
8
|
+
|
|
9
|
+
const MAX_URLS = 250; // the google limit is 50K, however, SF API only allow querying for 250 resources each time
|
|
10
|
+
|
|
11
|
+
interface SitemapQueryData {
|
|
12
|
+
products: ProductConnection;
|
|
13
|
+
collections: CollectionConnection;
|
|
14
|
+
pages: PageConnection;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ProductEntry {
|
|
18
|
+
url: string;
|
|
19
|
+
lastMod: string;
|
|
20
|
+
changeFreq: string;
|
|
21
|
+
image?: {
|
|
22
|
+
url: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
caption?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function loader({request, context: {storefront}}: LoaderArgs) {
|
|
29
|
+
const data = await storefront.query<SitemapQueryData>(SITEMAP_QUERY, {
|
|
30
|
+
variables: {
|
|
31
|
+
urlLimits: MAX_URLS,
|
|
32
|
+
language: storefront.i18n.language,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!data) {
|
|
37
|
+
throw new Response(null, {status: 404});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new Response(
|
|
41
|
+
shopSitemap({data, baseUrl: new URL(request.url).origin}),
|
|
42
|
+
{
|
|
43
|
+
headers: {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
45
|
+
'content-type': 'application/xml',
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
47
|
+
'cache-control': `max-age=${60 * 60 * 24}`,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function xmlEncode(string: string) {
|
|
54
|
+
return string.replace(/[&<>'"]/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function shopSitemap({
|
|
58
|
+
data,
|
|
59
|
+
baseUrl,
|
|
60
|
+
}: {
|
|
61
|
+
data: SitemapQueryData;
|
|
62
|
+
baseUrl: string;
|
|
63
|
+
}) {
|
|
64
|
+
const productsData = flattenConnection(data.products)
|
|
65
|
+
.filter((product) => product.onlineStoreUrl)
|
|
66
|
+
.map((product) => {
|
|
67
|
+
const url = `${baseUrl}/products/${xmlEncode(product.handle)}`;
|
|
68
|
+
|
|
69
|
+
const finalObject: ProductEntry = {
|
|
70
|
+
url,
|
|
71
|
+
lastMod: product.updatedAt,
|
|
72
|
+
changeFreq: 'daily',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (product.featuredImage?.url) {
|
|
76
|
+
finalObject.image = {
|
|
77
|
+
url: xmlEncode(product.featuredImage.url),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (product.title) {
|
|
81
|
+
finalObject.image.title = xmlEncode(product.title);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (product.featuredImage.altText) {
|
|
85
|
+
finalObject.image.caption = xmlEncode(product.featuredImage.altText);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return finalObject;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const collectionsData = flattenConnection(data.collections)
|
|
93
|
+
.filter((collection) => collection.onlineStoreUrl)
|
|
94
|
+
.map((collection) => {
|
|
95
|
+
const url = `${baseUrl}/collections/${collection.handle}`;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
url,
|
|
99
|
+
lastMod: collection.updatedAt,
|
|
100
|
+
changeFreq: 'daily',
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const pagesData = flattenConnection(data.pages)
|
|
105
|
+
.filter((page) => page.onlineStoreUrl)
|
|
106
|
+
.map((page) => {
|
|
107
|
+
const url = `${baseUrl}/pages/${page.handle}`;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
url,
|
|
111
|
+
lastMod: page.updatedAt,
|
|
112
|
+
changeFreq: 'weekly',
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const urlsDatas = [...productsData, ...collectionsData, ...pagesData];
|
|
117
|
+
|
|
118
|
+
return `
|
|
119
|
+
<urlset
|
|
120
|
+
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
121
|
+
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
|
|
122
|
+
>
|
|
123
|
+
${urlsDatas.map((url) => renderUrlTag(url)).join('')}
|
|
124
|
+
</urlset>`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderUrlTag({
|
|
128
|
+
url,
|
|
129
|
+
lastMod,
|
|
130
|
+
changeFreq,
|
|
131
|
+
image,
|
|
132
|
+
}: {
|
|
133
|
+
url: string;
|
|
134
|
+
lastMod?: string;
|
|
135
|
+
changeFreq?: string;
|
|
136
|
+
image?: {
|
|
137
|
+
url: string;
|
|
138
|
+
title?: string;
|
|
139
|
+
caption?: string;
|
|
140
|
+
};
|
|
141
|
+
}) {
|
|
142
|
+
return `
|
|
143
|
+
<url>
|
|
144
|
+
<loc>${url}</loc>
|
|
145
|
+
<lastmod>${lastMod}</lastmod>
|
|
146
|
+
<changefreq>${changeFreq}</changefreq>
|
|
147
|
+
${
|
|
148
|
+
image
|
|
149
|
+
? `
|
|
150
|
+
<image:image>
|
|
151
|
+
<image:loc>${image.url}</image:loc>
|
|
152
|
+
<image:title>${image.title ?? ''}</image:title>
|
|
153
|
+
<image:caption>${image.caption ?? ''}</image:caption>
|
|
154
|
+
</image:image>`
|
|
155
|
+
: ''
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
</url>
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const SITEMAP_QUERY = `#graphql
|
|
163
|
+
query sitemaps($urlLimits: Int, $language: LanguageCode)
|
|
164
|
+
@inContext(language: $language) {
|
|
165
|
+
products(
|
|
166
|
+
first: $urlLimits
|
|
167
|
+
query: "published_status:'online_store:visible'"
|
|
168
|
+
) {
|
|
169
|
+
edges {
|
|
170
|
+
node {
|
|
171
|
+
updatedAt
|
|
172
|
+
handle
|
|
173
|
+
onlineStoreUrl
|
|
174
|
+
title
|
|
175
|
+
featuredImage {
|
|
176
|
+
url
|
|
177
|
+
altText
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
collections(
|
|
183
|
+
first: $urlLimits
|
|
184
|
+
query: "published_status:'online_store:visible'"
|
|
185
|
+
) {
|
|
186
|
+
edges {
|
|
187
|
+
node {
|
|
188
|
+
updatedAt
|
|
189
|
+
handle
|
|
190
|
+
onlineStoreUrl
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
pages(first: $urlLimits, query: "published_status:'published'") {
|
|
195
|
+
edges {
|
|
196
|
+
node {
|
|
197
|
+
updatedAt
|
|
198
|
+
handle
|
|
199
|
+
onlineStoreUrl
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
`;
|