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/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
|
|
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,
|
|
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,13 @@ tiendu build
|
|
|
91
94
|
|
|
92
95
|
The build:
|
|
93
96
|
|
|
94
|
-
1. Copies theme files
|
|
95
|
-
2.
|
|
96
|
-
3.
|
|
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)
|
|
102
|
+
|
|
103
|
+
For TypeScript source, extensionless relative imports such as `import { initHeaderCart } from '../lib/scripts/cart'` are supported and recommended.
|
|
97
104
|
|
|
98
105
|
Entry naming convention:
|
|
99
106
|
|
|
@@ -115,7 +122,10 @@ tiendu dev
|
|
|
115
122
|
```
|
|
116
123
|
|
|
117
124
|
- Prints the preview URL on start
|
|
125
|
+
- Re-syncs the full local theme to the preview on startup
|
|
118
126
|
- Syncs file creates, edits and deletes
|
|
127
|
+
- Retries failed file sync operations up to 3 times before giving up
|
|
128
|
+
- Starts a local live-preview URL on `localhost` that refreshes after successful uploads
|
|
119
129
|
- Handles both text and binary files (images, fonts, etc.)
|
|
120
130
|
- Press `Ctrl+C` to stop
|
|
121
131
|
|
|
@@ -272,26 +282,59 @@ my-theme/
|
|
|
272
282
|
├── .gitignore
|
|
273
283
|
├── src/
|
|
274
284
|
│ ├── layout/
|
|
285
|
+
│ │ ├── theme.liquid # copied to dist/layout/theme.liquid
|
|
275
286
|
│ │ ├── theme.ts # layout TS entry → layout-theme.bundle.js
|
|
276
287
|
│ │ └── theme.css # layout CSS entry → layout-theme.bundle.css
|
|
277
288
|
│ ├── templates/
|
|
289
|
+
│ │ ├── product.liquid # copied to dist/templates/product.liquid
|
|
278
290
|
│ │ ├── product.ts # template TS entry → template-product.bundle.js
|
|
279
291
|
│ │ └── product.css # template CSS entry → template-product.bundle.css
|
|
292
|
+
│ ├── snippets/ # Liquid snippets copied to dist/snippets/
|
|
293
|
+
│ ├── assets/ # source assets → flattened into dist/assets/
|
|
280
294
|
│ ├── lib/ # shared modules (bundled into entries, not served)
|
|
281
295
|
│ └── css/ # shared CSS (imported by entry CSS)
|
|
282
|
-
├── layout/ # Liquid layouts
|
|
283
|
-
├── templates/ # Liquid templates
|
|
284
|
-
├── snippets/ # Liquid snippets
|
|
285
|
-
├── assets/ # static assets (SVGs, images, fonts)
|
|
286
296
|
└── dist/ # build output (gitignored, uploaded to Tiendu)
|
|
287
297
|
```
|
|
288
298
|
|
|
289
299
|
### How it works
|
|
290
300
|
|
|
291
|
-
1. Source
|
|
292
|
-
2.
|
|
293
|
-
3.
|
|
294
|
-
4. Liquid
|
|
301
|
+
1. Source assets in `src/assets/` are flattened into `dist/assets/` (`payment-methods/visa.svg` becomes `payment-methods___visa.svg`)
|
|
302
|
+
2. Source JS/TS/CSS in `src/` is bundled by esbuild into `dist/assets/`
|
|
303
|
+
3. CSS entries also run through your local PostCSS pipeline when configured
|
|
304
|
+
4. Liquid files are copied from `src/` to `dist/`
|
|
305
|
+
5. `dist/` is what gets uploaded — it looks like a normal Tiendu theme
|
|
306
|
+
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 }}`)
|
|
307
|
+
|
|
308
|
+
### Tailwind v4
|
|
309
|
+
|
|
310
|
+
Built themes can use Tailwind v4 in CSS entry files.
|
|
311
|
+
|
|
312
|
+
Install it in your theme project:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
npm install -D tailwindcss @tailwindcss/postcss postcss
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Then import Tailwind from a CSS entry such as `src/layout/theme.css`:
|
|
319
|
+
|
|
320
|
+
```css
|
|
321
|
+
@import "tailwindcss";
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
You can either:
|
|
325
|
+
|
|
326
|
+
- rely on Tiendu CLI's automatic Tailwind detection when `@tailwindcss/postcss` is installed, or
|
|
327
|
+
- add a local `postcss.config.mjs` / `postcss.config.js` / `postcss.config.cjs` / `postcss.config.json`
|
|
328
|
+
|
|
329
|
+
Example `postcss.config.mjs`:
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
export default {
|
|
333
|
+
plugins: {
|
|
334
|
+
"@tailwindcss/postcss": {},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
```
|
|
295
338
|
|
|
296
339
|
### tiendu.config.json
|
|
297
340
|
|
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/bin/tiendu.mjs
CHANGED
|
@@ -1,138 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
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 [--skip-build] Subir archivos locales al preview activo (ZIP)
|
|
27
|
-
tiendu dev Modo desarrollo: watch + sync automático
|
|
28
|
-
tiendu publish [--skip-build]
|
|
29
|
-
Publicar el preview activo al storefront live
|
|
30
|
-
|
|
31
|
-
tiendu preview create Crear un preview remoto
|
|
32
|
-
tiendu preview list Listar previews de la tienda
|
|
33
|
-
tiendu preview delete Eliminar el preview activo
|
|
34
|
-
tiendu preview open Abrir la URL del preview en el navegador
|
|
35
|
-
|
|
36
|
-
tiendu check-updates Buscar una nueva version del CLI
|
|
37
|
-
tiendu version Mostrar la version actual del CLI
|
|
38
|
-
|
|
39
|
-
tiendu help Mostrar esta ayuda
|
|
40
|
-
|
|
41
|
-
Opciones:
|
|
42
|
-
--help, -h Mostrar esta ayuda
|
|
43
|
-
--version, -v Mostrar la version actual del CLI
|
|
44
|
-
`;
|
|
45
|
-
|
|
46
|
-
const main = async () => {
|
|
47
|
-
const args = process.argv.slice(2);
|
|
48
|
-
const command = args[0];
|
|
49
|
-
const subcommand = args[1];
|
|
50
|
-
const skipBuild = args.includes("--skip-build");
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
command === "version" ||
|
|
54
|
-
command === "--version" ||
|
|
55
|
-
command === "-v"
|
|
56
|
-
) {
|
|
57
|
-
console.log(getCurrentVersion());
|
|
58
|
-
process.exit(0);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
!command ||
|
|
63
|
-
command === "help" ||
|
|
64
|
-
command === "--help" ||
|
|
65
|
-
command === "-h"
|
|
66
|
-
) {
|
|
67
|
-
console.log(HELP.trim());
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (command === "check-updates") {
|
|
72
|
-
await checkForUpdatesNow();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
await checkForUpdates();
|
|
77
|
-
|
|
78
|
-
if (command === "init") {
|
|
79
|
-
await init();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (command === "pull") {
|
|
84
|
-
await pull();
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (command === "push") {
|
|
89
|
-
await push({ skipBuild });
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (command === "dev") {
|
|
94
|
-
await dev();
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (command === "publish") {
|
|
99
|
-
await publish({ skipBuild });
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (command === "preview") {
|
|
104
|
-
if (subcommand === "create") {
|
|
105
|
-
const name = args[2];
|
|
106
|
-
await previewCreate(name);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (subcommand === "list") {
|
|
111
|
-
await previewList();
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (subcommand === "delete") {
|
|
116
|
-
await previewDelete();
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (subcommand === "open") {
|
|
121
|
-
await previewOpen();
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
console.error(`Subcomando desconocido: preview ${subcommand ?? "(vacío)"}`);
|
|
126
|
-
console.log(HELP.trim());
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
console.error(`Comando desconocido: ${command}`);
|
|
131
|
-
console.log(HELP.trim());
|
|
132
|
-
process.exit(1);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
main().catch((error) => {
|
|
136
|
-
console.error(error.message || error);
|
|
137
|
-
process.exit(1);
|
|
138
|
-
});
|
|
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
|
|
|
@@ -77,6 +83,47 @@ export const fetchUserStores = async (apiBaseUrl, apiKey) => {
|
|
|
77
83
|
}
|
|
78
84
|
};
|
|
79
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
|
+
|
|
80
127
|
/**
|
|
81
128
|
* Download the storefront archive (zip) as a buffer.
|
|
82
129
|
*
|
|
@@ -119,76 +166,74 @@ export const downloadStorefrontArchive = async (
|
|
|
119
166
|
};
|
|
120
167
|
|
|
121
168
|
/**
|
|
122
|
-
*
|
|
169
|
+
* Download a preview's archive (zip) as a buffer.
|
|
123
170
|
*
|
|
124
171
|
* @param {string} apiBaseUrl
|
|
125
172
|
* @param {string} apiKey
|
|
126
173
|
* @param {number} storeId
|
|
127
174
|
* @param {string} previewKey
|
|
128
|
-
* @
|
|
129
|
-
* @returns {Promise<{ ok: true } | { ok: false, error: string }>}
|
|
175
|
+
* @returns {Promise<{ ok: true, data: Buffer } | { ok: false, error: string }>}
|
|
130
176
|
*/
|
|
131
|
-
export const
|
|
177
|
+
export const downloadPreviewArchive = async (
|
|
132
178
|
apiBaseUrl,
|
|
133
179
|
apiKey,
|
|
134
180
|
storeId,
|
|
135
181
|
previewKey,
|
|
136
|
-
zipBuffer,
|
|
137
182
|
) => {
|
|
138
183
|
try {
|
|
139
184
|
const response = await apiFetch(
|
|
140
185
|
apiBaseUrl,
|
|
141
186
|
apiKey,
|
|
142
|
-
`/api/admin/stores/${storeId}/theme-previews/${previewKey}/
|
|
143
|
-
{
|
|
144
|
-
method: "POST",
|
|
145
|
-
body: zipBuffer,
|
|
146
|
-
contentType: "application/zip",
|
|
147
|
-
},
|
|
187
|
+
`/api/admin/stores/${storeId}/theme-previews/${previewKey}/download`,
|
|
148
188
|
);
|
|
149
189
|
|
|
190
|
+
const authError = checkAuthErrors(response);
|
|
191
|
+
if (authError) return authError;
|
|
192
|
+
|
|
150
193
|
if (!response.ok) {
|
|
151
194
|
const body = await response.text().catch(() => "");
|
|
152
195
|
return {
|
|
153
196
|
ok: false,
|
|
154
|
-
error: `
|
|
197
|
+
error: `Server error: ${response.status} ${response.statusText}${body ? ` — ${body}` : ""}`,
|
|
155
198
|
};
|
|
156
199
|
}
|
|
157
200
|
|
|
158
|
-
|
|
201
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
202
|
+
return { ok: true, data: Buffer.from(arrayBuffer) };
|
|
159
203
|
} catch (error) {
|
|
160
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
error: `Could not download preview: ${error.message}`,
|
|
207
|
+
};
|
|
161
208
|
}
|
|
162
209
|
};
|
|
163
210
|
|
|
164
211
|
/**
|
|
165
|
-
* Upload a
|
|
212
|
+
* Upload a zip buffer to a preview, replacing its content.
|
|
166
213
|
*
|
|
167
214
|
* @param {string} apiBaseUrl
|
|
168
215
|
* @param {string} apiKey
|
|
169
216
|
* @param {number} storeId
|
|
170
217
|
* @param {string} previewKey
|
|
171
|
-
* @param {
|
|
172
|
-
* @
|
|
173
|
-
* @returns {Promise<{ ok: true } | { ok: false, error: string }>}
|
|
218
|
+
* @param {Buffer} zipBuffer
|
|
219
|
+
* @returns {Promise<{ ok: true } | { ok: false, error: string, retriable?: boolean }>}
|
|
174
220
|
*/
|
|
175
|
-
export const
|
|
221
|
+
export const uploadPreviewZip = async (
|
|
176
222
|
apiBaseUrl,
|
|
177
223
|
apiKey,
|
|
178
224
|
storeId,
|
|
179
225
|
previewKey,
|
|
180
|
-
|
|
181
|
-
content,
|
|
226
|
+
zipBuffer,
|
|
182
227
|
) => {
|
|
183
228
|
try {
|
|
184
|
-
const query = new URLSearchParams({ path: filePath }).toString();
|
|
185
229
|
const response = await apiFetch(
|
|
186
230
|
apiBaseUrl,
|
|
187
231
|
apiKey,
|
|
188
|
-
`/api/
|
|
232
|
+
`/api/admin/stores/${storeId}/theme-previews/${previewKey}/upload`,
|
|
189
233
|
{
|
|
190
|
-
method: "
|
|
191
|
-
body:
|
|
234
|
+
method: "POST",
|
|
235
|
+
body: zipBuffer,
|
|
236
|
+
contentType: "application/zip",
|
|
192
237
|
},
|
|
193
238
|
);
|
|
194
239
|
|
|
@@ -196,7 +241,8 @@ export const uploadPreviewFile = async (
|
|
|
196
241
|
const body = await response.text().catch(() => "");
|
|
197
242
|
return {
|
|
198
243
|
ok: false,
|
|
199
|
-
error: `Error
|
|
244
|
+
error: `Error del servidor: ${response.status}${body ? ` — ${body}` : ""}`,
|
|
245
|
+
retriable: isRetriableStatus(response.status),
|
|
200
246
|
};
|
|
201
247
|
}
|
|
202
248
|
|
|
@@ -204,7 +250,8 @@ export const uploadPreviewFile = async (
|
|
|
204
250
|
} catch (error) {
|
|
205
251
|
return {
|
|
206
252
|
ok: false,
|
|
207
|
-
error: `
|
|
253
|
+
error: `No se pudo subir: ${error.message}`,
|
|
254
|
+
retriable: true,
|
|
208
255
|
};
|
|
209
256
|
}
|
|
210
257
|
};
|
|
@@ -219,7 +266,7 @@ export const uploadPreviewFile = async (
|
|
|
219
266
|
* @param {string} previewKey
|
|
220
267
|
* @param {string} relativePath
|
|
221
268
|
* @param {Buffer} fileBuffer
|
|
222
|
-
* @returns {Promise<{ ok: true } | { ok: false, error: string }>}
|
|
269
|
+
* @returns {Promise<{ ok: true } | { ok: false, error: string, retriable?: boolean }>}
|
|
223
270
|
*/
|
|
224
271
|
export const uploadPreviewFileMultipart = async (
|
|
225
272
|
apiBaseUrl,
|
|
@@ -249,6 +296,7 @@ export const uploadPreviewFileMultipart = async (
|
|
|
249
296
|
Authorization: `Bearer ${apiKey}`,
|
|
250
297
|
},
|
|
251
298
|
body: formData,
|
|
299
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
252
300
|
},
|
|
253
301
|
);
|
|
254
302
|
|
|
@@ -257,6 +305,7 @@ export const uploadPreviewFileMultipart = async (
|
|
|
257
305
|
return {
|
|
258
306
|
ok: false,
|
|
259
307
|
error: `Error subiendo ${relativePath}: ${response.status}${body ? ` — ${body}` : ""}`,
|
|
308
|
+
retriable: isRetriableStatus(response.status),
|
|
260
309
|
};
|
|
261
310
|
}
|
|
262
311
|
|
|
@@ -265,6 +314,7 @@ export const uploadPreviewFileMultipart = async (
|
|
|
265
314
|
return {
|
|
266
315
|
ok: false,
|
|
267
316
|
error: `Error subiendo ${relativePath}: ${error.message}`,
|
|
317
|
+
retriable: true,
|
|
268
318
|
};
|
|
269
319
|
}
|
|
270
320
|
};
|
|
@@ -277,7 +327,7 @@ export const uploadPreviewFileMultipart = async (
|
|
|
277
327
|
* @param {number} storeId
|
|
278
328
|
* @param {string} previewKey
|
|
279
329
|
* @param {string} filePath
|
|
280
|
-
* @returns {Promise<{ ok: true } | { ok: false, error: string }>}
|
|
330
|
+
* @returns {Promise<{ ok: true } | { ok: false, error: string, retriable?: boolean }>}
|
|
281
331
|
*/
|
|
282
332
|
export const deletePreviewFile = async (
|
|
283
333
|
apiBaseUrl,
|
|
@@ -302,6 +352,7 @@ export const deletePreviewFile = async (
|
|
|
302
352
|
return {
|
|
303
353
|
ok: false,
|
|
304
354
|
error: `Error eliminando ${filePath}: ${response.status}${body ? ` — ${body}` : ""}`,
|
|
355
|
+
retriable: isRetriableStatus(response.status),
|
|
305
356
|
};
|
|
306
357
|
}
|
|
307
358
|
|
|
@@ -310,6 +361,7 @@ export const deletePreviewFile = async (
|
|
|
310
361
|
return {
|
|
311
362
|
ok: false,
|
|
312
363
|
error: `Error eliminando ${filePath}: ${error.message}`,
|
|
364
|
+
retriable: true,
|
|
313
365
|
};
|
|
314
366
|
}
|
|
315
367
|
};
|
package/lib/archive.mjs
ADDED
|
@@ -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
|
+
};
|