tiendu 0.5.0 → 0.6.1
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 +2 -0
- package/bin/tiendu.js +43 -16
- package/lib/api.mjs +84 -0
- package/lib/build.mjs +3 -3
- package/lib/dev.mjs +50 -63
- package/lib/local-preview.mjs +53 -10
- package/lib/preview.mjs +260 -64
- package/lib/publish.mjs +32 -17
- package/lib/pull.mjs +37 -12
- package/lib/push.mjs +25 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,8 @@ The build:
|
|
|
100
100
|
4. Bundles JS/TS and CSS into `dist/assets/`
|
|
101
101
|
5. Runs project PostCSS plugins for CSS entries when available (for example Tailwind v4)
|
|
102
102
|
|
|
103
|
+
For TypeScript source, extensionless relative imports such as `import { initHeaderCart } from '../lib/scripts/cart'` are supported and recommended.
|
|
104
|
+
|
|
103
105
|
Entry naming convention:
|
|
104
106
|
|
|
105
107
|
- `src/layout/theme.ts` → `dist/assets/layout-theme.bundle.js`
|
package/bin/tiendu.js
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
previewList,
|
|
13
13
|
previewDelete,
|
|
14
14
|
previewOpen,
|
|
15
|
+
previewAttach,
|
|
16
|
+
previewDetach,
|
|
15
17
|
} from "../lib/preview.mjs";
|
|
16
18
|
import {
|
|
17
19
|
checkForUpdates,
|
|
@@ -24,23 +26,29 @@ tiendu — Tiendu theme development CLI
|
|
|
24
26
|
|
|
25
27
|
Usage:
|
|
26
28
|
tiendu init [dir] Set up a theme project (optionally in a new directory)
|
|
27
|
-
tiendu pull
|
|
29
|
+
tiendu pull [previewKey] Download the live theme, or a specific preview's files
|
|
28
30
|
tiendu build Build a theme (requires tiendu.config.json)
|
|
29
|
-
tiendu push [--skip-build]
|
|
31
|
+
tiendu push [previewKey] [--skip-build]
|
|
32
|
+
Upload files to the attached or specified preview
|
|
30
33
|
tiendu dev Start dev mode: auto-sync changes to a live preview URL
|
|
31
|
-
tiendu publish [--skip-build]
|
|
32
|
-
Publish the
|
|
33
|
-
|
|
34
|
-
tiendu preview Show the
|
|
35
|
-
tiendu preview create
|
|
36
|
-
|
|
37
|
-
tiendu preview
|
|
38
|
-
tiendu preview
|
|
34
|
+
tiendu publish [previewKey] [--skip-build]
|
|
35
|
+
Publish the attached or specified preview to the live storefront
|
|
36
|
+
|
|
37
|
+
tiendu preview Show the attached preview details
|
|
38
|
+
tiendu preview create [name]
|
|
39
|
+
Create a new preview (and attach to it)
|
|
40
|
+
tiendu preview list List all previews for your store
|
|
41
|
+
tiendu preview attach [key]
|
|
42
|
+
Attach to an existing preview by its key
|
|
43
|
+
tiendu preview detach Detach from the current preview (without deleting it)
|
|
44
|
+
tiendu preview delete [key]
|
|
45
|
+
Delete a preview (defaults to the attached one)
|
|
46
|
+
tiendu preview open Open the attached preview URL in your browser
|
|
39
47
|
|
|
40
48
|
tiendu check-updates Check npm for a newer CLI version
|
|
41
49
|
tiendu version Show the current CLI version
|
|
42
50
|
|
|
43
|
-
tiendu help
|
|
51
|
+
tiendu --help, -h Show this help message
|
|
44
52
|
tiendu --version, -v Show the current CLI version
|
|
45
53
|
|
|
46
54
|
Typical workflow:
|
|
@@ -52,10 +60,19 @@ Typical workflow:
|
|
|
52
60
|
tiendu publish Ship to the live storefront when ready
|
|
53
61
|
`;
|
|
54
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Extract the first positional argument that is not a flag (--skip-build, etc.).
|
|
65
|
+
* @param {string[]} args - CLI args after the command name
|
|
66
|
+
* @returns {string | undefined}
|
|
67
|
+
*/
|
|
68
|
+
const extractPositionalArg = (args) =>
|
|
69
|
+
args.find((arg) => !arg.startsWith("--"));
|
|
70
|
+
|
|
55
71
|
const main = async () => {
|
|
56
72
|
const args = process.argv.slice(2);
|
|
57
73
|
const command = args[0];
|
|
58
74
|
const subcommand = args[1];
|
|
75
|
+
const restArgs = args.slice(1);
|
|
59
76
|
const skipBuild = args.includes("--skip-build");
|
|
60
77
|
|
|
61
78
|
if (
|
|
@@ -69,7 +86,6 @@ const main = async () => {
|
|
|
69
86
|
|
|
70
87
|
if (
|
|
71
88
|
!command ||
|
|
72
|
-
command === "help" ||
|
|
73
89
|
command === "--help" ||
|
|
74
90
|
command === "-h"
|
|
75
91
|
) {
|
|
@@ -91,7 +107,8 @@ const main = async () => {
|
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
if (command === "pull") {
|
|
94
|
-
|
|
110
|
+
const previewKey = extractPositionalArg(restArgs);
|
|
111
|
+
await pull({ previewKey });
|
|
95
112
|
return;
|
|
96
113
|
}
|
|
97
114
|
|
|
@@ -102,7 +119,8 @@ const main = async () => {
|
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
if (command === "push") {
|
|
105
|
-
|
|
122
|
+
const previewKey = extractPositionalArg(restArgs);
|
|
123
|
+
await push({ skipBuild, previewKey });
|
|
106
124
|
return;
|
|
107
125
|
}
|
|
108
126
|
|
|
@@ -112,7 +130,8 @@ const main = async () => {
|
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
if (command === "publish") {
|
|
115
|
-
|
|
133
|
+
const previewKey = extractPositionalArg(restArgs);
|
|
134
|
+
await publish({ skipBuild, previewKey });
|
|
116
135
|
return;
|
|
117
136
|
}
|
|
118
137
|
|
|
@@ -129,8 +148,16 @@ const main = async () => {
|
|
|
129
148
|
await previewList();
|
|
130
149
|
return;
|
|
131
150
|
}
|
|
151
|
+
if (subcommand === "attach") {
|
|
152
|
+
await previewAttach(args[2]);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (subcommand === "detach") {
|
|
156
|
+
await previewDetach();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
132
159
|
if (subcommand === "delete") {
|
|
133
|
-
await previewDelete();
|
|
160
|
+
await previewDelete(args[2]);
|
|
134
161
|
return;
|
|
135
162
|
}
|
|
136
163
|
if (subcommand === "open") {
|
package/lib/api.mjs
CHANGED
|
@@ -83,6 +83,47 @@ export const fetchUserStores = async (apiBaseUrl, apiKey) => {
|
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Fetch a single preview by key.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} apiBaseUrl
|
|
90
|
+
* @param {string} apiKey
|
|
91
|
+
* @param {number} storeId
|
|
92
|
+
* @param {string} previewKey
|
|
93
|
+
* @returns {Promise<{ ok: true, data: any } | { ok: false, error: string }>}
|
|
94
|
+
*/
|
|
95
|
+
export const fetchPreview = async (apiBaseUrl, apiKey, storeId, previewKey) => {
|
|
96
|
+
try {
|
|
97
|
+
const response = await apiFetch(
|
|
98
|
+
apiBaseUrl,
|
|
99
|
+
apiKey,
|
|
100
|
+
`/api/v2/stores/${storeId}/theme-previews/${previewKey}`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const authError = checkAuthErrors(response);
|
|
104
|
+
if (authError) return authError;
|
|
105
|
+
|
|
106
|
+
if (response.status === 404) {
|
|
107
|
+
return { ok: false, error: "Preview not found." };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: `Server error: ${response.status} ${response.statusText}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const preview = await response.json();
|
|
118
|
+
return { ok: true, data: preview };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: `Could not fetch preview: ${error.message}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
86
127
|
/**
|
|
87
128
|
* Download the storefront archive (zip) as a buffer.
|
|
88
129
|
*
|
|
@@ -124,6 +165,49 @@ export const downloadStorefrontArchive = async (
|
|
|
124
165
|
}
|
|
125
166
|
};
|
|
126
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Download a preview's archive (zip) as a buffer.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} apiBaseUrl
|
|
172
|
+
* @param {string} apiKey
|
|
173
|
+
* @param {number} storeId
|
|
174
|
+
* @param {string} previewKey
|
|
175
|
+
* @returns {Promise<{ ok: true, data: Buffer } | { ok: false, error: string }>}
|
|
176
|
+
*/
|
|
177
|
+
export const downloadPreviewArchive = async (
|
|
178
|
+
apiBaseUrl,
|
|
179
|
+
apiKey,
|
|
180
|
+
storeId,
|
|
181
|
+
previewKey,
|
|
182
|
+
) => {
|
|
183
|
+
try {
|
|
184
|
+
const response = await apiFetch(
|
|
185
|
+
apiBaseUrl,
|
|
186
|
+
apiKey,
|
|
187
|
+
`/api/admin/stores/${storeId}/theme-previews/${previewKey}/download`,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const authError = checkAuthErrors(response);
|
|
191
|
+
if (authError) return authError;
|
|
192
|
+
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
const body = await response.text().catch(() => "");
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
error: `Server error: ${response.status} ${response.statusText}${body ? ` — ${body}` : ""}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
202
|
+
return { ok: true, data: Buffer.from(arrayBuffer) };
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
error: `Could not download preview: ${error.message}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
127
211
|
/**
|
|
128
212
|
* Upload a zip buffer to a preview, replacing its content.
|
|
129
213
|
*
|
package/lib/build.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import { listFilesRecursive } from "./fs-utils.mjs";
|
|
24
24
|
import { createCssPostCssPlugin } from "./postcss.mjs";
|
|
25
25
|
|
|
26
|
-
const THEME_SOURCE_OUTPUT_DIRS = ["layout", "templates", "snippets"];
|
|
26
|
+
const THEME_SOURCE_OUTPUT_DIRS = ["layout", "templates", "sections", "snippets", "config"];
|
|
27
27
|
const LIQUID_LIKE_EXTENSIONS = new Set([".liquid", ".html", ".htm"]);
|
|
28
28
|
const ENTRY_SOURCE_EXTENSIONS = new Set([".js", ".ts", ".css"]);
|
|
29
29
|
const NESTED_ASSET_PATH_PATTERN = /\/assets\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._/-]+)+)([?#][A-Za-z0-9=&._-]+)?/g;
|
|
@@ -155,7 +155,7 @@ const shouldTriggerTailwindCssRebuild = (relativePath) => {
|
|
|
155
155
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
156
156
|
const extension = path.extname(normalizedPath).toLowerCase();
|
|
157
157
|
|
|
158
|
-
if (!["layout/", "templates/", "snippets/"].some((prefix) => normalizedPath.startsWith(prefix))) {
|
|
158
|
+
if (!["layout/", "templates/", "sections/", "snippets/"].some((prefix) => normalizedPath.startsWith(prefix))) {
|
|
159
159
|
return false;
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -275,7 +275,7 @@ export const build = async ({ watch: watchMode = false } = {}) => {
|
|
|
275
275
|
|
|
276
276
|
const outdir = path.join(distDir, "assets");
|
|
277
277
|
const jsBuildOptions = jsCount > 0
|
|
278
|
-
? { entryPoints: jsEntries, bundle: true, format: "esm", target: "es2020", outdir, logLevel: "warning", write: true, plugins: jsPlugins }
|
|
278
|
+
? { entryPoints: jsEntries, bundle: true, format: "esm", target: "es2020", outdir, logLevel: "warning", write: true, resolveExtensions: [".ts", ".tsx", ".js", ".jsx", ".json"], plugins: jsPlugins }
|
|
279
279
|
: null;
|
|
280
280
|
const cssBuildOptions = cssCount > 0
|
|
281
281
|
? { entryPoints: cssEntries, bundle: true, outdir, logLevel: "warning", write: true, plugins: cssPlugins }
|
package/lib/dev.mjs
CHANGED
|
@@ -2,12 +2,10 @@ import { watch } from "node:fs";
|
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
|
-
import { loadConfigOrFail,
|
|
5
|
+
import { loadConfigOrFail, isBuiltTheme, getDistDir } from "./config.mjs";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
listPreviews,
|
|
10
|
-
resolveActivePreview,
|
|
7
|
+
fetchPreviewDetails,
|
|
8
|
+
resolvePreviewKeyInteractively,
|
|
11
9
|
} from "./preview.mjs";
|
|
12
10
|
import {
|
|
13
11
|
deletePreviewFile,
|
|
@@ -21,6 +19,7 @@ import { retryAsync } from "./retry.mjs";
|
|
|
21
19
|
|
|
22
20
|
const RETRY_ATTEMPTS = 3;
|
|
23
21
|
const MAX_SYNC_FILE_SIZE_BYTES = 20 * 1024 * 1024;
|
|
22
|
+
const CLEANUP_TIMEOUT_MS = 5_000;
|
|
24
23
|
const IGNORED_ROOT_SEGMENTS = new Set(["node_modules", ".git"]);
|
|
25
24
|
|
|
26
25
|
const hasDotfileSegment = (relativePath) =>
|
|
@@ -45,6 +44,27 @@ const shouldIgnoreWatchedPath = (relativePath, builtTheme) => {
|
|
|
45
44
|
const shouldRetrySyncResult = (result) =>
|
|
46
45
|
!result.ok && Boolean(result.retriable);
|
|
47
46
|
|
|
47
|
+
const runCleanupStep = async (label, cleanupFn) => {
|
|
48
|
+
if (!cleanupFn) return;
|
|
49
|
+
|
|
50
|
+
let timeoutId = null;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await Promise.race([
|
|
54
|
+
Promise.resolve().then(() => cleanupFn()),
|
|
55
|
+
new Promise((_, reject) => {
|
|
56
|
+
timeoutId = setTimeout(() => {
|
|
57
|
+
reject(new Error(`${label} did not finish within ${CLEANUP_TIMEOUT_MS}ms.`));
|
|
58
|
+
}, CLEANUP_TIMEOUT_MS);
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
p.log.warn(error.message);
|
|
63
|
+
} finally {
|
|
64
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
48
68
|
const uploadFileWithRetries = (
|
|
49
69
|
apiBaseUrl,
|
|
50
70
|
apiKey,
|
|
@@ -95,19 +115,6 @@ const deleteFileWithRetries = (
|
|
|
95
115
|
},
|
|
96
116
|
);
|
|
97
117
|
|
|
98
|
-
const resolvePreviewForDev = (previews, configuredPreviewKey) => {
|
|
99
|
-
const activePreview = resolveActivePreview(previews, configuredPreviewKey);
|
|
100
|
-
if (activePreview) {
|
|
101
|
-
return { preview: activePreview, fallbackUsed: false };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (configuredPreviewKey && previews.length === 1) {
|
|
105
|
-
return { preview: previews[0], fallbackUsed: true };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return { preview: null, fallbackUsed: false };
|
|
109
|
-
};
|
|
110
|
-
|
|
111
118
|
export const dev = async () => {
|
|
112
119
|
const { config, credentials } = await loadConfigOrFail();
|
|
113
120
|
const { apiBaseUrl, storeId } = config;
|
|
@@ -127,48 +134,26 @@ export const dev = async () => {
|
|
|
127
134
|
buildCleanup = buildResult.cleanup;
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
// Resolve preview via shared interactive picker
|
|
138
|
+
const previewKey = await resolvePreviewKeyInteractively({ config, credentials });
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
// Fetch preview to get hostname for local proxy
|
|
141
|
+
const previewResult = await fetchPreviewDetails(
|
|
142
|
+
apiBaseUrl,
|
|
143
|
+
apiKey,
|
|
144
|
+
storeId,
|
|
145
|
+
previewKey,
|
|
146
|
+
);
|
|
147
|
+
if (!previewResult.ok) {
|
|
148
|
+
p.log.error(`Preview ${previewKey} not found.`);
|
|
137
149
|
process.exit(1);
|
|
138
150
|
}
|
|
139
151
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
if (previewResolution.fallbackUsed && activePreview) {
|
|
143
|
-
p.log.warn(
|
|
144
|
-
`Stored preview ${config.previewKey} was not found. Using the only available preview ${activePreview.previewKey}.`,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!activePreview) {
|
|
149
|
-
if (config.previewKey) {
|
|
150
|
-
p.log.warn(
|
|
151
|
-
`Stored preview ${config.previewKey} was not found. Creating a new preview...`,
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
spinner.message("Creating preview...");
|
|
156
|
-
const previewResult = await createPreview(apiBaseUrl, apiKey, storeId, "Dev");
|
|
157
|
-
if (!previewResult.ok) {
|
|
158
|
-
spinner.stop("Failed to create preview.", 1);
|
|
159
|
-
p.log.error(previewResult.error);
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
activePreview = previewResult.data;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const previewKey = activePreview.previewKey;
|
|
167
|
-
if (config.previewKey !== previewKey) {
|
|
168
|
-
await writeConfig({ ...config, previewKey });
|
|
169
|
-
}
|
|
152
|
+
const previewHostname = previewResult.data.preview.previewHostname;
|
|
153
|
+
const previewUrl = previewResult.data.url;
|
|
170
154
|
|
|
171
|
-
const
|
|
155
|
+
const spinner = p.spinner();
|
|
156
|
+
spinner.start("Compressing files...");
|
|
172
157
|
|
|
173
158
|
const uploadResult = await pushPreparedDirectoryToPreview({
|
|
174
159
|
apiBaseUrl,
|
|
@@ -177,7 +162,7 @@ export const dev = async () => {
|
|
|
177
162
|
previewKey,
|
|
178
163
|
rootDir,
|
|
179
164
|
spinner,
|
|
180
|
-
|
|
165
|
+
compressMessage: "Compressing files...",
|
|
181
166
|
retryMessage: (result, nextAttempt) =>
|
|
182
167
|
`Initial push failed. Retrying ${nextAttempt}/${RETRY_ATTEMPTS}... ${result.error}`,
|
|
183
168
|
});
|
|
@@ -191,19 +176,19 @@ export const dev = async () => {
|
|
|
191
176
|
try {
|
|
192
177
|
localPreviewServer = await startLocalPreviewServer({
|
|
193
178
|
apiBaseUrl,
|
|
194
|
-
previewHostname
|
|
179
|
+
previewHostname,
|
|
195
180
|
});
|
|
196
181
|
} catch (error) {
|
|
197
182
|
p.log.warn(`Could not start local live preview: ${error.message}`);
|
|
198
183
|
}
|
|
199
184
|
|
|
200
|
-
spinner.stop(
|
|
185
|
+
spinner.stop(`Preview ready (${previewKey}).`);
|
|
201
186
|
if (localPreviewServer) {
|
|
202
187
|
p.log.message(`Local live preview: ${localPreviewServer.url}`);
|
|
203
188
|
}
|
|
204
189
|
p.log.message(`Sharable preview: ${previewUrl}`);
|
|
205
190
|
|
|
206
|
-
p.log.message("Watching for changes
|
|
191
|
+
p.log.message("Watching for changes \u2014 press Ctrl+C to stop.");
|
|
207
192
|
|
|
208
193
|
// ── File watcher ──────────────────────────────────────────────────────────
|
|
209
194
|
/** @type {Map<string, NodeJS.Timeout>} */
|
|
@@ -238,7 +223,7 @@ export const dev = async () => {
|
|
|
238
223
|
|
|
239
224
|
if (!fileStat || !fileStat.isFile()) {
|
|
240
225
|
if (!fileStat) {
|
|
241
|
-
console.log(
|
|
226
|
+
console.log(`\u2715 ${relativePath}`);
|
|
242
227
|
const result = await deleteFileWithRetries(
|
|
243
228
|
apiBaseUrl,
|
|
244
229
|
apiKey,
|
|
@@ -262,7 +247,7 @@ export const dev = async () => {
|
|
|
262
247
|
return;
|
|
263
248
|
}
|
|
264
249
|
|
|
265
|
-
console.log(
|
|
250
|
+
console.log(`\u2191 ${relativePath}`);
|
|
266
251
|
if (fileStat.size > MAX_SYNC_FILE_SIZE_BYTES) {
|
|
267
252
|
p.log.warn(
|
|
268
253
|
` Skipping ${relativePath}: file is ${(fileStat.size / (1024 * 1024)).toFixed(1)} MB (limit ${(MAX_SYNC_FILE_SIZE_BYTES / (1024 * 1024)).toFixed(0)} MB).`,
|
|
@@ -317,8 +302,10 @@ export const dev = async () => {
|
|
|
317
302
|
|
|
318
303
|
watcher.close();
|
|
319
304
|
for (const timer of debounceMap.values()) clearTimeout(timer);
|
|
320
|
-
|
|
321
|
-
|
|
305
|
+
|
|
306
|
+
await runCleanupStep("Local preview shutdown", () => localPreviewServer?.close());
|
|
307
|
+
await runCleanupStep("Build watcher shutdown", buildCleanup);
|
|
308
|
+
|
|
322
309
|
p.outro("Dev mode stopped.");
|
|
323
310
|
process.exit(0);
|
|
324
311
|
};
|
package/lib/local-preview.mjs
CHANGED
|
@@ -210,7 +210,11 @@ export const startLocalPreviewServer = async ({
|
|
|
210
210
|
const upstreamOrigin = new URL(apiBaseUrl);
|
|
211
211
|
const previewOrigin = new URL(`${upstreamOrigin.protocol}//${previewHostname}`);
|
|
212
212
|
const sseClients = new Set();
|
|
213
|
+
const sockets = new Set();
|
|
214
|
+
const upstreamRequests = new Set();
|
|
213
215
|
let reloadTimer = null;
|
|
216
|
+
let closed = false;
|
|
217
|
+
let closePromise = null;
|
|
214
218
|
|
|
215
219
|
const server = createServer(async (request, response) => {
|
|
216
220
|
if (!request.url) {
|
|
@@ -256,6 +260,8 @@ export const startLocalPreviewServer = async ({
|
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
const targetUrl = new URL(requestUrl.pathname + requestUrl.search, upstreamOrigin);
|
|
263
|
+
const upstreamRequest = new AbortController();
|
|
264
|
+
upstreamRequests.add(upstreamRequest);
|
|
259
265
|
|
|
260
266
|
try {
|
|
261
267
|
const body = await readRequestBody(request);
|
|
@@ -264,10 +270,14 @@ export const startLocalPreviewServer = async ({
|
|
|
264
270
|
headers: createForwardHeaders(request, previewHostname),
|
|
265
271
|
body,
|
|
266
272
|
redirect: "manual",
|
|
267
|
-
signal: AbortSignal.
|
|
273
|
+
signal: AbortSignal.any([
|
|
274
|
+
AbortSignal.timeout(PROXY_TIMEOUT_MS),
|
|
275
|
+
upstreamRequest.signal,
|
|
276
|
+
]),
|
|
268
277
|
});
|
|
269
278
|
|
|
270
279
|
if (isHtmlResponse(upstreamResponse.headers)) {
|
|
280
|
+
if (closed || response.destroyed) return;
|
|
271
281
|
const html = injectLiveReloadScript(await upstreamResponse.text());
|
|
272
282
|
writeResponseHeaders(upstreamResponse, response, {
|
|
273
283
|
localOrigin,
|
|
@@ -286,6 +296,7 @@ export const startLocalPreviewServer = async ({
|
|
|
286
296
|
previewOrigin,
|
|
287
297
|
upstreamOrigin,
|
|
288
298
|
});
|
|
299
|
+
if (closed || response.destroyed) return;
|
|
289
300
|
response.statusCode = upstreamResponse.status;
|
|
290
301
|
|
|
291
302
|
if (!upstreamResponse.body) {
|
|
@@ -300,12 +311,29 @@ export const startLocalPreviewServer = async ({
|
|
|
300
311
|
});
|
|
301
312
|
proxyStream.pipe(response);
|
|
302
313
|
} catch (error) {
|
|
303
|
-
|
|
314
|
+
if (response.destroyed || response.writableEnded) return;
|
|
315
|
+
|
|
316
|
+
const wasAbort = error?.name === "AbortError" || error?.name === "TimeoutError";
|
|
317
|
+
if (closed && wasAbort) {
|
|
318
|
+
response.destroy();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const statusCode = error.statusCode ?? (error?.name === "TimeoutError" ? 504 : 502);
|
|
304
323
|
response.writeHead(statusCode, { "content-type": "text/plain; charset=utf-8" });
|
|
305
324
|
response.end(`Local preview proxy error: ${error.message}`);
|
|
325
|
+
} finally {
|
|
326
|
+
upstreamRequests.delete(upstreamRequest);
|
|
306
327
|
}
|
|
307
328
|
});
|
|
308
329
|
|
|
330
|
+
server.on("connection", (socket) => {
|
|
331
|
+
sockets.add(socket);
|
|
332
|
+
socket.on("close", () => {
|
|
333
|
+
sockets.delete(socket);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
309
337
|
const heartbeat = setInterval(() => {
|
|
310
338
|
for (const client of sseClients) {
|
|
311
339
|
client.write(": ping\n\n");
|
|
@@ -327,24 +355,39 @@ export const startLocalPreviewServer = async ({
|
|
|
327
355
|
}, RELOAD_DEBOUNCE_MS);
|
|
328
356
|
},
|
|
329
357
|
async close() {
|
|
330
|
-
if (
|
|
331
|
-
|
|
358
|
+
if (closePromise) return closePromise;
|
|
359
|
+
closed = true;
|
|
332
360
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
361
|
+
closePromise = new Promise((resolve, reject) => {
|
|
362
|
+
if (reloadTimer) clearTimeout(reloadTimer);
|
|
363
|
+
clearInterval(heartbeat);
|
|
364
|
+
|
|
365
|
+
for (const client of sseClients) {
|
|
366
|
+
client.end();
|
|
367
|
+
}
|
|
368
|
+
sseClients.clear();
|
|
369
|
+
|
|
370
|
+
for (const upstreamRequest of upstreamRequests) {
|
|
371
|
+
upstreamRequest.abort();
|
|
372
|
+
}
|
|
337
373
|
|
|
338
|
-
await new Promise((resolve, reject) => {
|
|
339
374
|
server.close((error) => {
|
|
340
|
-
if (error) {
|
|
375
|
+
if (error && error.code !== "ERR_SERVER_NOT_RUNNING") {
|
|
341
376
|
reject(error);
|
|
342
377
|
return;
|
|
343
378
|
}
|
|
344
379
|
|
|
345
380
|
resolve();
|
|
346
381
|
});
|
|
382
|
+
|
|
383
|
+
server.closeIdleConnections?.();
|
|
384
|
+
server.closeAllConnections?.();
|
|
385
|
+
for (const socket of sockets) {
|
|
386
|
+
socket.destroy();
|
|
387
|
+
}
|
|
347
388
|
});
|
|
389
|
+
|
|
390
|
+
return closePromise;
|
|
348
391
|
},
|
|
349
392
|
};
|
|
350
393
|
};
|
package/lib/preview.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { loadConfigOrFail, writeConfig } from "./config.mjs";
|
|
3
|
-
import { apiFetch } from "./api.mjs";
|
|
3
|
+
import { apiFetch, fetchPreview } from "./api.mjs";
|
|
4
4
|
|
|
5
5
|
export const buildPreviewUrl = (apiBaseUrl, previewHostname) => {
|
|
6
6
|
const base = new URL(apiBaseUrl);
|
|
@@ -8,6 +8,47 @@ export const buildPreviewUrl = (apiBaseUrl, previewHostname) => {
|
|
|
8
8
|
return `${base.protocol}//${previewHostname}${!hasExplicitPort && base.port ? `:${base.port}` : ""}/`;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
const formatShortDateTime = (value) => {
|
|
12
|
+
if (!value) return "Unknown";
|
|
13
|
+
const date = new Date(value);
|
|
14
|
+
if (Number.isNaN(date.getTime())) return "Unknown";
|
|
15
|
+
|
|
16
|
+
const months = [
|
|
17
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
18
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
19
|
+
];
|
|
20
|
+
const month = months[date.getMonth()];
|
|
21
|
+
const day = date.getDate();
|
|
22
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
23
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
24
|
+
return `${month} ${day} ${hours}:${minutes}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getPreviewDisplayName = (preview) =>
|
|
28
|
+
preview.name || `${formatShortDateTime(preview.createdAt)} preview (no name)`;
|
|
29
|
+
|
|
30
|
+
export const getPreviewUrl = (apiBaseUrl, preview) =>
|
|
31
|
+
buildPreviewUrl(apiBaseUrl, preview.previewHostname);
|
|
32
|
+
|
|
33
|
+
export const fetchPreviewDetails = async (
|
|
34
|
+
apiBaseUrl,
|
|
35
|
+
apiKey,
|
|
36
|
+
storeId,
|
|
37
|
+
previewKey,
|
|
38
|
+
) => {
|
|
39
|
+
const result = await fetchPreview(apiBaseUrl, apiKey, storeId, previewKey);
|
|
40
|
+
if (!result.ok) return result;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
data: {
|
|
45
|
+
preview: result.data,
|
|
46
|
+
displayName: getPreviewDisplayName(result.data),
|
|
47
|
+
url: getPreviewUrl(apiBaseUrl, result.data),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
11
52
|
/**
|
|
12
53
|
* @param {Array<any>} previews
|
|
13
54
|
* @param {string | undefined} previewKey
|
|
@@ -42,18 +83,10 @@ export const createPreview = async (apiBaseUrl, apiKey, storeId, name) => {
|
|
|
42
83
|
`/api/v2/stores/${storeId}/theme-previews`,
|
|
43
84
|
{
|
|
44
85
|
method: "POST",
|
|
45
|
-
body: JSON.stringify({ name: name ?? "
|
|
86
|
+
body: JSON.stringify({ name: name ?? "" }),
|
|
46
87
|
},
|
|
47
88
|
);
|
|
48
89
|
|
|
49
|
-
if (response.status === 409) {
|
|
50
|
-
const body = await response.json().catch(() => ({}));
|
|
51
|
-
const message =
|
|
52
|
-
body?.error?.message ??
|
|
53
|
-
"A preview already exists for this store. Delete it first with: tiendu preview delete";
|
|
54
|
-
return { ok: false, error: message };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
90
|
if (!response.ok) {
|
|
58
91
|
const body = await response.text().catch(() => "");
|
|
59
92
|
return {
|
|
@@ -158,6 +191,116 @@ export const publishPreview = async (
|
|
|
158
191
|
}
|
|
159
192
|
};
|
|
160
193
|
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Shared interactive preview picker
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
const CREATE_NEW_VALUE = "__create_new__";
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Interactively resolve a preview key. Uses attached key if valid, otherwise
|
|
202
|
+
* prompts the user to pick an existing preview or create a new one.
|
|
203
|
+
*
|
|
204
|
+
* @param {{ config: import("./config.mjs").TienduConfig, credentials: import("./config.mjs").TienduCredentials }} opts
|
|
205
|
+
* @returns {Promise<string>} The resolved preview key
|
|
206
|
+
*/
|
|
207
|
+
export const resolvePreviewKeyInteractively = async ({ config, credentials }) => {
|
|
208
|
+
const { apiBaseUrl, storeId } = config;
|
|
209
|
+
const { apiKey } = credentials;
|
|
210
|
+
|
|
211
|
+
// 1. Validate stored key
|
|
212
|
+
if (config.previewKey) {
|
|
213
|
+
const result = await fetchPreview(apiBaseUrl, apiKey, storeId, config.previewKey);
|
|
214
|
+
if (result.ok) {
|
|
215
|
+
return config.previewKey;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
p.log.warn(`Stored preview ${config.previewKey} was not found. Please select a preview.`);
|
|
219
|
+
const { previewKey: _, ...rest } = config;
|
|
220
|
+
await writeConfig(rest);
|
|
221
|
+
} else {
|
|
222
|
+
p.log.warn("No preview attached.");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 2. List previews
|
|
226
|
+
const listResult = await listPreviews(apiBaseUrl, apiKey, storeId);
|
|
227
|
+
if (!listResult.ok) {
|
|
228
|
+
p.log.error(listResult.error);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const previews = listResult.data;
|
|
233
|
+
|
|
234
|
+
if (previews.length === 0) {
|
|
235
|
+
p.log.info("No previews found for this store.");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 3. Show picker
|
|
239
|
+
const options = [
|
|
240
|
+
...previews.map((preview) => ({
|
|
241
|
+
value: preview.previewKey,
|
|
242
|
+
label: getPreviewDisplayName(preview),
|
|
243
|
+
hint: preview.previewKey,
|
|
244
|
+
})),
|
|
245
|
+
{
|
|
246
|
+
value: CREATE_NEW_VALUE,
|
|
247
|
+
label: "Create a new preview",
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
const selected = await p.select({
|
|
252
|
+
message: "Select a preview",
|
|
253
|
+
options,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (p.isCancel(selected)) {
|
|
257
|
+
p.cancel("Cancelled.");
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 4. Handle create new
|
|
262
|
+
if (selected === CREATE_NEW_VALUE) {
|
|
263
|
+
const nameInput = await p.text({
|
|
264
|
+
message: "Preview name (optional)",
|
|
265
|
+
placeholder: "Press Enter to skip",
|
|
266
|
+
defaultValue: "",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (p.isCancel(nameInput)) {
|
|
270
|
+
p.cancel("Cancelled.");
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const name = (nameInput ?? "").trim();
|
|
275
|
+
const spinner = p.spinner();
|
|
276
|
+
spinner.start("Creating preview...");
|
|
277
|
+
|
|
278
|
+
const createResult = await createPreview(apiBaseUrl, apiKey, storeId, name);
|
|
279
|
+
if (!createResult.ok) {
|
|
280
|
+
spinner.stop("Failed to create preview.", 1);
|
|
281
|
+
p.log.error(createResult.error);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const preview = createResult.data;
|
|
286
|
+
const displayName = getPreviewDisplayName(preview);
|
|
287
|
+
const url = getPreviewUrl(apiBaseUrl, preview);
|
|
288
|
+
spinner.stop(`Preview "${displayName}" created (${preview.previewKey})`);
|
|
289
|
+
p.log.message(` ${url}`);
|
|
290
|
+
|
|
291
|
+
await writeConfig({ ...config, previewKey: preview.previewKey });
|
|
292
|
+
return preview.previewKey;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 5. Attach to selected preview
|
|
296
|
+
await writeConfig({ ...config, previewKey: selected });
|
|
297
|
+
const selectedPreview = previews.find((p) => p.previewKey === selected);
|
|
298
|
+
const displayName = selectedPreview ? getPreviewDisplayName(selectedPreview) : selected;
|
|
299
|
+
p.log.success(`Attached to "${displayName}" (${selected})`);
|
|
300
|
+
|
|
301
|
+
return selected;
|
|
302
|
+
};
|
|
303
|
+
|
|
161
304
|
// ---------------------------------------------------------------------------
|
|
162
305
|
// CLI commands
|
|
163
306
|
// ---------------------------------------------------------------------------
|
|
@@ -182,8 +325,10 @@ export const previewCreate = async (name) => {
|
|
|
182
325
|
}
|
|
183
326
|
|
|
184
327
|
const preview = result.data;
|
|
185
|
-
const url =
|
|
186
|
-
|
|
328
|
+
const url = getPreviewUrl(config.apiBaseUrl, preview);
|
|
329
|
+
const displayName = getPreviewDisplayName(preview);
|
|
330
|
+
spinner.stop(`Preview "${displayName}" created (${preview.previewKey})`);
|
|
331
|
+
p.log.message(` ${url}`);
|
|
187
332
|
|
|
188
333
|
await writeConfig({ ...config, previewKey: preview.previewKey });
|
|
189
334
|
};
|
|
@@ -215,14 +360,15 @@ export const previewList = async () => {
|
|
|
215
360
|
`${result.data.length} preview${result.data.length === 1 ? "" : "s"}:`,
|
|
216
361
|
);
|
|
217
362
|
|
|
218
|
-
const activePreview = resolveActivePreview(result.data, config.previewKey);
|
|
219
|
-
|
|
220
363
|
for (const preview of result.data) {
|
|
221
|
-
const
|
|
222
|
-
|
|
364
|
+
const isAttached = config.previewKey === preview.previewKey;
|
|
365
|
+
const indicator = isAttached ? " \u2190 attached" : "";
|
|
223
366
|
const url = buildPreviewUrl(config.apiBaseUrl, preview.previewHostname);
|
|
224
|
-
|
|
367
|
+
const displayName = getPreviewDisplayName(preview);
|
|
368
|
+
p.log.message(` ${displayName} ${url}${indicator}`);
|
|
225
369
|
}
|
|
370
|
+
|
|
371
|
+
p.log.info("Tip: run tiendu preview attach <key> to switch previews.");
|
|
226
372
|
};
|
|
227
373
|
|
|
228
374
|
const formatRelativeDate = (value) => {
|
|
@@ -245,67 +391,119 @@ const formatRelativeDate = (value) => {
|
|
|
245
391
|
export const previewShow = async () => {
|
|
246
392
|
const { config, credentials } = await loadConfigOrFail();
|
|
247
393
|
|
|
248
|
-
|
|
394
|
+
if (!config.previewKey) {
|
|
395
|
+
p.log.warn("No preview attached. Run tiendu preview list or tiendu preview create.");
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const result = await fetchPreview(
|
|
249
400
|
config.apiBaseUrl,
|
|
250
401
|
credentials.apiKey,
|
|
251
402
|
config.storeId,
|
|
403
|
+
config.previewKey,
|
|
252
404
|
);
|
|
253
405
|
|
|
254
406
|
if (!result.ok) {
|
|
255
|
-
p.log.
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const preview = resolveActivePreview(result.data, config.previewKey);
|
|
260
|
-
if (!preview) {
|
|
261
|
-
p.log.error(
|
|
262
|
-
result.data.length === 0
|
|
263
|
-
? "No previews found for this store."
|
|
264
|
-
: "Run tiendu preview list to inspect available previews.",
|
|
265
|
-
);
|
|
407
|
+
p.log.warn(`Stored preview ${config.previewKey} was not found.`);
|
|
408
|
+
p.log.info("Run tiendu preview list to see available previews.");
|
|
266
409
|
process.exit(1);
|
|
267
410
|
}
|
|
268
411
|
|
|
269
|
-
const
|
|
412
|
+
const preview = result.data;
|
|
413
|
+
const url = getPreviewUrl(config.apiBaseUrl, preview);
|
|
414
|
+
const displayName = getPreviewDisplayName(preview);
|
|
270
415
|
|
|
271
416
|
p.note(
|
|
272
417
|
[
|
|
273
|
-
`Name: ${
|
|
418
|
+
`Name: ${displayName}`,
|
|
419
|
+
`Key: ${preview.previewKey}`,
|
|
274
420
|
`URL: ${url}`,
|
|
275
421
|
`Created: ${formatRelativeDate(preview.createdAt)}`,
|
|
276
422
|
].join("\n"),
|
|
277
|
-
"
|
|
423
|
+
"Attached preview",
|
|
278
424
|
);
|
|
279
425
|
};
|
|
280
426
|
|
|
281
|
-
export const
|
|
427
|
+
export const previewAttach = async (keyArg) => {
|
|
282
428
|
const { config, credentials } = await loadConfigOrFail();
|
|
283
429
|
|
|
284
|
-
|
|
430
|
+
if (!keyArg) {
|
|
431
|
+
await resolvePreviewKeyInteractively({ config, credentials });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const spinner = p.spinner();
|
|
436
|
+
spinner.start("Validating preview...");
|
|
437
|
+
|
|
438
|
+
const result = await fetchPreview(
|
|
285
439
|
config.apiBaseUrl,
|
|
286
440
|
credentials.apiKey,
|
|
287
441
|
config.storeId,
|
|
442
|
+
keyArg,
|
|
288
443
|
);
|
|
289
|
-
|
|
290
|
-
|
|
444
|
+
|
|
445
|
+
if (!result.ok) {
|
|
446
|
+
spinner.stop("Preview not found.", 1);
|
|
447
|
+
p.log.error("Preview not found. Run tiendu preview list to see available previews.");
|
|
291
448
|
process.exit(1);
|
|
292
449
|
}
|
|
293
450
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
451
|
+
const preview = result.data;
|
|
452
|
+
const url = getPreviewUrl(config.apiBaseUrl, preview);
|
|
453
|
+
const displayName = getPreviewDisplayName(preview);
|
|
454
|
+
spinner.stop(`Attached to preview "${displayName}" (${preview.previewKey})`);
|
|
455
|
+
p.log.message(` ${url}`);
|
|
456
|
+
|
|
457
|
+
await writeConfig({ ...config, previewKey: preview.previewKey });
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
export const previewDetach = async () => {
|
|
461
|
+
const { config } = await loadConfigOrFail();
|
|
462
|
+
|
|
463
|
+
if (!config.previewKey) {
|
|
464
|
+
p.log.warn("No preview is currently attached.");
|
|
465
|
+
process.exit(0);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const detachedKey = config.previewKey;
|
|
469
|
+
const { previewKey: _, ...rest } = config;
|
|
470
|
+
await writeConfig(rest);
|
|
471
|
+
|
|
472
|
+
p.log.success(`Detached from preview ${detachedKey}. No active preview.`);
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
export const previewDelete = async (keyArg) => {
|
|
476
|
+
const { config, credentials } = await loadConfigOrFail();
|
|
477
|
+
|
|
478
|
+
let previewKey = keyArg;
|
|
479
|
+
|
|
480
|
+
if (!previewKey) {
|
|
481
|
+
if (!config.previewKey) {
|
|
482
|
+
p.log.warn("No preview attached and no key provided.");
|
|
483
|
+
p.log.info("Run tiendu preview delete <key> or tiendu preview attach first.");
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
previewKey = config.previewKey;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Fetch preview to show its name in the confirmation
|
|
490
|
+
const fetchResult = await fetchPreview(
|
|
491
|
+
config.apiBaseUrl,
|
|
492
|
+
credentials.apiKey,
|
|
493
|
+
config.storeId,
|
|
494
|
+
previewKey,
|
|
297
495
|
);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
? "No previews found for this store."
|
|
302
|
-
: "Could not determine the active preview. Run tiendu preview list first.",
|
|
303
|
-
);
|
|
496
|
+
|
|
497
|
+
if (!fetchResult.ok) {
|
|
498
|
+
p.log.error(`Preview ${previewKey} not found.`);
|
|
304
499
|
process.exit(1);
|
|
305
500
|
}
|
|
306
501
|
|
|
502
|
+
const displayName = getPreviewDisplayName(fetchResult.data);
|
|
503
|
+
const url = getPreviewUrl(config.apiBaseUrl, fetchResult.data);
|
|
504
|
+
|
|
307
505
|
const confirmed = await p.confirm({
|
|
308
|
-
message: `Delete preview ${
|
|
506
|
+
message: `Delete preview ${previewKey} "${displayName}" (${url})?`,
|
|
309
507
|
});
|
|
310
508
|
|
|
311
509
|
if (p.isCancel(confirmed) || !confirmed) {
|
|
@@ -320,7 +518,7 @@ export const previewDelete = async () => {
|
|
|
320
518
|
config.apiBaseUrl,
|
|
321
519
|
credentials.apiKey,
|
|
322
520
|
config.storeId,
|
|
323
|
-
|
|
521
|
+
previewKey,
|
|
324
522
|
);
|
|
325
523
|
|
|
326
524
|
if (!result.ok) {
|
|
@@ -331,40 +529,38 @@ export const previewDelete = async () => {
|
|
|
331
529
|
|
|
332
530
|
spinner.stop("Preview deleted.");
|
|
333
531
|
|
|
334
|
-
|
|
335
|
-
|
|
532
|
+
if (config.previewKey === previewKey) {
|
|
533
|
+
const { previewKey: _, ...rest } = config;
|
|
534
|
+
await writeConfig(rest);
|
|
535
|
+
}
|
|
336
536
|
};
|
|
337
537
|
|
|
338
538
|
export const previewOpen = async () => {
|
|
339
539
|
const { config, credentials } = await loadConfigOrFail();
|
|
340
540
|
|
|
541
|
+
if (!config.previewKey) {
|
|
542
|
+
p.log.warn("No preview attached. Run tiendu preview attach or tiendu preview create.");
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
|
|
341
546
|
const spinner = p.spinner();
|
|
342
547
|
spinner.start("Fetching preview URL...");
|
|
343
548
|
|
|
344
|
-
const result = await
|
|
549
|
+
const result = await fetchPreview(
|
|
345
550
|
config.apiBaseUrl,
|
|
346
551
|
credentials.apiKey,
|
|
347
552
|
config.storeId,
|
|
553
|
+
config.previewKey,
|
|
348
554
|
);
|
|
349
555
|
|
|
350
556
|
if (!result.ok) {
|
|
351
|
-
spinner.stop("
|
|
352
|
-
p.log.error(
|
|
557
|
+
spinner.stop("Preview not found.", 1);
|
|
558
|
+
p.log.error("Stored preview was not found. Run tiendu preview list.");
|
|
353
559
|
process.exit(1);
|
|
354
560
|
}
|
|
355
561
|
|
|
356
|
-
const preview =
|
|
357
|
-
|
|
358
|
-
spinner.stop("Could not determine the active preview.", 1);
|
|
359
|
-
p.log.error(
|
|
360
|
-
result.data.length === 0
|
|
361
|
-
? "No previews found for this store."
|
|
362
|
-
: "Run tiendu preview list and then set or recreate the preview.",
|
|
363
|
-
);
|
|
364
|
-
process.exit(1);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const url = buildPreviewUrl(config.apiBaseUrl, preview.previewHostname);
|
|
562
|
+
const preview = result.data;
|
|
563
|
+
const url = getPreviewUrl(config.apiBaseUrl, preview);
|
|
368
564
|
spinner.stop(`Opening ${url}`);
|
|
369
565
|
|
|
370
566
|
const { spawn } = await import("node:child_process");
|
package/lib/publish.mjs
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
|
-
import { loadConfigOrFail,
|
|
3
|
-
import {
|
|
2
|
+
import { loadConfigOrFail, isBuiltTheme } from "./config.mjs";
|
|
3
|
+
import {
|
|
4
|
+
fetchPreviewDetails,
|
|
5
|
+
publishPreview,
|
|
6
|
+
resolvePreviewKeyInteractively,
|
|
7
|
+
} from "./preview.mjs";
|
|
4
8
|
import { push } from "./push.mjs";
|
|
5
9
|
|
|
6
|
-
export const publish = async ({ skipBuild = false } = {}) => {
|
|
10
|
+
export const publish = async ({ skipBuild = false, previewKey: previewKeyArg } = {}) => {
|
|
7
11
|
const { config, credentials } = await loadConfigOrFail();
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
// Resolve preview key: explicit arg > interactive picker
|
|
14
|
+
const previewKey = previewKeyArg ?? await resolvePreviewKeyInteractively({ config, credentials });
|
|
15
|
+
|
|
16
|
+
// Fetch preview to show its name in the confirmation
|
|
17
|
+
const fetchResult = await fetchPreviewDetails(
|
|
18
|
+
config.apiBaseUrl,
|
|
19
|
+
credentials.apiKey,
|
|
20
|
+
config.storeId,
|
|
21
|
+
previewKey,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const displayName = fetchResult.ok
|
|
25
|
+
? fetchResult.data.displayName
|
|
26
|
+
: previewKey;
|
|
27
|
+
const previewUrl = fetchResult.ok ? fetchResult.data.url : null;
|
|
13
28
|
|
|
14
29
|
const confirmed = await p.confirm({
|
|
15
|
-
message:
|
|
30
|
+
message: previewUrl
|
|
31
|
+
? `Publish preview "${displayName}" (${previewKey}) at ${previewUrl} to the live storefront?`
|
|
32
|
+
: `Publish preview "${displayName}" (${previewKey}) to the live storefront?`,
|
|
16
33
|
});
|
|
17
34
|
|
|
18
35
|
if (p.isCancel(confirmed) || !confirmed) {
|
|
@@ -26,17 +43,17 @@ export const publish = async ({ skipBuild = false } = {}) => {
|
|
|
26
43
|
? "Syncing existing dist/ output to the preview before publishing..."
|
|
27
44
|
: "Building and syncing the latest dist/ output before publishing...",
|
|
28
45
|
);
|
|
29
|
-
await push({ skipBuild });
|
|
46
|
+
await push({ skipBuild, previewKey });
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
const spinner = p.spinner();
|
|
33
|
-
spinner.start("Publishing preview...");
|
|
50
|
+
spinner.start("Publishing preview to live storefront...");
|
|
34
51
|
|
|
35
52
|
const result = await publishPreview(
|
|
36
53
|
config.apiBaseUrl,
|
|
37
54
|
credentials.apiKey,
|
|
38
55
|
config.storeId,
|
|
39
|
-
|
|
56
|
+
previewKey,
|
|
40
57
|
);
|
|
41
58
|
|
|
42
59
|
if (!result.ok) {
|
|
@@ -45,10 +62,8 @@ export const publish = async ({ skipBuild = false } = {}) => {
|
|
|
45
62
|
process.exit(1);
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
spinner.stop(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const { previewKey, ...rest } = config;
|
|
53
|
-
await writeConfig(rest);
|
|
65
|
+
spinner.stop(`Preview ${previewKey} published. Your live storefront has been updated.`);
|
|
66
|
+
if (previewUrl) {
|
|
67
|
+
p.log.message(` ${previewUrl}`);
|
|
68
|
+
}
|
|
54
69
|
};
|
package/lib/pull.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { loadConfigOrFail, isBuiltTheme, getDistDir } from "./config.mjs";
|
|
3
|
-
import { downloadStorefrontArchive } from "./api.mjs";
|
|
3
|
+
import { downloadStorefrontArchive, downloadPreviewArchive } from "./api.mjs";
|
|
4
|
+
import { fetchPreviewDetails } from "./preview.mjs";
|
|
4
5
|
import { extractZip } from "./zip.mjs";
|
|
5
6
|
|
|
6
7
|
/** @param {number} bytes */
|
|
@@ -10,32 +11,56 @@ const formatBytes = (bytes) => {
|
|
|
10
11
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
export const pull = async () => {
|
|
14
|
+
export const pull = async ({ previewKey } = {}) => {
|
|
14
15
|
const { config, credentials } = await loadConfigOrFail();
|
|
16
|
+
const previewDetails = previewKey
|
|
17
|
+
? await fetchPreviewDetails(
|
|
18
|
+
config.apiBaseUrl,
|
|
19
|
+
credentials.apiKey,
|
|
20
|
+
config.storeId,
|
|
21
|
+
previewKey,
|
|
22
|
+
)
|
|
23
|
+
: null;
|
|
15
24
|
|
|
16
25
|
const spinner = p.spinner();
|
|
17
|
-
|
|
26
|
+
const isPreviewPull = Boolean(previewKey);
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
spinner.start(
|
|
29
|
+
isPreviewPull
|
|
30
|
+
? `Downloading preview ${previewKey} from store #${config.storeId}...`
|
|
31
|
+
: `Downloading live theme from store #${config.storeId}...`,
|
|
23
32
|
);
|
|
24
33
|
|
|
34
|
+
const result = isPreviewPull
|
|
35
|
+
? await downloadPreviewArchive(
|
|
36
|
+
config.apiBaseUrl,
|
|
37
|
+
credentials.apiKey,
|
|
38
|
+
config.storeId,
|
|
39
|
+
previewKey,
|
|
40
|
+
)
|
|
41
|
+
: await downloadStorefrontArchive(
|
|
42
|
+
config.apiBaseUrl,
|
|
43
|
+
credentials.apiKey,
|
|
44
|
+
config.storeId,
|
|
45
|
+
);
|
|
46
|
+
|
|
25
47
|
if (!result.ok) {
|
|
26
48
|
spinner.stop("Download failed.", 1);
|
|
27
49
|
p.log.error(result.error);
|
|
28
50
|
process.exit(1);
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
spinner.
|
|
32
|
-
`Archive received (${formatBytes(result.data.length)}). Extracting...`,
|
|
33
|
-
);
|
|
53
|
+
spinner.message(`Extracting archive (${formatBytes(result.data.length)})...`);
|
|
34
54
|
|
|
35
55
|
const outputDir = (await isBuiltTheme()) ? getDistDir() : process.cwd();
|
|
36
56
|
const extractedFiles = await extractZip(result.data, outputDir);
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
const suffix = isPreviewPull ? ` from preview ${previewKey}` : "";
|
|
59
|
+
spinner.stop(
|
|
60
|
+
`${extractedFiles.length} file${extractedFiles.length === 1 ? "" : "s"} extracted${suffix}.`,
|
|
40
61
|
);
|
|
62
|
+
|
|
63
|
+
if (previewDetails?.ok) {
|
|
64
|
+
p.log.message(` ${previewDetails.data.url}`);
|
|
65
|
+
}
|
|
41
66
|
};
|
package/lib/push.mjs
CHANGED
|
@@ -3,6 +3,10 @@ import { loadConfigOrFail, isBuiltTheme, getDistDir } from "./config.mjs";
|
|
|
3
3
|
import { uploadPreviewZip } from "./api.mjs";
|
|
4
4
|
import { createZipFromDirectory } from "./archive.mjs";
|
|
5
5
|
import { build } from "./build.mjs";
|
|
6
|
+
import {
|
|
7
|
+
fetchPreviewDetails,
|
|
8
|
+
resolvePreviewKeyInteractively,
|
|
9
|
+
} from "./preview.mjs";
|
|
6
10
|
import { retryAsync } from "./retry.mjs";
|
|
7
11
|
|
|
8
12
|
/** @param {number} bytes */
|
|
@@ -19,11 +23,11 @@ export const pushPreparedDirectoryToPreview = async ({
|
|
|
19
23
|
previewKey,
|
|
20
24
|
rootDir,
|
|
21
25
|
spinner,
|
|
22
|
-
|
|
26
|
+
compressMessage = "Compressing files...",
|
|
23
27
|
uploadMessage,
|
|
24
28
|
retryMessage,
|
|
25
29
|
}) => {
|
|
26
|
-
spinner.message(
|
|
30
|
+
spinner.message(compressMessage);
|
|
27
31
|
|
|
28
32
|
const zipBuffer = await createZipFromDirectory(rootDir);
|
|
29
33
|
spinner.message(
|
|
@@ -45,14 +49,9 @@ export const pushPreparedDirectoryToPreview = async ({
|
|
|
45
49
|
);
|
|
46
50
|
};
|
|
47
51
|
|
|
48
|
-
export const push = async ({ skipBuild = false } = {}) => {
|
|
52
|
+
export const push = async ({ skipBuild = false, previewKey: previewKeyArg } = {}) => {
|
|
49
53
|
const { config, credentials } = await loadConfigOrFail();
|
|
50
54
|
|
|
51
|
-
if (!config.previewKey) {
|
|
52
|
-
p.log.error("No active preview. Create one with: tiendu preview create");
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
55
|
const builtTheme = await isBuiltTheme();
|
|
57
56
|
|
|
58
57
|
if (builtTheme && !skipBuild) {
|
|
@@ -62,15 +61,29 @@ export const push = async ({ skipBuild = false } = {}) => {
|
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
|
|
64
|
+
// Resolve preview key: explicit arg > interactive picker
|
|
65
|
+
const previewKey = previewKeyArg ?? await resolvePreviewKeyInteractively({ config, credentials });
|
|
66
|
+
const previewDetails = await fetchPreviewDetails(
|
|
67
|
+
config.apiBaseUrl,
|
|
68
|
+
credentials.apiKey,
|
|
69
|
+
config.storeId,
|
|
70
|
+
previewKey,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (!previewDetails.ok) {
|
|
74
|
+
p.log.error(`Preview ${previewKey} not found.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
const rootDir = builtTheme ? getDistDir() : process.cwd();
|
|
66
79
|
const spinner = p.spinner();
|
|
67
|
-
spinner.start("
|
|
80
|
+
spinner.start("Compressing files...");
|
|
68
81
|
|
|
69
82
|
const result = await pushPreparedDirectoryToPreview({
|
|
70
83
|
apiBaseUrl: config.apiBaseUrl,
|
|
71
84
|
apiKey: credentials.apiKey,
|
|
72
85
|
storeId: config.storeId,
|
|
73
|
-
previewKey
|
|
86
|
+
previewKey,
|
|
74
87
|
rootDir,
|
|
75
88
|
spinner,
|
|
76
89
|
});
|
|
@@ -81,5 +94,6 @@ export const push = async ({ skipBuild = false } = {}) => {
|
|
|
81
94
|
process.exit(1);
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
spinner.stop(
|
|
97
|
+
spinner.stop(`Files uploaded to preview ${previewKey}.`);
|
|
98
|
+
p.log.message(` ${previewDetails.data.url}`);
|
|
85
99
|
};
|