@shopify/cli-hydrogen 5.1.1 → 5.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/build.js +10 -5
- package/dist/commands/hydrogen/check.js +1 -1
- package/dist/commands/hydrogen/codegen-unstable.js +3 -3
- package/dist/commands/hydrogen/dev.js +26 -17
- package/dist/commands/hydrogen/init.js +3 -0
- package/dist/commands/hydrogen/init.test.js +199 -51
- package/dist/commands/hydrogen/preview.js +4 -3
- package/dist/commands/hydrogen/setup.js +5 -2
- package/dist/commands/hydrogen/setup.test.js +62 -0
- package/dist/generator-templates/starter/app/components/Footer.tsx +1 -1
- package/dist/generator-templates/starter/app/components/Header.tsx +1 -1
- package/dist/generator-templates/starter/app/components/Search.tsx +3 -3
- package/dist/generator-templates/starter/app/root.tsx +24 -1
- package/dist/generator-templates/starter/app/routes/$.tsx +4 -0
- package/dist/generator-templates/starter/app/routes/_index.tsx +6 -2
- package/dist/generator-templates/starter/app/routes/account.$.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/account.addresses.tsx +1 -1
- package/dist/generator-templates/starter/app/routes/account.orders._index.tsx +2 -7
- package/dist/generator-templates/starter/app/routes/account.profile.tsx +7 -2
- package/dist/generator-templates/starter/app/routes/account.tsx +4 -3
- package/dist/generator-templates/starter/app/routes/account_.activate.$id.$activationToken.tsx +6 -2
- package/dist/generator-templates/starter/app/routes/account_.login.tsx +6 -2
- package/dist/generator-templates/starter/app/routes/account_.logout.tsx +2 -6
- package/dist/generator-templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/blogs.$blogHandle._index.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/blogs._index.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/cart.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/collections.$handle.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/pages.$handle.tsx +1 -2
- package/dist/generator-templates/starter/app/routes/policies.$handle.tsx +2 -3
- package/dist/generator-templates/starter/app/routes/products.$handle.tsx +23 -15
- package/dist/generator-templates/starter/app/routes/search.tsx +1 -2
- package/dist/generator-templates/starter/package.json +4 -4
- package/dist/generator-templates/starter/remix.config.js +1 -0
- package/dist/generator-templates/starter/storefrontapi.generated.d.ts +9 -9
- package/dist/lib/ast.js +9 -0
- package/dist/lib/check-version.test.js +1 -0
- package/dist/lib/codegen.js +17 -7
- package/dist/lib/environment-variables.js +15 -11
- package/dist/lib/file.test.js +4 -5
- package/dist/lib/find-port.js +9 -0
- package/dist/lib/flags.js +3 -2
- package/dist/lib/format-code.js +3 -0
- package/dist/lib/live-reload.js +62 -0
- package/dist/lib/log.js +6 -1
- package/dist/lib/mini-oxygen.js +28 -18
- package/dist/lib/missing-routes.js +17 -1
- package/dist/lib/onboarding/common.js +5 -0
- package/dist/lib/onboarding/local.js +21 -8
- package/dist/lib/onboarding/remote.js +8 -3
- package/dist/lib/remix-config.js +2 -0
- package/dist/lib/remix-version-check.test.js +1 -0
- package/dist/lib/setups/css/replacers.js +7 -4
- package/dist/lib/setups/i18n/replacers.js +7 -5
- package/dist/lib/setups/routes/generate.js +4 -1
- package/dist/lib/setups/routes/generate.test.js +10 -11
- package/dist/lib/template-downloader.js +4 -0
- package/dist/lib/transpile-ts.js +1 -0
- package/dist/lib/virtual-routes.js +4 -1
- package/dist/virtual-routes/components/HydrogenLogoBaseBW.jsx +26 -4
- package/dist/virtual-routes/components/HydrogenLogoBaseColor.jsx +40 -10
- package/dist/virtual-routes/components/IconBanner.jsx +286 -44
- package/dist/virtual-routes/components/IconDiscord.jsx +17 -1
- package/dist/virtual-routes/components/IconError.jsx +55 -17
- package/dist/virtual-routes/components/IconGithub.jsx +19 -1
- package/dist/virtual-routes/components/IconTwitter.jsx +17 -1
- package/dist/virtual-routes/components/Layout.jsx +1 -1
- package/dist/virtual-routes/routes/index.jsx +110 -94
- package/dist/virtual-routes/virtual-root.jsx +7 -15
- package/oclif.manifest.json +1 -1
- package/package.json +9 -8
package/dist/lib/codegen.js
CHANGED
|
@@ -2,7 +2,7 @@ import { loadCodegenConfig, generate } from '@graphql-codegen/cli';
|
|
|
2
2
|
import { patchGqlPluck, pluckConfig, preset, schema } from '@shopify/hydrogen-codegen';
|
|
3
3
|
import { getCodeFormatOptions, formatCode } from './format-code.js';
|
|
4
4
|
import { renderWarning, renderFatalError } from '@shopify/cli-kit/node/ui';
|
|
5
|
-
import { joinPath } from '@shopify/cli-kit/node/path';
|
|
5
|
+
import { relativePath, joinPath } from '@shopify/cli-kit/node/path';
|
|
6
6
|
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -82,16 +82,22 @@ async function generateTypes({
|
|
|
82
82
|
forceSfapiVersion,
|
|
83
83
|
...dirs
|
|
84
84
|
}) {
|
|
85
|
-
const { config: codegenConfig } =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
const { config: codegenConfig } = (
|
|
86
|
+
// Load <root>/codegen.ts if available
|
|
87
|
+
await loadCodegenConfig({
|
|
88
|
+
configFilePath,
|
|
89
|
+
searchPlaces: [dirs.rootDirectory]
|
|
90
|
+
}) || // Fall back to default config
|
|
91
|
+
generateDefaultConfig(dirs, forceSfapiVersion)
|
|
92
|
+
);
|
|
89
93
|
await addHooksToHydrogenOptions(codegenConfig, dirs);
|
|
90
94
|
await generate(
|
|
91
95
|
{
|
|
92
96
|
...codegenConfig,
|
|
93
97
|
cwd: dirs.rootDirectory,
|
|
94
98
|
watch,
|
|
99
|
+
// Note: do not use `silent` without `watch`, it will swallow errors and
|
|
100
|
+
// won't hide all logs. `errorsOnly` flag doesn't work either.
|
|
95
101
|
silent: !watch
|
|
96
102
|
},
|
|
97
103
|
true
|
|
@@ -100,6 +106,7 @@ async function generateTypes({
|
|
|
100
106
|
}
|
|
101
107
|
function generateDefaultConfig({ rootDirectory, appDirectory }, forceSfapiVersion) {
|
|
102
108
|
const tsDefaultGlob = "*!(*.d).{ts,tsx}";
|
|
109
|
+
const appDirRelative = relativePath(rootDirectory, appDirectory);
|
|
103
110
|
return {
|
|
104
111
|
filepath: "virtual:codegen",
|
|
105
112
|
config: {
|
|
@@ -110,8 +117,10 @@ function generateDefaultConfig({ rootDirectory, appDirectory }, forceSfapiVersio
|
|
|
110
117
|
preset,
|
|
111
118
|
schema,
|
|
112
119
|
documents: [
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
tsDefaultGlob,
|
|
121
|
+
// E.g. ./server.ts
|
|
122
|
+
joinPath(appDirRelative, "**", tsDefaultGlob)
|
|
123
|
+
// E.g. app/routes/_index.tsx
|
|
115
124
|
],
|
|
116
125
|
...!!forceSfapiVersion && {
|
|
117
126
|
presetConfig: { importTypes: false },
|
|
@@ -142,6 +151,7 @@ async function addHooksToHydrogenOptions(codegenConfig, { rootDirectory }) {
|
|
|
142
151
|
const formatConfig = await getCodeFormatOptions(rootDirectory);
|
|
143
152
|
hydrogenOptions.hooks = {
|
|
144
153
|
beforeOneFileWrite: (file, content) => formatCode(content, formatConfig, file),
|
|
154
|
+
// Run Prettier before writing files
|
|
145
155
|
...hydrogenOptions.hooks
|
|
146
156
|
};
|
|
147
157
|
}
|
|
@@ -8,6 +8,10 @@ import colors from '@shopify/cli-kit/node/colors';
|
|
|
8
8
|
import { getStorefrontEnvVariables } from './graphql/admin/pull-variables.js';
|
|
9
9
|
import { login } from './auth.js';
|
|
10
10
|
|
|
11
|
+
const createEmptyRemoteVars = () => ({
|
|
12
|
+
remoteVariables: {},
|
|
13
|
+
remoteSecrets: {}
|
|
14
|
+
});
|
|
11
15
|
async function getAllEnvironmentVariables({
|
|
12
16
|
root,
|
|
13
17
|
envBranch,
|
|
@@ -15,7 +19,15 @@ async function getAllEnvironmentVariables({
|
|
|
15
19
|
}) {
|
|
16
20
|
const dotEnvPath = resolvePath(root, ".env");
|
|
17
21
|
const [{ remoteVariables, remoteSecrets }, { variables: localVariables }] = await Promise.all([
|
|
18
|
-
|
|
22
|
+
// Get remote vars
|
|
23
|
+
fetchRemote ? getRemoteVariables(root, envBranch).catch((error) => {
|
|
24
|
+
renderWarning({
|
|
25
|
+
headline: "Failed to load environment variables from Shopify. The development server will still start, but the following error occurred:",
|
|
26
|
+
body: [error.message, error.tryMessage, error.nextSteps].filter(Boolean).join("\n\n")
|
|
27
|
+
});
|
|
28
|
+
return createEmptyRemoteVars();
|
|
29
|
+
}) : createEmptyRemoteVars(),
|
|
30
|
+
// Get local vars
|
|
19
31
|
fileExists(dotEnvPath).then(
|
|
20
32
|
(exists) => exists ? readAndParseDotEnv(dotEnvPath) : { variables: {} }
|
|
21
33
|
)
|
|
@@ -29,6 +41,7 @@ async function getAllEnvironmentVariables({
|
|
|
29
41
|
linesToColumns([
|
|
30
42
|
...remotePublicKeys.filter((key) => !localKeys.includes(key)).map((key) => [key, "from Oxygen"]),
|
|
31
43
|
...localKeys.map((key) => [key, "from local .env"]),
|
|
44
|
+
// Ensure secret variables always get added to the bottom of the list
|
|
32
45
|
...remoteSecretKeys.filter((key) => !localKeys.includes(key)).map((key) => [
|
|
33
46
|
colors.dim(key),
|
|
34
47
|
colors.dim("from Oxygen (Marked as secret)")
|
|
@@ -44,16 +57,7 @@ async function getAllEnvironmentVariables({
|
|
|
44
57
|
}
|
|
45
58
|
async function getRemoteVariables(root, envBranch) {
|
|
46
59
|
const { session, config } = await login(root);
|
|
47
|
-
const envVariables = (await getStorefrontEnvVariables(
|
|
48
|
-
session,
|
|
49
|
-
config.storefront.id,
|
|
50
|
-
envBranch
|
|
51
|
-
).catch((error) => {
|
|
52
|
-
renderWarning({
|
|
53
|
-
headline: `Failed to load environment variables. The development server will still start, but the following error occurred:`,
|
|
54
|
-
body: error?.stack ?? error?.message ?? error
|
|
55
|
-
});
|
|
56
|
-
}))?.environmentVariables || [];
|
|
60
|
+
const envVariables = (await getStorefrontEnvVariables(session, config.storefront.id, envBranch))?.environmentVariables || [];
|
|
57
61
|
const remoteVariables = {};
|
|
58
62
|
const remoteSecrets = {};
|
|
59
63
|
for (const { key, value, isSecret } of envVariables) {
|
package/dist/lib/file.test.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { temporaryDirectoryTask } from 'tempy';
|
|
2
1
|
import { describe, it, expect } from 'vitest';
|
|
3
2
|
import { replaceFileContent, findFileWithExtension } from './file.js';
|
|
4
3
|
import { resolvePath } from '@shopify/cli-kit/node/path';
|
|
5
|
-
import { writeFile, readFile, mkdir } from '@shopify/cli-kit/node/fs';
|
|
4
|
+
import { inTemporaryDirectory, writeFile, readFile, mkdir } from '@shopify/cli-kit/node/fs';
|
|
6
5
|
|
|
7
6
|
describe("File utils", () => {
|
|
8
7
|
describe("replaceFileContent", () => {
|
|
9
8
|
it("replaces the content of a file and formats it", async () => {
|
|
10
|
-
await
|
|
9
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
11
10
|
const filepath = resolvePath(tmpDir, "index.js");
|
|
12
11
|
await writeFile(
|
|
13
12
|
filepath,
|
|
@@ -24,7 +23,7 @@ describe("File utils", () => {
|
|
|
24
23
|
});
|
|
25
24
|
describe("findFileWithExtension", () => {
|
|
26
25
|
it("ignores missing files", async () => {
|
|
27
|
-
await
|
|
26
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
28
27
|
expect(findFileWithExtension(tmpDir, "nope")).resolves.toEqual({
|
|
29
28
|
filepath: void 0,
|
|
30
29
|
extension: void 0,
|
|
@@ -33,7 +32,7 @@ describe("File utils", () => {
|
|
|
33
32
|
});
|
|
34
33
|
});
|
|
35
34
|
it("finds the file with its corresponding extension and astType", async () => {
|
|
36
|
-
await
|
|
35
|
+
await inTemporaryDirectory(async (tmpDir) => {
|
|
37
36
|
await writeFile(resolvePath(tmpDir, "first.js"), "content");
|
|
38
37
|
await writeFile(resolvePath(tmpDir, "second.tsx"), "content");
|
|
39
38
|
await writeFile(resolvePath(tmpDir, "third.mjs"), "content");
|
package/dist/lib/flags.js
CHANGED
|
@@ -6,6 +6,7 @@ import colors from '@shopify/cli-kit/node/colors';
|
|
|
6
6
|
import { SETUP_CSS_STRATEGIES } from './setups/css/index.js';
|
|
7
7
|
import { I18N_CHOICES } from './setups/i18n/index.js';
|
|
8
8
|
|
|
9
|
+
const DEFAULT_PORT = 3e3;
|
|
9
10
|
const commonFlags = {
|
|
10
11
|
path: Flags.string({
|
|
11
12
|
description: "The path to the directory of the Hydrogen storefront. The default is the current directory.",
|
|
@@ -14,7 +15,7 @@ const commonFlags = {
|
|
|
14
15
|
port: Flags.integer({
|
|
15
16
|
description: "Port to run the server on.",
|
|
16
17
|
env: "SHOPIFY_HYDROGEN_FLAG_PORT",
|
|
17
|
-
default:
|
|
18
|
+
default: DEFAULT_PORT
|
|
18
19
|
}),
|
|
19
20
|
force: Flags.boolean({
|
|
20
21
|
description: "Overwrite the destination directory and files if they already exist.",
|
|
@@ -112,4 +113,4 @@ function overrideFlag(flag, extra) {
|
|
|
112
113
|
};
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
export { commonFlags, deprecated, flagsToCamelObject, overrideFlag, parseProcessFlags };
|
|
116
|
+
export { DEFAULT_PORT, commonFlags, deprecated, flagsToCamelObject, overrideFlag, parseProcessFlags };
|
package/dist/lib/format-code.js
CHANGED
|
@@ -17,6 +17,9 @@ async function getCodeFormatOptions(filePath = process.cwd()) {
|
|
|
17
17
|
function formatCode(content, config = DEFAULT_PRETTIER_CONFIG, filePath = "") {
|
|
18
18
|
const ext = extname(filePath);
|
|
19
19
|
const formattedContent = prettier.format(content, {
|
|
20
|
+
// Specify the TypeScript parser for ts/tsx files. Otherwise
|
|
21
|
+
// we need to use the babel parser because the default parser
|
|
22
|
+
// Otherwise prettier will print a warning.
|
|
20
23
|
parser: ext === ".tsx" || ext === ".ts" ? "typescript" : "babel",
|
|
21
24
|
...config
|
|
22
25
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
|
|
3
|
+
async function setupLiveReload(devServerPort) {
|
|
4
|
+
try {
|
|
5
|
+
const [{ updates: hmrUpdates }, { serve }, { detectLoaderChanges }, { ok, err }] = await Promise.all([
|
|
6
|
+
import('@remix-run/dev/dist/devServer_unstable/hmr.js'),
|
|
7
|
+
import('@remix-run/dev/dist/devServer_unstable/socket.js'),
|
|
8
|
+
import('@remix-run/dev/dist/devServer_unstable/hdr.js'),
|
|
9
|
+
import('@remix-run/dev/dist/result.js')
|
|
10
|
+
]);
|
|
11
|
+
const state = {};
|
|
12
|
+
const server = http.createServer(function(req, res) {
|
|
13
|
+
res.writeHead(200);
|
|
14
|
+
res.end();
|
|
15
|
+
}).listen(devServerPort);
|
|
16
|
+
const socket = serve(server);
|
|
17
|
+
return {
|
|
18
|
+
onBuildStart: (ctx) => {
|
|
19
|
+
state.loaderChanges = detectLoaderChanges(ctx).then(ok, err);
|
|
20
|
+
},
|
|
21
|
+
onBuildManifest: (manifest) => {
|
|
22
|
+
state.manifest = manifest;
|
|
23
|
+
},
|
|
24
|
+
onAppReady: async (ctx) => {
|
|
25
|
+
const nextState = { prevManifest: state.manifest };
|
|
26
|
+
try {
|
|
27
|
+
const loaderChanges = await state.loaderChanges;
|
|
28
|
+
if (loaderChanges.ok) {
|
|
29
|
+
nextState.prevLoaderHashes = loaderChanges.value;
|
|
30
|
+
}
|
|
31
|
+
if (loaderChanges.ok && state.manifest && state.prevManifest) {
|
|
32
|
+
socket.hmr(
|
|
33
|
+
state.manifest,
|
|
34
|
+
hmrUpdates(
|
|
35
|
+
ctx.config,
|
|
36
|
+
state.manifest,
|
|
37
|
+
state.prevManifest,
|
|
38
|
+
loaderChanges.value,
|
|
39
|
+
state.prevLoaderHashes
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
} else if (state.prevManifest) {
|
|
43
|
+
socket.reload();
|
|
44
|
+
}
|
|
45
|
+
} finally {
|
|
46
|
+
Object.assign(state, nextState);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
close: () => {
|
|
50
|
+
socket.close();
|
|
51
|
+
server.close();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn(
|
|
56
|
+
"Could not start HMR server. Please make sure your Remix packages are in sync with Hydrogen. Defaulting to regular live reload.",
|
|
57
|
+
error.stack
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { setupLiveReload };
|
package/dist/lib/log.js
CHANGED
|
@@ -121,7 +121,10 @@ function enhanceH2Logs(options) {
|
|
|
121
121
|
injectLogReplacer("error");
|
|
122
122
|
injectLogReplacer(
|
|
123
123
|
"warn",
|
|
124
|
-
([first]) =>
|
|
124
|
+
([first]) => (
|
|
125
|
+
// Show createStorefrontClient warnings only once.
|
|
126
|
+
first?.includes?.("[h2:warn:createStorefrontClient]") ? true : void 0
|
|
127
|
+
)
|
|
125
128
|
);
|
|
126
129
|
addMessageReplacers("h2-warn", [
|
|
127
130
|
([first]) => {
|
|
@@ -164,7 +167,9 @@ function enhanceH2Logs(options) {
|
|
|
164
167
|
if (firstAppLineIndex > 0 && lastAppLineIndex > firstAppLineIndex) {
|
|
165
168
|
stack = [
|
|
166
169
|
stackLines[0],
|
|
170
|
+
// Error message
|
|
167
171
|
...stackLines.slice(firstAppLineIndex, lastAppLineIndex)
|
|
172
|
+
// App code
|
|
168
173
|
].join("\n").trim() || void 0;
|
|
169
174
|
}
|
|
170
175
|
const error = new BugError(
|
package/dist/lib/mini-oxygen.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { outputToken, outputInfo, outputContent } from '@shopify/cli-kit/node/output';
|
|
2
2
|
import { resolvePath } from '@shopify/cli-kit/node/path';
|
|
3
|
-
import { fileExists } from '@shopify/cli-kit/node/fs';
|
|
3
|
+
import { readFile, fileExists } from '@shopify/cli-kit/node/fs';
|
|
4
4
|
import colors from '@shopify/cli-kit/node/colors';
|
|
5
5
|
import { renderSuccess } from '@shopify/cli-kit/node/ui';
|
|
6
|
+
import { startServer } from '@shopify/mini-oxygen';
|
|
7
|
+
import { DEFAULT_PORT } from './flags.js';
|
|
6
8
|
|
|
7
9
|
async function startMiniOxygen({
|
|
8
10
|
root,
|
|
9
|
-
port =
|
|
11
|
+
port = DEFAULT_PORT,
|
|
10
12
|
watch = false,
|
|
13
|
+
autoReload = watch,
|
|
11
14
|
buildPathWorkerFile,
|
|
12
15
|
buildPathClient,
|
|
13
16
|
env
|
|
14
17
|
}) {
|
|
15
|
-
const { default: miniOxygenImport } = await import('@shopify/mini-oxygen');
|
|
16
|
-
const miniOxygenPreview = miniOxygenImport.default ?? miniOxygenImport;
|
|
17
18
|
const dotenvPath = resolvePath(root, ".env");
|
|
18
|
-
const miniOxygen = await
|
|
19
|
-
|
|
19
|
+
const miniOxygen = await startServer({
|
|
20
|
+
script: await readFile(buildPathWorkerFile),
|
|
20
21
|
assetsDir: buildPathClient,
|
|
21
22
|
publicPath: "",
|
|
22
23
|
port,
|
|
23
24
|
watch,
|
|
24
|
-
autoReload
|
|
25
|
+
autoReload,
|
|
25
26
|
modules: true,
|
|
26
27
|
env: {
|
|
27
28
|
...env,
|
|
@@ -30,23 +31,31 @@ async function startMiniOxygen({
|
|
|
30
31
|
envPath: !env && await fileExists(dotenvPath) ? dotenvPath : void 0,
|
|
31
32
|
log: () => {
|
|
32
33
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
onResponse: (request, response) => (
|
|
35
|
+
// 'Request' and 'Response' types in MiniOxygen comes from
|
|
36
|
+
// Miniflare and are slightly different from standard types.
|
|
37
|
+
logResponse(
|
|
38
|
+
request,
|
|
39
|
+
response
|
|
40
|
+
)
|
|
37
41
|
)
|
|
38
42
|
});
|
|
39
43
|
const listeningAt = `http://localhost:${miniOxygen.port}`;
|
|
40
44
|
return {
|
|
41
45
|
listeningAt,
|
|
42
46
|
port: miniOxygen.port,
|
|
43
|
-
reload(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
async reload(options = {}) {
|
|
48
|
+
const nextOptions = {};
|
|
49
|
+
if (options.env) {
|
|
50
|
+
nextOptions.env = {
|
|
51
|
+
...options.env,
|
|
47
52
|
...process.env
|
|
48
|
-
}
|
|
49
|
-
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (options.worker) {
|
|
56
|
+
nextOptions.script = await readFile(buildPathWorkerFile);
|
|
57
|
+
}
|
|
58
|
+
return miniOxygen.reload(nextOptions);
|
|
50
59
|
},
|
|
51
60
|
showBanner(options) {
|
|
52
61
|
console.log("");
|
|
@@ -64,8 +73,9 @@ async function startMiniOxygen({
|
|
|
64
73
|
function logResponse(request, response) {
|
|
65
74
|
try {
|
|
66
75
|
const url = new URL(request.url);
|
|
67
|
-
if (["/graphiql"].includes(url.pathname))
|
|
76
|
+
if (["/graphiql"].includes(url.pathname)) {
|
|
68
77
|
return;
|
|
78
|
+
}
|
|
69
79
|
const isProxy = !!response.url && response.url !== request.url;
|
|
70
80
|
const isDataRequest = !isProxy && url.searchParams.has("_data");
|
|
71
81
|
let route = request.url.replace(url.origin, "");
|
|
@@ -3,20 +3,34 @@ import { renderWarning, renderSuccess } from '@shopify/cli-kit/node/ui';
|
|
|
3
3
|
const REQUIRED_ROUTES = [
|
|
4
4
|
"",
|
|
5
5
|
"cart",
|
|
6
|
+
// 'products',
|
|
6
7
|
"products/:productHandle",
|
|
7
8
|
"collections",
|
|
9
|
+
// 'collections/all',
|
|
8
10
|
"collections/:collectionHandle",
|
|
11
|
+
// 'collections/:collectionHandle/:constraint',
|
|
12
|
+
// 'collections/:collectionHandle/products/:productHandle',
|
|
9
13
|
"sitemap.xml",
|
|
10
14
|
"robots.txt",
|
|
11
15
|
"pages/:pageHandle",
|
|
16
|
+
// 'blogs/:blogHandle/tagged/:tagHandle',
|
|
17
|
+
// 'blogs/:blogHandle/:articleHandle',
|
|
18
|
+
// 'blogs/:blogHandle/:articleHandle/comments',
|
|
12
19
|
"policies/:policyHandle",
|
|
20
|
+
// 'variants/:variantId',
|
|
13
21
|
"search",
|
|
22
|
+
// 'gift_cards/:storeId/:cardId',
|
|
23
|
+
// 'discount/:discountCode', => Handled in storefrontRedirect
|
|
14
24
|
"account",
|
|
15
25
|
"account/login",
|
|
16
26
|
"account/register",
|
|
27
|
+
// 'account/addresses',
|
|
28
|
+
// 'account/orders',
|
|
17
29
|
"account/orders/:orderId",
|
|
18
30
|
"account/reset/:id/:token",
|
|
19
31
|
"account/activate/:id/:token"
|
|
32
|
+
// 'password',
|
|
33
|
+
// 'opening_soon',
|
|
20
34
|
];
|
|
21
35
|
function findMissingRoutes(config, requiredRoutes = REQUIRED_ROUTES) {
|
|
22
36
|
const userRoutes = Object.values(config.routes);
|
|
@@ -40,7 +54,9 @@ function findMissingRoutes(config, requiredRoutes = REQUIRED_ROUTES) {
|
|
|
40
54
|
currentRoute.parentId = parentRoute.parentId;
|
|
41
55
|
}
|
|
42
56
|
const optionalSegment = ":?[^\\/\\?]+\\?";
|
|
43
|
-
const reString = `^(${optionalSegment}\\/)?` +
|
|
57
|
+
const reString = `^(${optionalSegment}\\/)?` + // Starts with an optional segment
|
|
58
|
+
requiredRoute.replaceAll(".", "\\.").replace(/\//g, `\\/(${optionalSegment}\\/)?`).replace(/:[^/)?]+/g, ":[^\\/]+") + // Replace params with regex
|
|
59
|
+
`(\\/${optionalSegment})?$`;
|
|
44
60
|
if (new RegExp(reString).test(currentRoute.path)) {
|
|
45
61
|
missingRoutes.delete(requiredRoute);
|
|
46
62
|
}
|
|
@@ -163,7 +163,9 @@ async function handleProjectLocation({
|
|
|
163
163
|
return {
|
|
164
164
|
name: basename(location),
|
|
165
165
|
location,
|
|
166
|
+
// User input. E.g. "./hydrogen-storefront"
|
|
166
167
|
directory,
|
|
168
|
+
// Absolute path to location
|
|
167
169
|
storefrontTitle: storefrontInfo?.title
|
|
168
170
|
};
|
|
169
171
|
}
|
|
@@ -201,6 +203,7 @@ async function handleCssStrategy(projectDir, controller, flagStyling) {
|
|
|
201
203
|
{
|
|
202
204
|
rootDirectory: projectDir,
|
|
203
205
|
appDirectory: joinPath(projectDir, "app")
|
|
206
|
+
// Default value in new projects
|
|
204
207
|
},
|
|
205
208
|
true
|
|
206
209
|
);
|
|
@@ -344,6 +347,8 @@ async function renderProjectReady(project, {
|
|
|
344
347
|
body: bodyLines.map(
|
|
345
348
|
([label, value]) => ` ${(label + ":").padEnd(padMin, " ")} ${colors.dim(value)}`
|
|
346
349
|
).join("\n") + routeSummary,
|
|
350
|
+
// Use `customSections` instead of `nextSteps` and `references`
|
|
351
|
+
// here to enforce a newline between title and items.
|
|
347
352
|
customSections: [
|
|
348
353
|
hasErrors && {
|
|
349
354
|
title: "Warnings\n",
|
|
@@ -18,7 +18,7 @@ async function setupLocalStarterTemplate(options, controller) {
|
|
|
18
18
|
message: "Connect to Shopify",
|
|
19
19
|
choices: [
|
|
20
20
|
{
|
|
21
|
-
label: "Use sample data from
|
|
21
|
+
label: "Use sample data from mock.shop (You can connect a Shopify account later)",
|
|
22
22
|
value: "mock"
|
|
23
23
|
},
|
|
24
24
|
{ label: "Link your Shopify account", value: "link" }
|
|
@@ -46,17 +46,22 @@ async function setupLocalStarterTemplate(options, controller) {
|
|
|
46
46
|
let backgroundWorkPromise = copy(
|
|
47
47
|
templateDir,
|
|
48
48
|
project.directory,
|
|
49
|
+
// Filter out the `app` directory, which will be generated later
|
|
49
50
|
{
|
|
50
51
|
filter: (filepath) => !/^(app|dist|node_modules)\//i.test(
|
|
51
52
|
relativePath(templateDir, filepath)
|
|
52
53
|
)
|
|
53
54
|
}
|
|
54
55
|
).then(
|
|
55
|
-
() =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
() => (
|
|
57
|
+
// Generate project entries and their file dependencies
|
|
58
|
+
generateProjectEntries({
|
|
59
|
+
rootDirectory: project.directory,
|
|
60
|
+
appDirectory: joinPath(project.directory, "app"),
|
|
61
|
+
typescript: true
|
|
62
|
+
// Will be transpiled later
|
|
63
|
+
})
|
|
64
|
+
)
|
|
60
65
|
).catch(abort);
|
|
61
66
|
const tasks = [
|
|
62
67
|
{
|
|
@@ -74,6 +79,7 @@ async function setupLocalStarterTemplate(options, controller) {
|
|
|
74
79
|
];
|
|
75
80
|
backgroundWorkPromise = backgroundWorkPromise.then(() => {
|
|
76
81
|
const promises = [
|
|
82
|
+
// Add project name to package.json
|
|
77
83
|
replaceFileContent(
|
|
78
84
|
joinPath(project.directory, "package.json"),
|
|
79
85
|
false,
|
|
@@ -83,17 +89,23 @@ async function setupLocalStarterTemplate(options, controller) {
|
|
|
83
89
|
)
|
|
84
90
|
)
|
|
85
91
|
];
|
|
86
|
-
const envLeadingComment = "# The variables added in this file are only available locally in MiniOxygen
|
|
92
|
+
const envLeadingComment = "# The variables added in this file are only available locally in MiniOxygen.\n# Run `h2 link` to also inject environment variables from your storefront,\n# or `h2 env pull` to populate this file.";
|
|
87
93
|
if (storefrontInfo && createStorefrontPromise) {
|
|
88
94
|
promises.push(
|
|
95
|
+
// Save linked storefront in project
|
|
89
96
|
setUserAccount(project.directory, storefrontInfo),
|
|
90
97
|
createStorefrontPromise.then(
|
|
91
|
-
(storefront) =>
|
|
98
|
+
(storefront) => (
|
|
99
|
+
// Save linked storefront in project
|
|
100
|
+
setStorefront(project.directory, storefront)
|
|
101
|
+
)
|
|
92
102
|
),
|
|
103
|
+
// Write empty dotenv file to fallback to remote Oxygen variables
|
|
93
104
|
writeFile(joinPath(project.directory, ".env"), envLeadingComment)
|
|
94
105
|
);
|
|
95
106
|
} else if (templateAction === "mock") {
|
|
96
107
|
promises.push(
|
|
108
|
+
// Set required env vars
|
|
97
109
|
writeFile(
|
|
98
110
|
joinPath(project.directory, ".env"),
|
|
99
111
|
envLeadingComment + "\n" + [
|
|
@@ -188,6 +200,7 @@ async function setupLocalStarterTemplate(options, controller) {
|
|
|
188
200
|
const { setupRoutes } = await handleRouteGeneration(
|
|
189
201
|
controller,
|
|
190
202
|
options.routes || true
|
|
203
|
+
// TODO: Remove default value when multi-select UI component is available
|
|
191
204
|
);
|
|
192
205
|
setupSummary.i18n = i18nStrategy;
|
|
193
206
|
backgroundWorkPromise = backgroundWorkPromise.then(async () => {
|
|
@@ -3,7 +3,7 @@ import { copyFile } from '@shopify/cli-kit/node/fs';
|
|
|
3
3
|
import { joinPath } from '@shopify/cli-kit/node/path';
|
|
4
4
|
import { renderTasks, renderInfo } from '@shopify/cli-kit/node/ui';
|
|
5
5
|
import { getLatestTemplates } from '../template-downloader.js';
|
|
6
|
-
import { handleProjectLocation, createAbortHandler, handleLanguage, createInitialCommit, handleDependencies, renderProjectReady } from './common.js';
|
|
6
|
+
import { handleProjectLocation, createAbortHandler, handleLanguage, createInitialCommit, handleDependencies, commitAll, renderProjectReady } from './common.js';
|
|
7
7
|
|
|
8
8
|
async function setupRemoteTemplate(options, controller) {
|
|
9
9
|
const isOfficialTemplate = options.template === "demo-store" || options.template === "hello-world";
|
|
@@ -33,7 +33,9 @@ async function setupRemoteTemplate(options, controller) {
|
|
|
33
33
|
controller,
|
|
34
34
|
options.language
|
|
35
35
|
);
|
|
36
|
-
backgroundWorkPromise = backgroundWorkPromise.then(() => transpileProject().catch(abort)).then(
|
|
36
|
+
backgroundWorkPromise = backgroundWorkPromise.then(() => transpileProject().catch(abort)).then(
|
|
37
|
+
() => options.git ? createInitialCommit(project.directory) : void 0
|
|
38
|
+
);
|
|
37
39
|
const { packageManager, shouldInstallDeps, installDeps } = await handleDependencies(
|
|
38
40
|
project.directory,
|
|
39
41
|
controller,
|
|
@@ -73,10 +75,13 @@ async function setupRemoteTemplate(options, controller) {
|
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
await renderTasks(tasks);
|
|
78
|
+
if (options.git) {
|
|
79
|
+
await commitAll(project.directory, "Lockfile");
|
|
80
|
+
}
|
|
76
81
|
await renderProjectReady(project, setupSummary);
|
|
77
82
|
if (isOfficialTemplate) {
|
|
78
83
|
renderInfo({
|
|
79
|
-
headline: `Your project will display inventory from the Hydrogen Demo Store.`,
|
|
84
|
+
headline: `Your project will display inventory from ${options.template === "demo-store" ? "the Hydrogen Demo Store" : "Mock.shop"}.`,
|
|
80
85
|
body: `To connect this project to your Shopify store\u2019s inventory, update \`${project.name}/.env\` with your store ID and Storefront API key.`
|
|
81
86
|
});
|
|
82
87
|
}
|
package/dist/lib/remix-config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readdir } from 'node:fs/promises';
|
|
|
5
5
|
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
6
6
|
import { outputWarn } from '@shopify/cli-kit/node/output';
|
|
7
7
|
import { fileExists } from '@shopify/cli-kit/node/fs';
|
|
8
|
+
import { muteRemixLogs } from './log.js';
|
|
8
9
|
|
|
9
10
|
const BUILD_DIR = "dist";
|
|
10
11
|
const CLIENT_SUBDIR = "client";
|
|
@@ -25,6 +26,7 @@ function getProjectPaths(appPath, entry) {
|
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
async function getRemixConfig(root, mode = process.env.NODE_ENV) {
|
|
29
|
+
await muteRemixLogs();
|
|
28
30
|
const { readConfig } = await import('@remix-run/dev/dist/config.js');
|
|
29
31
|
const config = await readConfig(root, mode);
|
|
30
32
|
if (process.env.LOCAL_DEV) {
|
|
@@ -26,6 +26,7 @@ describe("remix-version-check", () => {
|
|
|
26
26
|
it("warns when versions are out of sync", () => {
|
|
27
27
|
const expectedVersion = "42.0.0-test";
|
|
28
28
|
vi.mocked(requireMock).mockReturnValueOnce({
|
|
29
|
+
// Hydrogen expected version
|
|
29
30
|
dependencies: { "@remix-run/dev": expectedVersion }
|
|
30
31
|
});
|
|
31
32
|
const outputMock = mockAndCaptureOutput();
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
2
|
-
import { ts, tsx, js, jsx } from '@ast-grep/napi';
|
|
3
2
|
import { findFileWithExtension, replaceFileContent } from '../../file.js';
|
|
3
|
+
import { importLangAstGrep } from '../../ast.js';
|
|
4
4
|
|
|
5
|
-
const astGrep = { ts, tsx, js, jsx };
|
|
6
5
|
async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
|
|
7
6
|
const { filepath, astType } = await findFileWithExtension(
|
|
8
7
|
rootDirectory,
|
|
@@ -14,7 +13,8 @@ async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
|
|
|
14
13
|
);
|
|
15
14
|
}
|
|
16
15
|
await replaceFileContent(filepath, formatConfig, async (content) => {
|
|
17
|
-
const
|
|
16
|
+
const astGrep = await importLangAstGrep(astType);
|
|
17
|
+
const root = astGrep.parse(content).root();
|
|
18
18
|
const remixConfigNode = root.find({
|
|
19
19
|
rule: {
|
|
20
20
|
kind: "object",
|
|
@@ -22,9 +22,11 @@ async function replaceRemixConfig(rootDirectory, formatConfig, newProperties) {
|
|
|
22
22
|
any: [
|
|
23
23
|
{
|
|
24
24
|
kind: "export_statement"
|
|
25
|
+
// ESM
|
|
25
26
|
},
|
|
26
27
|
{
|
|
27
28
|
kind: "assignment_expression",
|
|
29
|
+
// CJS
|
|
28
30
|
has: {
|
|
29
31
|
kind: "member_expression",
|
|
30
32
|
field: "left",
|
|
@@ -77,7 +79,8 @@ async function replaceRootLinks(appDirectory, formatConfig, importer) {
|
|
|
77
79
|
if (content.includes(importStatement.split("from")[0])) {
|
|
78
80
|
return;
|
|
79
81
|
}
|
|
80
|
-
const
|
|
82
|
+
const astGrep = await importLangAstGrep(astType);
|
|
83
|
+
const root = astGrep.parse(content).root();
|
|
81
84
|
const lastImportNode = root.findAll({ rule: { kind: "import_statement" } }).pop();
|
|
82
85
|
const linksReturnNode = root.find({
|
|
83
86
|
utils: {
|