tiendu 0.3.1 → 0.5.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/lib/push.mjs CHANGED
@@ -1,9 +1,9 @@
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";
5
+ import { build } from "./build.mjs";
6
+ import { retryAsync } from "./retry.mjs";
7
7
 
8
8
  /** @param {number} bytes */
9
9
  const formatBytes = (bytes) => {
@@ -12,46 +12,40 @@ const formatBytes = (bytes) => {
12
12
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
13
13
  };
14
14
 
15
- const isDotfile = (name) => name.startsWith(".");
15
+ export const pushPreparedDirectoryToPreview = async ({
16
+ apiBaseUrl,
17
+ apiKey,
18
+ storeId,
19
+ previewKey,
20
+ rootDir,
21
+ spinner,
22
+ packMessage = "Packing files...",
23
+ uploadMessage,
24
+ retryMessage,
25
+ }) => {
26
+ spinner.message(packMessage);
16
27
 
17
- /**
18
- * Recursively list all files, skipping dotfiles/dotdirs.
19
- * @param {string} rootDir
20
- * @param {string} currentDir
21
- * @returns {Promise<string[]>}
22
- */
23
- const listAllFiles = async (rootDir, currentDir) => {
24
- const entries = await readdir(currentDir, { withFileTypes: true });
25
- const files = [];
26
- for (const entry of entries) {
27
- if (isDotfile(entry.name)) continue;
28
- const abs = path.join(currentDir, entry.name);
29
- if (entry.isDirectory()) {
30
- files.push(...(await listAllFiles(rootDir, abs)));
31
- } else if (entry.isFile()) {
32
- files.push(abs);
33
- }
34
- }
35
- return files;
36
- };
28
+ const zipBuffer = await createZipFromDirectory(rootDir);
29
+ spinner.message(
30
+ uploadMessage ?? `Uploading to preview ${previewKey} (${formatBytes(zipBuffer.length)})...`,
31
+ );
37
32
 
38
- /**
39
- * Create a zip buffer from the current directory, skipping dotfiles.
40
- * @param {string} rootDir
41
- * @returns {Promise<Buffer>}
42
- */
43
- const createZipFromDirectory = async (rootDir) => {
44
- const absoluteFiles = await listAllFiles(rootDir, rootDir);
45
- /** @type {Record<string, Uint8Array>} */
46
- const entries = {};
47
- for (const abs of absoluteFiles) {
48
- const rel = path.relative(rootDir, abs).split(path.sep).join("/");
49
- entries[rel] = new Uint8Array(await readFile(abs));
50
- }
51
- return Buffer.from(zipSync(entries, { level: 6 }));
33
+ return retryAsync(
34
+ () => uploadPreviewZip(apiBaseUrl, apiKey, storeId, previewKey, zipBuffer),
35
+ {
36
+ attempts: 3,
37
+ shouldRetry: (uploadResult) => !uploadResult.ok && Boolean(uploadResult.retriable),
38
+ onRetry: async (uploadResult, nextAttempt) => {
39
+ spinner.message(
40
+ retryMessage?.(uploadResult, nextAttempt) ??
41
+ `Upload failed. Retrying ${nextAttempt}/3... ${uploadResult.error}`,
42
+ );
43
+ },
44
+ },
45
+ );
52
46
  };
53
47
 
54
- export const push = async () => {
48
+ export const push = async ({ skipBuild = false } = {}) => {
55
49
  const { config, credentials } = await loadConfigOrFail();
56
50
 
57
51
  if (!config.previewKey) {
@@ -59,22 +53,27 @@ export const push = async () => {
59
53
  process.exit(1);
60
54
  }
61
55
 
62
- const rootDir = (await isBuiltTheme()) ? getDistDir() : process.cwd();
56
+ const builtTheme = await isBuiltTheme();
57
+
58
+ if (builtTheme && !skipBuild) {
59
+ const result = await build();
60
+ if (!result.ok) {
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ const rootDir = builtTheme ? getDistDir() : process.cwd();
63
66
  const spinner = p.spinner();
64
67
  spinner.start("Packing files...");
65
68
 
66
- const zipBuffer = await createZipFromDirectory(rootDir);
67
- spinner.message(
68
- `Uploading to preview ${config.previewKey} (${formatBytes(zipBuffer.length)})...`,
69
- );
70
-
71
- const result = await uploadPreviewZip(
72
- config.apiBaseUrl,
73
- credentials.apiKey,
74
- config.storeId,
75
- config.previewKey,
76
- zipBuffer,
77
- );
69
+ const result = await pushPreparedDirectoryToPreview({
70
+ apiBaseUrl: config.apiBaseUrl,
71
+ apiKey: credentials.apiKey,
72
+ storeId: config.storeId,
73
+ previewKey: config.previewKey,
74
+ rootDir,
75
+ spinner,
76
+ });
78
77
 
79
78
  if (!result.ok) {
80
79
  spinner.stop("Upload failed.", 1);
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.1",
3
+ "version": "0.5.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.mjs"
16
+ "dev": "node bin/tiendu.js"
17
17
  },
18
18
  "engines": {
19
19
  "node": ">=20"