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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Official CLI for [Tiendu](https://tiendu.uy) — develop and publish storefront themes from your local machine.
4
4
 
5
- Download your store's theme, edit files locally, preview changes live with a shareable URL, and publish when you're ready — all from the terminal.
5
+ Download your store's theme, edit files locally, preview changes live with a local auto-reloading URL plus a sharable preview URL, and publish when you're ready — all from the terminal.
6
6
 
7
7
  ---
8
8
 
@@ -44,7 +44,7 @@ tiendu init
44
44
  tiendu dev
45
45
  ```
46
46
 
47
- `tiendu dev` creates a remote preview, builds your source files, uploads the output, and watches for changes. It prints a shareable URL like:
47
+ `tiendu dev` creates a remote preview, builds your source files, runs an initial push from the prepared output, and then watches for changes. It prints a local live-preview URL first, plus a sharable preview URL like:
48
48
 
49
49
  ```
50
50
  http://preview-xxxxxxxxxxxx.tiendu.uy/
@@ -52,6 +52,9 @@ http://preview-xxxxxxxxxxxx.tiendu.uy/
52
52
 
53
53
  The preview renders with the real Tiendu engine — same output as production.
54
54
 
55
+ When `tiendu dev` starts, it always re-syncs your current local files to the active preview before watching for changes.
56
+ It also starts a local live-preview URL that proxies the preview and auto-reloads after successful syncs.
57
+
55
58
  ---
56
59
 
57
60
  ## Commands
@@ -91,9 +94,11 @@ tiendu build
91
94
 
92
95
  The build:
93
96
 
94
- 1. Copies theme files (`layout/`, `templates/`, `snippets/`, `assets/`) to `dist/`
95
- 2. Discovers entry points in `src/layout/` and `src/templates/`
96
- 3. Bundles JS/TS and CSS via esbuild into `dist/assets/`
97
+ 1. Copies theme files from `src/layout/`, `src/templates/`, and `src/snippets/` to `dist/`
98
+ 2. Flattens static files from `src/assets/` into `dist/assets/`
99
+ 3. Discovers entry points in `src/layout/` and `src/templates/`
100
+ 4. Bundles JS/TS and CSS into `dist/assets/`
101
+ 5. Runs project PostCSS plugins for CSS entries when available (for example Tailwind v4)
97
102
 
98
103
  Entry naming convention:
99
104
 
@@ -115,7 +120,10 @@ tiendu dev
115
120
  ```
116
121
 
117
122
  - Prints the preview URL on start
123
+ - Re-syncs the full local theme to the preview on startup
118
124
  - Syncs file creates, edits and deletes
125
+ - Retries failed file sync operations up to 3 times before giving up
126
+ - Starts a local live-preview URL on `localhost` that refreshes after successful uploads
119
127
  - Handles both text and binary files (images, fonts, etc.)
120
128
  - Press `Ctrl+C` to stop
121
129
 
@@ -126,22 +134,31 @@ tiendu dev
126
134
  Zips and uploads files to the active preview, replacing its content entirely.
127
135
 
128
136
  - **Buildless themes:** uploads from the current directory.
129
- - **Built themes:** uploads from `dist/`.
137
+ - **Built themes:** runs `tiendu build` first, then uploads from `dist/`.
130
138
 
131
139
  ```bash
132
140
  tiendu push
141
+ tiendu push --skip-build
133
142
  ```
134
143
 
144
+ Use `--skip-build` to upload the existing `dist/` output without rebuilding.
145
+
135
146
  ---
136
147
 
137
148
  ### `tiendu publish`
138
149
 
139
150
  Publishes the active preview to the live storefront. Visitors will see the new theme immediately. All previews for the store are removed after publishing.
140
151
 
152
+ - **Buildless themes:** publishes the active preview as-is.
153
+ - **Built themes:** builds the theme, uploads the latest `dist/` output to the preview, then publishes it.
154
+
141
155
  ```bash
142
156
  tiendu publish
157
+ tiendu publish --skip-build
143
158
  ```
144
159
 
160
+ Use `--skip-build` to publish after uploading the existing `dist/` output without rebuilding.
161
+
145
162
  ---
146
163
 
147
164
  ### `tiendu check-updates`
@@ -263,26 +280,59 @@ my-theme/
263
280
  ├── .gitignore
264
281
  ├── src/
265
282
  │ ├── layout/
283
+ │ │ ├── theme.liquid # copied to dist/layout/theme.liquid
266
284
  │ │ ├── theme.ts # layout TS entry → layout-theme.bundle.js
267
285
  │ │ └── theme.css # layout CSS entry → layout-theme.bundle.css
268
286
  │ ├── templates/
287
+ │ │ ├── product.liquid # copied to dist/templates/product.liquid
269
288
  │ │ ├── product.ts # template TS entry → template-product.bundle.js
270
289
  │ │ └── product.css # template CSS entry → template-product.bundle.css
290
+ │ ├── snippets/ # Liquid snippets copied to dist/snippets/
291
+ │ ├── assets/ # source assets → flattened into dist/assets/
271
292
  │ ├── lib/ # shared modules (bundled into entries, not served)
272
293
  │ └── css/ # shared CSS (imported by entry CSS)
273
- ├── layout/ # Liquid layouts
274
- ├── templates/ # Liquid templates
275
- ├── snippets/ # Liquid snippets
276
- ├── assets/ # static assets (SVGs, images, fonts)
277
294
  └── dist/ # build output (gitignored, uploaded to Tiendu)
278
295
  ```
279
296
 
280
297
  ### How it works
281
298
 
282
- 1. Source JS/TS/CSS in `src/` is bundled by esbuild into `dist/assets/`
283
- 2. Liquid files and static assets are copied from root to `dist/`
284
- 3. `dist/` is what gets uploaded it looks like a normal Tiendu theme
285
- 4. Liquid templates reference bundles via `asset_url` (e.g. `{{ 'layout-theme.bundle.js' | asset_url | script_tag }}`)
299
+ 1. Source assets in `src/assets/` are flattened into `dist/assets/` (`payment-methods/visa.svg` becomes `payment-methods___visa.svg`)
300
+ 2. Source JS/TS/CSS in `src/` is bundled by esbuild into `dist/assets/`
301
+ 3. CSS entries also run through your local PostCSS pipeline when configured
302
+ 4. Liquid files are copied from `src/` to `dist/`
303
+ 5. `dist/` is what gets uploaded — it looks like a normal Tiendu theme
304
+ 6. Liquid templates reference bundles and assets via `asset_url` (e.g. `{{ 'layout-theme.bundle.js' | asset_url | script_tag }}` or `{{ 'payment-methods/visa.svg' | asset_url }}`)
305
+
306
+ ### Tailwind v4
307
+
308
+ Built themes can use Tailwind v4 in CSS entry files.
309
+
310
+ Install it in your theme project:
311
+
312
+ ```bash
313
+ npm install -D tailwindcss @tailwindcss/postcss postcss
314
+ ```
315
+
316
+ Then import Tailwind from a CSS entry such as `src/layout/theme.css`:
317
+
318
+ ```css
319
+ @import "tailwindcss";
320
+ ```
321
+
322
+ You can either:
323
+
324
+ - rely on Tiendu CLI's automatic Tailwind detection when `@tailwindcss/postcss` is installed, or
325
+ - add a local `postcss.config.mjs` / `postcss.config.js` / `postcss.config.cjs` / `postcss.config.json`
326
+
327
+ Example `postcss.config.mjs`:
328
+
329
+ ```js
330
+ export default {
331
+ plugins: {
332
+ "@tailwindcss/postcss": {},
333
+ },
334
+ };
335
+ ```
286
336
 
287
337
  ### tiendu.config.json
288
338
 
package/bin/tiendu.js CHANGED
@@ -26,9 +26,10 @@ Usage:
26
26
  tiendu init [dir] Set up a theme project (optionally in a new directory)
27
27
  tiendu pull Download the live theme from your store
28
28
  tiendu build Build a theme (requires tiendu.config.json)
29
- tiendu push Upload local files to the active preview (full replace)
29
+ tiendu push [--skip-build] Upload local files to the active preview (full replace)
30
30
  tiendu dev Start dev mode: auto-sync changes to a live preview URL
31
- tiendu publish Publish the active preview to the live storefront
31
+ tiendu publish [--skip-build]
32
+ Publish the active preview to the live storefront
32
33
 
33
34
  tiendu preview Show the active preview details
34
35
  tiendu preview create Create a new remote preview
@@ -55,6 +56,7 @@ const main = async () => {
55
56
  const args = process.argv.slice(2);
56
57
  const command = args[0];
57
58
  const subcommand = args[1];
59
+ const skipBuild = args.includes("--skip-build");
58
60
 
59
61
  if (
60
62
  command === "version" ||
@@ -100,7 +102,7 @@ const main = async () => {
100
102
  }
101
103
 
102
104
  if (command === "push") {
103
- await push();
105
+ await push({ skipBuild });
104
106
  return;
105
107
  }
106
108
 
@@ -110,7 +112,7 @@ const main = async () => {
110
112
  }
111
113
 
112
114
  if (command === "publish") {
113
- await publish();
115
+ await publish({ skipBuild });
114
116
  return;
115
117
  }
116
118
 
package/bin/tiendu.mjs CHANGED
@@ -1,136 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { init } from "../lib/init.mjs";
4
- import { pull } from "../lib/pull.mjs";
5
- import { push } from "../lib/push.mjs";
6
- import { dev } from "../lib/dev.mjs";
7
- import { publish } from "../lib/publish.mjs";
8
- import {
9
- previewCreate,
10
- previewList,
11
- previewDelete,
12
- previewOpen,
13
- } from "../lib/preview.mjs";
14
- import {
15
- checkForUpdates,
16
- checkForUpdatesNow,
17
- getCurrentVersion,
18
- } from "../lib/update-check.mjs";
19
-
20
- const HELP = `
21
- tiendu — CLI para desarrollar temas de Tiendu
22
-
23
- Uso:
24
- tiendu init Inicializar un tema en el directorio actual
25
- tiendu pull Descargar el tema live desde Tiendu
26
- tiendu push Subir archivos locales al preview activo (ZIP)
27
- tiendu dev Modo desarrollo: watch + sync automático
28
- tiendu publish Publicar el preview activo al storefront live
29
-
30
- tiendu preview create Crear un preview remoto
31
- tiendu preview list Listar previews de la tienda
32
- tiendu preview delete Eliminar el preview activo
33
- tiendu preview open Abrir la URL del preview en el navegador
34
-
35
- tiendu check-updates Buscar una nueva version del CLI
36
- tiendu version Mostrar la version actual del CLI
37
-
38
- tiendu help Mostrar esta ayuda
39
-
40
- Opciones:
41
- --help, -h Mostrar esta ayuda
42
- --version, -v Mostrar la version actual del CLI
43
- `;
44
-
45
- const main = async () => {
46
- const args = process.argv.slice(2);
47
- const command = args[0];
48
- const subcommand = args[1];
49
-
50
- if (
51
- command === "version" ||
52
- command === "--version" ||
53
- command === "-v"
54
- ) {
55
- console.log(getCurrentVersion());
56
- process.exit(0);
57
- }
58
-
59
- if (
60
- !command ||
61
- command === "help" ||
62
- command === "--help" ||
63
- command === "-h"
64
- ) {
65
- console.log(HELP.trim());
66
- process.exit(0);
67
- }
68
-
69
- if (command === "check-updates") {
70
- await checkForUpdatesNow();
71
- return;
72
- }
73
-
74
- await checkForUpdates();
75
-
76
- if (command === "init") {
77
- await init();
78
- return;
79
- }
80
-
81
- if (command === "pull") {
82
- await pull();
83
- return;
84
- }
85
-
86
- if (command === "push") {
87
- await push();
88
- return;
89
- }
90
-
91
- if (command === "dev") {
92
- await dev();
93
- return;
94
- }
95
-
96
- if (command === "publish") {
97
- await publish();
98
- return;
99
- }
100
-
101
- if (command === "preview") {
102
- if (subcommand === "create") {
103
- const name = args[2];
104
- await previewCreate(name);
105
- return;
106
- }
107
-
108
- if (subcommand === "list") {
109
- await previewList();
110
- return;
111
- }
112
-
113
- if (subcommand === "delete") {
114
- await previewDelete();
115
- return;
116
- }
117
-
118
- if (subcommand === "open") {
119
- await previewOpen();
120
- return;
121
- }
122
-
123
- console.error(`Subcomando desconocido: preview ${subcommand ?? "(vacío)"}`);
124
- console.log(HELP.trim());
125
- process.exit(1);
126
- }
127
-
128
- console.error(`Comando desconocido: ${command}`);
129
- console.log(HELP.trim());
130
- process.exit(1);
131
- };
132
-
133
- main().catch((error) => {
134
- console.error(error.message || error);
135
- process.exit(1);
136
- });
3
+ import "./tiendu.js";
package/lib/api.mjs CHANGED
@@ -2,9 +2,14 @@
2
2
  * @param {string} apiBaseUrl
3
3
  * @param {string} apiKey
4
4
  * @param {string} path
5
- * @param {{ method?: string, body?: string | Buffer | Uint8Array, contentType?: string }} [options]
5
+ * @param {{ method?: string, body?: string | Buffer | Uint8Array, contentType?: string, signal?: AbortSignal }} [options]
6
6
  * @returns {Promise<Response>}
7
7
  */
8
+ const REQUEST_TIMEOUT_MS = 30_000;
9
+
10
+ const isRetriableStatus = (status) =>
11
+ status === 408 || status === 425 || status === 429 || status >= 500;
12
+
8
13
  export const apiFetch = (apiBaseUrl, apiKey, path, options = {}) => {
9
14
  const url = `${apiBaseUrl}${path}`;
10
15
  /** @type {Record<string, string>} */
@@ -22,6 +27,7 @@ export const apiFetch = (apiBaseUrl, apiKey, path, options = {}) => {
22
27
  method: options.method ?? "GET",
23
28
  headers,
24
29
  body: options.body,
30
+ signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
25
31
  });
26
32
  };
27
33
 
@@ -126,7 +132,7 @@ export const downloadStorefrontArchive = async (
126
132
  * @param {number} storeId
127
133
  * @param {string} previewKey
128
134
  * @param {Buffer} zipBuffer
129
- * @returns {Promise<{ ok: true } | { ok: false, error: string }>}
135
+ * @returns {Promise<{ ok: true } | { ok: false, error: string, retriable?: boolean }>}
130
136
  */
131
137
  export const uploadPreviewZip = async (
132
138
  apiBaseUrl,
@@ -152,51 +158,7 @@ export const uploadPreviewZip = async (
152
158
  return {
153
159
  ok: false,
154
160
  error: `Error del servidor: ${response.status}${body ? ` — ${body}` : ""}`,
155
- };
156
- }
157
-
158
- return { ok: true };
159
- } catch (error) {
160
- return { ok: false, error: `No se pudo subir: ${error.message}` };
161
- }
162
- };
163
-
164
- /**
165
- * Upload a single file to a preview.
166
- *
167
- * @param {string} apiBaseUrl
168
- * @param {string} apiKey
169
- * @param {number} storeId
170
- * @param {string} previewKey
171
- * @param {string} filePath - relative path within the preview
172
- * @param {string} content - file content
173
- * @returns {Promise<{ ok: true } | { ok: false, error: string }>}
174
- */
175
- export const uploadPreviewFile = async (
176
- apiBaseUrl,
177
- apiKey,
178
- storeId,
179
- previewKey,
180
- filePath,
181
- content,
182
- ) => {
183
- try {
184
- const query = new URLSearchParams({ path: filePath }).toString();
185
- const response = await apiFetch(
186
- apiBaseUrl,
187
- apiKey,
188
- `/api/v2/stores/${storeId}/theme-previews/${previewKey}/file?${query}`,
189
- {
190
- method: "PUT",
191
- body: JSON.stringify({ content }),
192
- },
193
- );
194
-
195
- if (!response.ok) {
196
- const body = await response.text().catch(() => "");
197
- return {
198
- ok: false,
199
- error: `Error subiendo ${filePath}: ${response.status}${body ? ` — ${body}` : ""}`,
161
+ retriable: isRetriableStatus(response.status),
200
162
  };
201
163
  }
202
164
 
@@ -204,7 +166,8 @@ export const uploadPreviewFile = async (
204
166
  } catch (error) {
205
167
  return {
206
168
  ok: false,
207
- error: `Error subiendo ${filePath}: ${error.message}`,
169
+ error: `No se pudo subir: ${error.message}`,
170
+ retriable: true,
208
171
  };
209
172
  }
210
173
  };
@@ -219,7 +182,7 @@ export const uploadPreviewFile = async (
219
182
  * @param {string} previewKey
220
183
  * @param {string} relativePath
221
184
  * @param {Buffer} fileBuffer
222
- * @returns {Promise<{ ok: true } | { ok: false, error: string }>}
185
+ * @returns {Promise<{ ok: true } | { ok: false, error: string, retriable?: boolean }>}
223
186
  */
224
187
  export const uploadPreviewFileMultipart = async (
225
188
  apiBaseUrl,
@@ -249,6 +212,7 @@ export const uploadPreviewFileMultipart = async (
249
212
  Authorization: `Bearer ${apiKey}`,
250
213
  },
251
214
  body: formData,
215
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
252
216
  },
253
217
  );
254
218
 
@@ -257,6 +221,7 @@ export const uploadPreviewFileMultipart = async (
257
221
  return {
258
222
  ok: false,
259
223
  error: `Error subiendo ${relativePath}: ${response.status}${body ? ` — ${body}` : ""}`,
224
+ retriable: isRetriableStatus(response.status),
260
225
  };
261
226
  }
262
227
 
@@ -265,6 +230,7 @@ export const uploadPreviewFileMultipart = async (
265
230
  return {
266
231
  ok: false,
267
232
  error: `Error subiendo ${relativePath}: ${error.message}`,
233
+ retriable: true,
268
234
  };
269
235
  }
270
236
  };
@@ -277,7 +243,7 @@ export const uploadPreviewFileMultipart = async (
277
243
  * @param {number} storeId
278
244
  * @param {string} previewKey
279
245
  * @param {string} filePath
280
- * @returns {Promise<{ ok: true } | { ok: false, error: string }>}
246
+ * @returns {Promise<{ ok: true } | { ok: false, error: string, retriable?: boolean }>}
281
247
  */
282
248
  export const deletePreviewFile = async (
283
249
  apiBaseUrl,
@@ -302,6 +268,7 @@ export const deletePreviewFile = async (
302
268
  return {
303
269
  ok: false,
304
270
  error: `Error eliminando ${filePath}: ${response.status}${body ? ` — ${body}` : ""}`,
271
+ retriable: isRetriableStatus(response.status),
305
272
  };
306
273
  }
307
274
 
@@ -310,6 +277,7 @@ export const deletePreviewFile = async (
310
277
  return {
311
278
  ok: false,
312
279
  error: `Error eliminando ${filePath}: ${error.message}`,
280
+ retriable: true,
313
281
  };
314
282
  }
315
283
  };
@@ -0,0 +1,30 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { zipSync } from "fflate";
4
+ import { listFilesRecursive } from "./fs-utils.mjs";
5
+
6
+ /**
7
+ * @param {string} rootDir
8
+ * @returns {Promise<string[]>}
9
+ */
10
+ export const listAllFiles = async (rootDir) => listFilesRecursive(rootDir);
11
+
12
+ /**
13
+ * @param {string} rootDir
14
+ * @returns {Promise<Buffer>}
15
+ */
16
+ export const createZipFromDirectory = async (rootDir) => {
17
+ const absoluteFiles = await listAllFiles(rootDir);
18
+ /** @type {Record<string, Uint8Array>} */
19
+ const entries = {};
20
+
21
+ for (const absolutePath of absoluteFiles) {
22
+ const relativePath = path
23
+ .relative(rootDir, absolutePath)
24
+ .split(path.sep)
25
+ .join("/");
26
+ entries[relativePath] = new Uint8Array(await readFile(absolutePath));
27
+ }
28
+
29
+ return Buffer.from(zipSync(entries, { level: 6 }));
30
+ };