tiendu 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -13
- package/bin/tiendu.js +43 -16
- package/bin/tiendu.mjs +1 -136
- package/lib/api.mjs +82 -30
- package/lib/archive.mjs +30 -0
- package/lib/assets.mjs +245 -0
- package/lib/build.mjs +299 -41
- package/lib/dev.mjs +234 -144
- package/lib/fs-utils.mjs +35 -0
- package/lib/local-preview.mjs +393 -0
- package/lib/postcss.mjs +166 -0
- package/lib/preview.mjs +279 -73
- package/lib/publish.mjs +32 -17
- package/lib/pull.mjs +37 -12
- package/lib/push.mjs +60 -57
- package/lib/retry.mjs +69 -0
- package/package.json +2 -2
package/lib/push.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import * as p from "@clack/prompts";
|
|
4
|
-
import { zipSync } from "fflate";
|
|
5
2
|
import { loadConfigOrFail, isBuiltTheme, getDistDir } from "./config.mjs";
|
|
6
3
|
import { uploadPreviewZip } from "./api.mjs";
|
|
4
|
+
import { createZipFromDirectory } from "./archive.mjs";
|
|
7
5
|
import { build } from "./build.mjs";
|
|
6
|
+
import {
|
|
7
|
+
fetchPreviewDetails,
|
|
8
|
+
resolvePreviewKeyInteractively,
|
|
9
|
+
} from "./preview.mjs";
|
|
10
|
+
import { retryAsync } from "./retry.mjs";
|
|
8
11
|
|
|
9
12
|
/** @param {number} bytes */
|
|
10
13
|
const formatBytes = (bytes) => {
|
|
@@ -13,53 +16,42 @@ const formatBytes = (bytes) => {
|
|
|
13
16
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
14
17
|
};
|
|
15
18
|
|
|
16
|
-
const
|
|
19
|
+
export const pushPreparedDirectoryToPreview = async ({
|
|
20
|
+
apiBaseUrl,
|
|
21
|
+
apiKey,
|
|
22
|
+
storeId,
|
|
23
|
+
previewKey,
|
|
24
|
+
rootDir,
|
|
25
|
+
spinner,
|
|
26
|
+
compressMessage = "Compressing files...",
|
|
27
|
+
uploadMessage,
|
|
28
|
+
retryMessage,
|
|
29
|
+
}) => {
|
|
30
|
+
spinner.message(compressMessage);
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* @returns {Promise<string[]>}
|
|
23
|
-
*/
|
|
24
|
-
const listAllFiles = async (rootDir, currentDir) => {
|
|
25
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
26
|
-
const files = [];
|
|
27
|
-
for (const entry of entries) {
|
|
28
|
-
if (isDotfile(entry.name)) continue;
|
|
29
|
-
const abs = path.join(currentDir, entry.name);
|
|
30
|
-
if (entry.isDirectory()) {
|
|
31
|
-
files.push(...(await listAllFiles(rootDir, abs)));
|
|
32
|
-
} else if (entry.isFile()) {
|
|
33
|
-
files.push(abs);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return files;
|
|
37
|
-
};
|
|
32
|
+
const zipBuffer = await createZipFromDirectory(rootDir);
|
|
33
|
+
spinner.message(
|
|
34
|
+
uploadMessage ?? `Uploading to preview ${previewKey} (${formatBytes(zipBuffer.length)})...`,
|
|
35
|
+
);
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return Buffer.from(zipSync(entries, { level: 6 }));
|
|
37
|
+
return retryAsync(
|
|
38
|
+
() => uploadPreviewZip(apiBaseUrl, apiKey, storeId, previewKey, zipBuffer),
|
|
39
|
+
{
|
|
40
|
+
attempts: 3,
|
|
41
|
+
shouldRetry: (uploadResult) => !uploadResult.ok && Boolean(uploadResult.retriable),
|
|
42
|
+
onRetry: async (uploadResult, nextAttempt) => {
|
|
43
|
+
spinner.message(
|
|
44
|
+
retryMessage?.(uploadResult, nextAttempt) ??
|
|
45
|
+
`Upload failed. Retrying ${nextAttempt}/3... ${uploadResult.error}`,
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
);
|
|
53
50
|
};
|
|
54
51
|
|
|
55
|
-
export const push = async ({ skipBuild = false } = {}) => {
|
|
52
|
+
export const push = async ({ skipBuild = false, previewKey: previewKeyArg } = {}) => {
|
|
56
53
|
const { config, credentials } = await loadConfigOrFail();
|
|
57
54
|
|
|
58
|
-
if (!config.previewKey) {
|
|
59
|
-
p.log.error("No active preview. Create one with: tiendu preview create");
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
55
|
const builtTheme = await isBuiltTheme();
|
|
64
56
|
|
|
65
57
|
if (builtTheme && !skipBuild) {
|
|
@@ -69,28 +61,39 @@ export const push = async ({ skipBuild = false } = {}) => {
|
|
|
69
61
|
}
|
|
70
62
|
}
|
|
71
63
|
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const zipBuffer = await createZipFromDirectory(rootDir);
|
|
77
|
-
spinner.message(
|
|
78
|
-
`Uploading to preview ${config.previewKey} (${formatBytes(zipBuffer.length)})...`,
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const result = await uploadPreviewZip(
|
|
64
|
+
// Resolve preview key: explicit arg > interactive picker
|
|
65
|
+
const previewKey = previewKeyArg ?? await resolvePreviewKeyInteractively({ config, credentials });
|
|
66
|
+
const previewDetails = await fetchPreviewDetails(
|
|
82
67
|
config.apiBaseUrl,
|
|
83
68
|
credentials.apiKey,
|
|
84
69
|
config.storeId,
|
|
85
|
-
|
|
86
|
-
zipBuffer,
|
|
70
|
+
previewKey,
|
|
87
71
|
);
|
|
88
72
|
|
|
73
|
+
if (!previewDetails.ok) {
|
|
74
|
+
p.log.error(`Preview ${previewKey} not found.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rootDir = builtTheme ? getDistDir() : process.cwd();
|
|
79
|
+
const spinner = p.spinner();
|
|
80
|
+
spinner.start("Compressing files...");
|
|
81
|
+
|
|
82
|
+
const result = await pushPreparedDirectoryToPreview({
|
|
83
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
84
|
+
apiKey: credentials.apiKey,
|
|
85
|
+
storeId: config.storeId,
|
|
86
|
+
previewKey,
|
|
87
|
+
rootDir,
|
|
88
|
+
spinner,
|
|
89
|
+
});
|
|
90
|
+
|
|
89
91
|
if (!result.ok) {
|
|
90
92
|
spinner.stop("Upload failed.", 1);
|
|
91
93
|
p.log.error(result.error);
|
|
92
94
|
process.exit(1);
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
spinner.stop(
|
|
97
|
+
spinner.stop(`Files uploaded to preview ${previewKey}.`);
|
|
98
|
+
p.log.message(` ${previewDetails.data.url}`);
|
|
96
99
|
};
|
package/lib/retry.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @template T
|
|
5
|
+
* @param {(attempt: number) => Promise<T>} operation
|
|
6
|
+
* @param {{
|
|
7
|
+
* attempts?: number,
|
|
8
|
+
* baseDelayMs?: number,
|
|
9
|
+
* maxDelayMs?: number,
|
|
10
|
+
* shouldRetry?: (result: T, attempt: number) => boolean,
|
|
11
|
+
* shouldRetryError?: (error: unknown, attempt: number) => boolean,
|
|
12
|
+
* onRetry?: (result: T, nextAttempt: number, delayMs: number) => void | Promise<void>,
|
|
13
|
+
* onRetryError?: (error: unknown, nextAttempt: number, delayMs: number) => void | Promise<void>,
|
|
14
|
+
* }} [options]
|
|
15
|
+
* @returns {Promise<T>}
|
|
16
|
+
*/
|
|
17
|
+
export const retryAsync = async (operation, options = {}) => {
|
|
18
|
+
const {
|
|
19
|
+
attempts = 3,
|
|
20
|
+
baseDelayMs = 300,
|
|
21
|
+
maxDelayMs = 2000,
|
|
22
|
+
shouldRetry = () => false,
|
|
23
|
+
shouldRetryError = () => true,
|
|
24
|
+
onRetry,
|
|
25
|
+
onRetryError,
|
|
26
|
+
} = options;
|
|
27
|
+
|
|
28
|
+
let lastResult;
|
|
29
|
+
let lastError;
|
|
30
|
+
|
|
31
|
+
const getDelayMs = (attempt) =>
|
|
32
|
+
Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1)) +
|
|
33
|
+
Math.floor(Math.random() * 100);
|
|
34
|
+
|
|
35
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
36
|
+
try {
|
|
37
|
+
const result = await operation(attempt);
|
|
38
|
+
lastResult = result;
|
|
39
|
+
|
|
40
|
+
if (!shouldRetry(result, attempt) || attempt === attempts) {
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const delayMs = getDelayMs(attempt);
|
|
45
|
+
|
|
46
|
+
if (onRetry) {
|
|
47
|
+
await onRetry(result, attempt + 1, delayMs);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await sleep(delayMs);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
lastError = error;
|
|
53
|
+
|
|
54
|
+
if (!shouldRetryError(error, attempt) || attempt === attempts) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const delayMs = getDelayMs(attempt);
|
|
59
|
+
if (onRetryError) {
|
|
60
|
+
await onRetryError(error, attempt + 1, delayMs);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await sleep(delayMs);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (lastError) throw lastError;
|
|
68
|
+
return lastResult;
|
|
69
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tiendu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CLI para desarrollar y publicar temas en Tiendu",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"dev": "node bin/tiendu.
|
|
16
|
+
"dev": "node bin/tiendu.js"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=20"
|