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/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 isDotfile = (name) => name.startsWith(".");
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
- * Recursively list all files, skipping dotfiles/dotdirs.
20
- * @param {string} rootDir
21
- * @param {string} currentDir
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
- * Create a zip buffer from the current directory, skipping dotfiles.
41
- * @param {string} rootDir
42
- * @returns {Promise<Buffer>}
43
- */
44
- const createZipFromDirectory = async (rootDir) => {
45
- const absoluteFiles = await listAllFiles(rootDir, rootDir);
46
- /** @type {Record<string, Uint8Array>} */
47
- const entries = {};
48
- for (const abs of absoluteFiles) {
49
- const rel = path.relative(rootDir, abs).split(path.sep).join("/");
50
- entries[rel] = new Uint8Array(await readFile(abs));
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
- const rootDir = builtTheme ? getDistDir() : process.cwd();
73
- const spinner = p.spinner();
74
- spinner.start("Packing files...");
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
- config.previewKey,
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("Files uploaded to preview.");
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.4.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.mjs"
16
+ "dev": "node bin/tiendu.js"
17
17
  },
18
18
  "engines": {
19
19
  "node": ">=20"