@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.
Files changed (81) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +14 -0
  3. package/dist/commands/hydrogen/build.d.ts +23 -0
  4. package/dist/commands/hydrogen/build.js +100 -0
  5. package/dist/commands/hydrogen/check.d.ts +18 -0
  6. package/dist/commands/hydrogen/check.js +35 -0
  7. package/dist/commands/hydrogen/dev.d.ts +15 -0
  8. package/dist/commands/hydrogen/dev.js +134 -0
  9. package/dist/commands/hydrogen/generate/route.d.ts +34 -0
  10. package/dist/commands/hydrogen/generate/route.js +166 -0
  11. package/dist/commands/hydrogen/generate/route.test.d.ts +1 -0
  12. package/dist/commands/hydrogen/generate/route.test.js +126 -0
  13. package/dist/commands/hydrogen/generate/routes.d.ts +16 -0
  14. package/dist/commands/hydrogen/generate/routes.js +22 -0
  15. package/dist/commands/hydrogen/init.d.ts +24 -0
  16. package/dist/commands/hydrogen/init.js +199 -0
  17. package/dist/commands/hydrogen/init.test.d.ts +1 -0
  18. package/dist/commands/hydrogen/init.test.js +79 -0
  19. package/dist/commands/hydrogen/preview.d.ts +17 -0
  20. package/dist/commands/hydrogen/preview.js +34 -0
  21. package/dist/create-app.d.ts +1 -0
  22. package/dist/create-app.js +4 -0
  23. package/dist/generator-templates/routes/[robots.txt].tsx +41 -0
  24. package/dist/generator-templates/routes/[sitemap.xml].tsx +204 -0
  25. package/dist/generator-templates/routes/account/login.tsx +74 -0
  26. package/dist/generator-templates/routes/account/register.tsx +74 -0
  27. package/dist/generator-templates/routes/cart.tsx +41 -0
  28. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +65 -0
  29. package/dist/generator-templates/routes/graphiql.tsx +10 -0
  30. package/dist/generator-templates/routes/index.tsx +7 -0
  31. package/dist/generator-templates/routes/pages/$pageHandle.tsx +74 -0
  32. package/dist/generator-templates/routes/policies/$policyHandle.tsx +87 -0
  33. package/dist/generator-templates/routes/policies/index.tsx +67 -0
  34. package/dist/generator-templates/routes/products/$productHandle.tsx +57 -0
  35. package/dist/hooks/init.d.ts +5 -0
  36. package/dist/hooks/init.js +18 -0
  37. package/dist/utils/check-lockfile.d.ts +3 -0
  38. package/dist/utils/check-lockfile.js +78 -0
  39. package/dist/utils/check-lockfile.test.d.ts +1 -0
  40. package/dist/utils/check-lockfile.test.js +74 -0
  41. package/dist/utils/check-version.d.ts +16 -0
  42. package/dist/utils/check-version.js +55 -0
  43. package/dist/utils/check-version.test.d.ts +1 -0
  44. package/dist/utils/check-version.test.js +60 -0
  45. package/dist/utils/config.d.ts +18 -0
  46. package/dist/utils/config.js +146 -0
  47. package/dist/utils/flags.d.ts +24 -0
  48. package/dist/utils/flags.js +60 -0
  49. package/dist/utils/flags.test.d.ts +1 -0
  50. package/dist/utils/flags.test.js +43 -0
  51. package/dist/utils/log.d.ts +5 -0
  52. package/dist/utils/log.js +20 -0
  53. package/dist/utils/mini-oxygen.d.ts +11 -0
  54. package/dist/utils/mini-oxygen.js +73 -0
  55. package/dist/utils/missing-routes.d.ts +6 -0
  56. package/dist/utils/missing-routes.js +71 -0
  57. package/dist/utils/template-downloader.d.ts +11 -0
  58. package/dist/utils/template-downloader.js +70 -0
  59. package/dist/utils/transpile-ts.d.ts +16 -0
  60. package/dist/utils/transpile-ts.js +169 -0
  61. package/dist/utils/virtual-routes.d.ts +7 -0
  62. package/dist/utils/virtual-routes.js +44 -0
  63. package/dist/utils/virtual-routes.test.d.ts +1 -0
  64. package/dist/utils/virtual-routes.test.js +49 -0
  65. package/dist/virtual-routes/assets/favicon.svg +28 -0
  66. package/dist/virtual-routes/assets/inter-variable-font.woff2 +0 -0
  67. package/dist/virtual-routes/assets/jetbrainsmono-variable-font.woff2 +0 -0
  68. package/dist/virtual-routes/assets/styles.css +238 -0
  69. package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +7 -0
  70. package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +13 -0
  71. package/dist/virtual-routes/components/IconBanner.jsx +47 -0
  72. package/dist/virtual-routes/components/IconDiscord.jsx +4 -0
  73. package/dist/virtual-routes/components/IconError.jsx +20 -0
  74. package/dist/virtual-routes/components/IconGithub.jsx +4 -0
  75. package/dist/virtual-routes/components/IconTwitter.jsx +4 -0
  76. package/dist/virtual-routes/components/Layout.jsx +6 -0
  77. package/dist/virtual-routes/routes/graphiql.jsx +5 -0
  78. package/dist/virtual-routes/routes/index.jsx +163 -0
  79. package/dist/virtual-routes/virtual-root.jsx +38 -0
  80. package/oclif.manifest.json +1 -0
  81. package/package.json +72 -0
package/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2023-present, Shopify Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # @shopify/cli-hydrogen
2
+
3
+ The Hydrogen extension for the [Shopify CLI](https://shopify.dev/apps/tools/cli). Hydrogen is a set of tools, utilities, and best-in-class examples for building a commerce application with [Remix](https://wwww.remix.run).
4
+
5
+ [Check out the docs](https://shopify.dev/custom-storefronts/hydrogen)
6
+
7
+ ## Contributing
8
+
9
+ The most common way to test the cli changes locally is to do the following:
10
+
11
+ - Run `npm run build` in this directory (`packages/cli` from the root of the repo).
12
+ - Run `npx shopify hydrogen` anywhere else in the monorepo, for example `npx shopify hydrogen init`.
13
+ - If you want to test a command inside of a template, run the command from within that template or use the `--path` flag to point to another template or any Hydrogen app.
14
+ - If you want to make changes to a file that is generated when running `npx shopify hydrogen generate`, make changes to that file from inside of the `templates/skeleton` directory.
@@ -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 Build extends Command {
5
+ static description: string;
6
+ static flags: {
7
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
8
+ sourcemap: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
9
+ "disable-route-warning": _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
10
+ base: _oclif_core_lib_interfaces_parser_js.OptionFlag<unknown>;
11
+ entry: _oclif_core_lib_interfaces_parser_js.OptionFlag<unknown>;
12
+ target: _oclif_core_lib_interfaces_parser_js.OptionFlag<unknown>;
13
+ };
14
+ run(): Promise<void>;
15
+ }
16
+ declare function runBuild({ path: appPath, sourcemap, disableRouteWarning, }: {
17
+ path?: string;
18
+ sourcemap?: boolean;
19
+ disableRouteWarning?: boolean;
20
+ }): Promise<void>;
21
+ declare function copyPublicFiles(publicPath: string, buildPathClient: string): Promise<void>;
22
+
23
+ export { copyPublicFiles, Build as default, runBuild };
@@ -0,0 +1,100 @@
1
+ import path from 'path';
2
+ import { file, output } from '@shopify/cli-kit';
3
+ import colors from '@shopify/cli-kit/node/colors';
4
+ import { getProjectPaths, getRemixConfig } from '../../utils/config.js';
5
+ import { commonFlags, deprecated, flagsToCamelObject } from '../../utils/flags.js';
6
+ import Command from '@shopify/cli-kit/node/base-command';
7
+ import Flags from '@oclif/core/lib/flags.js';
8
+ import { checkLockfileStatus } from '../../utils/check-lockfile.js';
9
+ import { findMissingRoutes } from '../../utils/missing-routes.js';
10
+ import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
11
+
12
+ const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
13
+ class Build extends Command {
14
+ static description = "Builds a Hydrogen storefront for production.";
15
+ static flags = {
16
+ path: commonFlags.path,
17
+ sourcemap: Flags.boolean({
18
+ description: "Generate sourcemaps for the build.",
19
+ env: "SHOPIFY_HYDROGEN_FLAG_SOURCEMAP",
20
+ default: true
21
+ }),
22
+ ["disable-route-warning"]: Flags.boolean({
23
+ description: "Disable warning about missing standard routes.",
24
+ env: "SHOPIFY_HYDROGEN_FLAG_DISABLE_ROUTE_WARNING"
25
+ }),
26
+ base: deprecated("--base")(),
27
+ entry: deprecated("--entry")(),
28
+ target: deprecated("--target")()
29
+ };
30
+ async run() {
31
+ const { flags } = await this.parse(Build);
32
+ const directory = flags.path ? path.resolve(flags.path) : process.cwd();
33
+ await runBuild({ ...flagsToCamelObject(flags), path: directory });
34
+ }
35
+ }
36
+ async function runBuild({
37
+ path: appPath,
38
+ sourcemap = true,
39
+ disableRouteWarning = false
40
+ }) {
41
+ if (!process.env.NODE_ENV) {
42
+ process.env.NODE_ENV = "production";
43
+ }
44
+ const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(appPath);
45
+ await checkLockfileStatus(root);
46
+ console.time(LOG_WORKER_BUILT);
47
+ const [remixConfig] = await Promise.all([
48
+ getRemixConfig(root),
49
+ file.rmdir(buildPath, { force: true })
50
+ ]);
51
+ output.info(`
52
+ \u{1F3D7}\uFE0F Building in ${process.env.NODE_ENV} mode...`);
53
+ const { build } = await import('@remix-run/dev/dist/compiler/build.js');
54
+ const { logCompileFailure } = await import('@remix-run/dev/dist/compiler/onCompileFailure.js');
55
+ await Promise.all([
56
+ copyPublicFiles(publicPath, buildPathClient),
57
+ build(remixConfig, {
58
+ mode: process.env.NODE_ENV,
59
+ sourcemap,
60
+ onCompileFailure: (failure) => {
61
+ logCompileFailure(failure);
62
+ throw Error();
63
+ }
64
+ })
65
+ ]);
66
+ if (process.env.NODE_ENV !== "development") {
67
+ console.timeEnd(LOG_WORKER_BUILT);
68
+ const sizeMB = await file.size(buildPathWorkerFile) / (1024 * 1024);
69
+ output.info(
70
+ output.content` ${colors.dim(
71
+ path.relative(root, buildPathWorkerFile)
72
+ )} ${output.token.yellow(sizeMB.toFixed(2))} MB\n`
73
+ );
74
+ if (sizeMB >= 1) {
75
+ output.warn(
76
+ `\u{1F6A8} Worker bundle exceeds 1 MB! This can delay your worker response.${remixConfig.serverMinify ? "" : " Minify your bundle by adding `serverMinify: true` to remix.config.js."}
77
+ `
78
+ );
79
+ }
80
+ }
81
+ if (!disableRouteWarning) {
82
+ const missingRoutes = findMissingRoutes(remixConfig);
83
+ if (missingRoutes.length) {
84
+ const packageManager = await getPackageManager(root);
85
+ const exec = packageManager === "npm" ? "npx" : packageManager;
86
+ output.warn(
87
+ `Heads up: Shopify stores have a number of standard routes that aren\u2019t set up yet.
88
+ Some functionality and backlinks might not work as expected until these are created or redirects are set up.
89
+ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ? "s" : ""}. For more details, run \`${exec} shopify hydrogen check routes\`.
90
+ `
91
+ );
92
+ }
93
+ }
94
+ process.exit(0);
95
+ }
96
+ async function copyPublicFiles(publicPath, buildPathClient) {
97
+ return file.copy(publicPath, buildPathClient);
98
+ }
99
+
100
+ export { copyPublicFiles, Build as default, runBuild };
@@ -0,0 +1,18 @@
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 GenerateRoute extends Command {
5
+ static description: string;
6
+ static flags: {
7
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
8
+ };
9
+ static args: {
10
+ name: string;
11
+ description: string;
12
+ required: boolean;
13
+ options: string[];
14
+ }[];
15
+ run(): Promise<void>;
16
+ }
17
+
18
+ export { GenerateRoute as default };
@@ -0,0 +1,35 @@
1
+ import Command from '@shopify/cli-kit/node/base-command';
2
+ import { path } from '@shopify/cli-kit';
3
+ import { commonFlags } from '../../utils/flags.js';
4
+ import { getRemixConfig } from '../../utils/config.js';
5
+ import { logMissingRoutes, findMissingRoutes } from '../../utils/missing-routes.js';
6
+
7
+ class GenerateRoute extends Command {
8
+ static description = "Returns diagnostic information about a Hydrogen storefront.";
9
+ static flags = {
10
+ path: commonFlags.path
11
+ };
12
+ static args = [
13
+ {
14
+ name: "resource",
15
+ description: `The resource to check. Currently only 'routes' is supported.`,
16
+ required: true,
17
+ options: ["routes"]
18
+ }
19
+ ];
20
+ async run() {
21
+ const { flags, args } = await this.parse(GenerateRoute);
22
+ const directory = flags.path ? path.resolve(flags.path) : process.cwd();
23
+ if (args.resource === "routes") {
24
+ await runCheckRoutes({ directory });
25
+ } else {
26
+ throw new Error("Invalid command argument.");
27
+ }
28
+ }
29
+ }
30
+ async function runCheckRoutes({ directory }) {
31
+ const remixConfig = await getRemixConfig(directory);
32
+ logMissingRoutes(findMissingRoutes(remixConfig));
33
+ }
34
+
35
+ export { GenerateRoute as default };
@@ -0,0 +1,15 @@
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 Dev 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
+ "disable-virtual-routes": _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
10
+ host: _oclif_core_lib_interfaces_parser_js.OptionFlag<unknown>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
14
+
15
+ export { Dev as default };
@@ -0,0 +1,134 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import { output, file } from '@shopify/cli-kit';
4
+ import { copyPublicFiles } from './build.js';
5
+ import { getProjectPaths, getRemixConfig } from '../../utils/config.js';
6
+ import { muteDevLogs } from '../../utils/log.js';
7
+ import { commonFlags, deprecated, flagsToCamelObject } from '../../utils/flags.js';
8
+ import Command from '@shopify/cli-kit/node/base-command';
9
+ import Flags from '@oclif/core/lib/flags.js';
10
+ import { startMiniOxygen } from '../../utils/mini-oxygen.js';
11
+ import { checkHydrogenVersion } from '../../utils/check-version.js';
12
+ import { addVirtualRoutes } from '../../utils/virtual-routes.js';
13
+
14
+ const LOG_INITIAL_BUILD = "\n\u{1F3C1} Initial build";
15
+ const LOG_REBUILDING = "\u{1F9F1} Rebuilding...";
16
+ const LOG_REBUILT = "\u{1F680} Rebuilt";
17
+ class Dev extends Command {
18
+ static description = "Runs Hydrogen storefront in an Oxygen worker for development.";
19
+ static flags = {
20
+ path: commonFlags.path,
21
+ port: commonFlags.port,
22
+ ["disable-virtual-routes"]: Flags.boolean({
23
+ description: "Disable rendering fallback routes when a route file doesn't exist",
24
+ env: "SHOPIFY_HYDROGEN_FLAG_DISABLE_VIRTUAL_ROUTES",
25
+ default: false
26
+ }),
27
+ host: deprecated("--host")()
28
+ };
29
+ async run() {
30
+ const { flags } = await this.parse(Dev);
31
+ const directory = flags.path ? path.resolve(flags.path) : process.cwd();
32
+ await runDev({ ...flagsToCamelObject(flags), path: directory });
33
+ }
34
+ }
35
+ async function runDev({
36
+ port,
37
+ path: appPath,
38
+ disableVirtualRoutes
39
+ }) {
40
+ if (!process.env.NODE_ENV)
41
+ process.env.NODE_ENV = "development";
42
+ muteDevLogs();
43
+ console.time(LOG_INITIAL_BUILD);
44
+ const { root, publicPath, buildPathClient, buildPathWorkerFile } = getProjectPaths(appPath);
45
+ const checkingHydrogenVersion = checkHydrogenVersion(root);
46
+ const copyingFiles = copyPublicFiles(publicPath, buildPathClient);
47
+ const reloadConfig = async () => {
48
+ const config = await getRemixConfig(root);
49
+ return disableVirtualRoutes ? config : addVirtualRoutes(config);
50
+ };
51
+ const getFilePaths = (file2) => {
52
+ const fileRelative = path.relative(root, file2);
53
+ return [fileRelative, path.resolve(root, fileRelative)];
54
+ };
55
+ const serverBundleExists = () => file.exists(buildPathWorkerFile);
56
+ let miniOxygenStarted = false;
57
+ async function safeStartMiniOxygen() {
58
+ if (miniOxygenStarted)
59
+ return;
60
+ await startMiniOxygen({
61
+ root,
62
+ port,
63
+ watch: true,
64
+ buildPathWorkerFile,
65
+ buildPathClient
66
+ });
67
+ miniOxygenStarted = true;
68
+ const showUpgrade = await checkingHydrogenVersion;
69
+ if (showUpgrade)
70
+ showUpgrade();
71
+ }
72
+ const { watch } = await import('@remix-run/dev/dist/compiler/watch.js');
73
+ await watch(await reloadConfig(), {
74
+ reloadConfig,
75
+ mode: process.env.NODE_ENV,
76
+ async onInitialBuild() {
77
+ await copyingFiles;
78
+ if (!await serverBundleExists()) {
79
+ const { renderFatalError } = await import('@shopify/cli-kit/node/ui');
80
+ return renderFatalError({
81
+ name: "BuildError",
82
+ type: 0,
83
+ message: "MiniOxygen cannot start because the server bundle has not been generated.",
84
+ tryMessage: "This is likely due to an error in your app and Remix is unable to compile. Try fixing the app and MiniOxygen will start."
85
+ });
86
+ }
87
+ console.timeEnd(LOG_INITIAL_BUILD);
88
+ await safeStartMiniOxygen();
89
+ },
90
+ async onFileCreated(file2) {
91
+ const [relative, absolute] = getFilePaths(file2);
92
+ output.info(`
93
+ \u{1F4C4} File created: ${relative}`);
94
+ if (absolute.startsWith(publicPath)) {
95
+ await copyPublicFiles(
96
+ absolute,
97
+ absolute.replace(publicPath, buildPathClient)
98
+ );
99
+ }
100
+ },
101
+ async onFileChanged(file2) {
102
+ const [relative, absolute] = getFilePaths(file2);
103
+ output.info(`
104
+ \u{1F4C4} File changed: ${relative}`);
105
+ if (absolute.startsWith(publicPath)) {
106
+ await copyPublicFiles(
107
+ absolute,
108
+ absolute.replace(publicPath, buildPathClient)
109
+ );
110
+ }
111
+ },
112
+ async onFileDeleted(file2) {
113
+ const [relative, absolute] = getFilePaths(file2);
114
+ output.info(`
115
+ \u{1F4C4} File deleted: ${relative}`);
116
+ if (absolute.startsWith(publicPath)) {
117
+ await fs.unlink(absolute.replace(publicPath, buildPathClient));
118
+ }
119
+ },
120
+ onRebuildStart() {
121
+ output.info(LOG_REBUILDING);
122
+ console.time(LOG_REBUILT);
123
+ },
124
+ async onRebuildFinish() {
125
+ console.timeEnd(LOG_REBUILT);
126
+ if (!miniOxygenStarted && await serverBundleExists()) {
127
+ console.log("");
128
+ await safeStartMiniOxygen();
129
+ }
130
+ }
131
+ });
132
+ }
133
+
134
+ export { Dev as default };
@@ -0,0 +1,34 @@
1
+ import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
2
+ import Command from '@shopify/cli-kit/node/base-command';
3
+
4
+ declare const GENERATOR_TEMPLATES_DIR = "generator-templates";
5
+ interface Result {
6
+ operation: 'generated' | 'skipped' | 'overwritten';
7
+ }
8
+ declare class GenerateRoute extends Command {
9
+ static description: string;
10
+ static flags: {
11
+ adapter: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
12
+ typescript: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
13
+ force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
14
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined>;
15
+ };
16
+ static hidden: true;
17
+ static args: {
18
+ name: string;
19
+ description: string;
20
+ required: boolean;
21
+ options: string[];
22
+ env: string;
23
+ }[];
24
+ run(): Promise<void>;
25
+ }
26
+ declare function runGenerate(route: string, { directory, typescript, force, adapter, templatesRoot, }: {
27
+ directory: string;
28
+ typescript?: boolean;
29
+ force?: boolean;
30
+ adapter?: string;
31
+ templatesRoot?: string;
32
+ }): Promise<Result>;
33
+
34
+ export { GENERATOR_TEMPLATES_DIR, GenerateRoute as default, runGenerate };
@@ -0,0 +1,166 @@
1
+ import { fileURLToPath } from 'url';
2
+ import Command from '@shopify/cli-kit/node/base-command';
3
+ import { path, file, ui } from '@shopify/cli-kit';
4
+ import { AbortError } from '@shopify/cli-kit/node/error';
5
+ import { renderSuccess } from '@shopify/cli-kit/node/ui';
6
+ import { commonFlags } from '../../../utils/flags.js';
7
+ import Flags from '@oclif/core/lib/flags.js';
8
+ import { transpileFile, format, resolveFormatConfig } from '../../../utils/transpile-ts.js';
9
+
10
+ const GENERATOR_TEMPLATES_DIR = "generator-templates";
11
+ const ROUTE_MAP = {
12
+ home: "/index",
13
+ page: "/pages/$pageHandle",
14
+ cart: "/cart",
15
+ products: "/products/$productHandle",
16
+ collections: "/collections/$collectionHandle",
17
+ policies: ["/policies/index", "/policies/$policyHandle"],
18
+ robots: "/[robots.txt]",
19
+ sitemap: "/[sitemap.xml]",
20
+ account: ["/account/login", "/account/register"]
21
+ };
22
+ const ROUTES = [...Object.keys(ROUTE_MAP), "all"];
23
+ class GenerateRoute extends Command {
24
+ static description = "Generates a standard Shopify route.";
25
+ static flags = {
26
+ adapter: Flags.string({
27
+ description: "Remix adapter used in the route. The default is `@shopify/remix-oxygen`.",
28
+ env: "SHOPIFY_HYDROGEN_FLAG_ADAPTER"
29
+ }),
30
+ typescript: Flags.boolean({
31
+ description: "Generate TypeScript files",
32
+ env: "SHOPIFY_HYDROGEN_FLAG_TYPESCRIPT"
33
+ }),
34
+ force: commonFlags.force,
35
+ path: commonFlags.path
36
+ };
37
+ static hidden;
38
+ static args = [
39
+ {
40
+ name: "route",
41
+ description: `The route to generate. One of ${ROUTES.join()}.`,
42
+ required: true,
43
+ options: ROUTES,
44
+ env: "SHOPIFY_HYDROGEN_ARG_ROUTE"
45
+ }
46
+ ];
47
+ async run() {
48
+ const result = /* @__PURE__ */ new Map();
49
+ const { flags, args } = await this.parse(GenerateRoute);
50
+ const directory = flags.path ? path.resolve(flags.path) : process.cwd();
51
+ const { route } = args;
52
+ const routePath = route === "all" ? Object.values(ROUTE_MAP).flat() : ROUTE_MAP[route];
53
+ if (!routePath) {
54
+ throw new AbortError(
55
+ `No route found for ${route}. Try one of ${ROUTES.join()}.`
56
+ );
57
+ }
58
+ const isTypescript = flags.typescript || await file.exists(path.join(directory, "tsconfig.json"));
59
+ const routesArray = Array.isArray(routePath) ? routePath : [routePath];
60
+ try {
61
+ for (const item of routesArray) {
62
+ result.set(
63
+ item,
64
+ await runGenerate(item, {
65
+ directory,
66
+ typescript: isTypescript,
67
+ force: flags.force,
68
+ adapter: flags.adapter
69
+ })
70
+ );
71
+ }
72
+ } catch (err) {
73
+ throw new AbortError(err.message);
74
+ }
75
+ const extension = isTypescript ? ".tsx" : ".jsx";
76
+ const success = Array.from(result.values()).filter(
77
+ (result2) => result2.operation !== "skipped"
78
+ );
79
+ renderSuccess({
80
+ headline: `${success.length} of ${result.size} route${result.size > 1 ? "s" : ""} generated`,
81
+ body: {
82
+ list: {
83
+ items: Array.from(result.entries()).map(
84
+ ([path2, { operation }]) => `[${operation}] app/routes${path2}${extension}`
85
+ )
86
+ }
87
+ }
88
+ });
89
+ }
90
+ }
91
+ async function runGenerate(route, {
92
+ directory,
93
+ typescript,
94
+ force,
95
+ adapter,
96
+ templatesRoot = fileURLToPath(new URL("../../../", import.meta.url))
97
+ }) {
98
+ let operation;
99
+ const extension = typescript ? ".tsx" : ".jsx";
100
+ const templatePath = path.join(
101
+ templatesRoot,
102
+ GENERATOR_TEMPLATES_DIR,
103
+ "routes",
104
+ `${route}.tsx`
105
+ );
106
+ const destinationPath = path.join(
107
+ directory,
108
+ "app",
109
+ "routes",
110
+ `${route}${extension}`
111
+ );
112
+ const relativeDestinationPath = path.relative(directory, destinationPath);
113
+ if (!force && await file.exists(destinationPath)) {
114
+ const options = [
115
+ { name: "No", value: "skip" },
116
+ { name: `Yes`, value: "overwrite" }
117
+ ];
118
+ const choice = await ui.prompt([
119
+ {
120
+ type: "select",
121
+ name: "value",
122
+ message: `The file ${path.relativize(
123
+ relativeDestinationPath
124
+ )} already exists. Do you want to overwrite it?`,
125
+ choices: options
126
+ }
127
+ ]);
128
+ operation = choice.value === "skip" ? "skipped" : "overwritten";
129
+ if (operation === "skipped") {
130
+ return { operation };
131
+ }
132
+ } else {
133
+ operation = "generated";
134
+ }
135
+ let templateContent = await file.read(templatePath);
136
+ if (!typescript) {
137
+ const jsConfigPath = path.join(directory, "jsconfig.json");
138
+ const config = await file.exists(jsConfigPath) ? JSON.parse(
139
+ (await file.read(jsConfigPath, { encoding: "utf8" })).replace(
140
+ /^\s*\/\/.*$/gm,
141
+ ""
142
+ )
143
+ ) : void 0;
144
+ templateContent = transpileFile(templateContent, config?.compilerOptions);
145
+ }
146
+ if (adapter) {
147
+ templateContent = templateContent.replace(
148
+ /@shopify\/remix-oxygen/g,
149
+ adapter
150
+ );
151
+ }
152
+ templateContent = format(
153
+ templateContent,
154
+ await resolveFormatConfig(destinationPath),
155
+ destinationPath
156
+ );
157
+ if (!await file.exists(path.dirname(destinationPath))) {
158
+ await file.mkdir(path.dirname(destinationPath));
159
+ }
160
+ await file.write(destinationPath, templateContent);
161
+ return {
162
+ operation
163
+ };
164
+ }
165
+
166
+ export { GENERATOR_TEMPLATES_DIR, GenerateRoute as default, runGenerate };
@@ -0,0 +1,126 @@
1
+ import { describe, beforeEach, vi, it, expect } from 'vitest';
2
+ import { temporaryDirectoryTask } from 'tempy';
3
+ import { runGenerate, GENERATOR_TEMPLATES_DIR } from './route.js';
4
+ import { file, path, ui } from '@shopify/cli-kit';
5
+
6
+ describe("generate/route", () => {
7
+ beforeEach(() => {
8
+ vi.resetAllMocks();
9
+ vi.mock("@shopify/cli-kit", async () => {
10
+ const cliKit = await vi.importActual("@shopify/cli-kit");
11
+ return {
12
+ ...cliKit,
13
+ output: {
14
+ ...cliKit.output,
15
+ success: vi.fn()
16
+ },
17
+ ui: {
18
+ prompt: vi.fn()
19
+ }
20
+ };
21
+ });
22
+ });
23
+ it("generates a route file", async () => {
24
+ await temporaryDirectoryTask(async (tmpDir) => {
25
+ const route = "pages/$pageHandle";
26
+ const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
27
+ files: [],
28
+ templates: [[route, `const str = "hello world"`]]
29
+ });
30
+ await runGenerate(route, {
31
+ directory: appRoot,
32
+ templatesRoot
33
+ });
34
+ expect(
35
+ await file.read(path.join(appRoot, "app/routes", `${route}.jsx`))
36
+ ).toContain(`const str = 'hello world'`);
37
+ });
38
+ });
39
+ it("produces a typescript file when typescript argument is true", async () => {
40
+ await temporaryDirectoryTask(async (tmpDir) => {
41
+ const route = "pages/$pageHandle";
42
+ const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
43
+ files: [],
44
+ templates: [[route, 'const str = "hello typescript"']]
45
+ });
46
+ await runGenerate(route, {
47
+ directory: appRoot,
48
+ templatesRoot,
49
+ typescript: true
50
+ });
51
+ expect(
52
+ await file.read(path.join(appRoot, "app/routes", `${route}.tsx`))
53
+ ).toContain(`const str = 'hello typescript'`);
54
+ });
55
+ });
56
+ it("prompts the user if there the file already exists", async () => {
57
+ await temporaryDirectoryTask(async (tmpDir) => {
58
+ vi.mocked(ui.prompt).mockImplementationOnce(async () => {
59
+ return { value: "overwrite" };
60
+ });
61
+ const route = "page/$pageHandle";
62
+ const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
63
+ files: [[`app/routes/${route}.jsx`, 'const str = "I exist"']],
64
+ templates: [[route, 'const str = "hello world"']]
65
+ });
66
+ await runGenerate(route, {
67
+ directory: appRoot,
68
+ templatesRoot
69
+ });
70
+ expect(ui.prompt).toHaveBeenCalledWith(
71
+ expect.arrayContaining([
72
+ expect.objectContaining({
73
+ message: expect.stringContaining("already exists")
74
+ })
75
+ ])
76
+ );
77
+ });
78
+ });
79
+ it("does not prompt the user if the force property is true", async () => {
80
+ await temporaryDirectoryTask(async (tmpDir) => {
81
+ vi.mocked(ui.prompt).mockImplementationOnce(async () => {
82
+ return { value: "overwrite" };
83
+ });
84
+ const route = "page/$pageHandle";
85
+ const { appRoot, templatesRoot } = await createHydrogen(tmpDir, {
86
+ files: [[`app/routes/${route}.jsx`, 'const str = "I exist"']],
87
+ templates: [[route, 'const str = "hello world"']]
88
+ });
89
+ await runGenerate(route, {
90
+ directory: appRoot,
91
+ templatesRoot,
92
+ force: true
93
+ });
94
+ expect(ui.prompt).not.toHaveBeenCalled();
95
+ });
96
+ });
97
+ });
98
+ async function createHydrogen(directory, {
99
+ files,
100
+ templates
101
+ } = {
102
+ files: [],
103
+ templates: []
104
+ }) {
105
+ for (const item of files) {
106
+ const [filePath, fileContent] = item;
107
+ const fullFilePath = path.join(directory, "app", filePath);
108
+ await file.mkdir(path.dirname(fullFilePath));
109
+ await file.write(fullFilePath, fileContent);
110
+ }
111
+ for (const item of templates) {
112
+ const [filePath, fileContent] = item;
113
+ const fullFilePath = path.join(
114
+ directory,
115
+ GENERATOR_TEMPLATES_DIR,
116
+ "routes",
117
+ `${filePath}.tsx`
118
+ );
119
+ await file.mkdir(path.dirname(fullFilePath));
120
+ await file.write(fullFilePath, fileContent);
121
+ }
122
+ return {
123
+ appRoot: path.join(directory, "app"),
124
+ templatesRoot: directory
125
+ };
126
+ }