tiendu 0.7.0 → 0.8.1

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
@@ -138,8 +138,11 @@ Builds or stages the current theme into its deployable output directory (`dist/`
138
138
 
139
139
  ```bash
140
140
  tiendu build
141
+ tiendu build --skip-instances
141
142
  ```
142
143
 
144
+ - Use `--skip-instances` to omit template JSON, section group JSON, and `config/settings_data.json` from `dist/`. This is useful when you want to preserve the existing page/section instances on the preview.
145
+
143
146
  The build:
144
147
 
145
148
  1. Copies theme files from `src/layout/`, `src/templates/`, `src/sections/`, `src/blocks/`, `src/snippets/`, and `src/config/` to `dist/`
@@ -167,8 +170,10 @@ The main development command.
167
170
 
168
171
  ```bash
169
172
  tiendu dev
173
+ tiendu dev --skip-instances
170
174
  ```
171
175
 
176
+ - Use `--skip-instances` to sync everything except template JSON, section group JSON, and `config/settings_data.json`. Existing instances on the preview are preserved.
172
177
  - Prints the preview URL on start
173
178
  - Re-syncs the full local theme to the preview on startup
174
179
  - Syncs file creates, edits and deletes
@@ -190,8 +195,11 @@ Zips and uploads `dist/` to the active preview, replacing its content entirely.
190
195
  tiendu push
191
196
  tiendu push --skip-build
192
197
  tiendu push --skip-build --non-interactive
198
+ tiendu push --skip-instances
193
199
  ```
194
200
 
201
+ - Use `--skip-instances` to upload everything except template JSON, section group JSON, and `config/settings_data.json`. Existing instances on the preview are preserved.
202
+
195
203
  ---
196
204
 
197
205
  ### `tiendu publish`
@@ -205,8 +213,11 @@ Publishes the active preview to the live storefront. Visitors will see the new t
205
213
  tiendu publish
206
214
  tiendu publish --skip-build
207
215
  tiendu publish --skip-build --non-interactive
216
+ tiendu publish --skip-instances
208
217
  ```
209
218
 
219
+ - Use `--skip-instances` to publish everything except template JSON, section group JSON, and `config/settings_data.json`. Existing instances on the preview are preserved.
220
+
210
221
  In non-interactive mode, the publish confirmation is skipped.
211
222
 
212
223
  ---
package/bin/tiendu.js CHANGED
@@ -55,6 +55,7 @@ Global options:
55
55
  --non-interactive Disable prompts, print plain text output, and skip confirmations
56
56
  --dir <path> Create the project inside a new directory during init
57
57
  --skip-build Reuse the existing dist/ output for push or publish
58
+ --skip-instances Skip template/section group JSON and settings_data.json (preserves existing instances on the preview)
58
59
  --help, -h Show this help message
59
60
  --version, -v Show the current CLI version
60
61
 
@@ -132,6 +133,7 @@ const main = async () => {
132
133
  const command = positionals[0];
133
134
  const subcommand = positionals[1];
134
135
  const skipBuild = flags.has("--skip-build");
136
+ const skipInstances = flags.has("--skip-instances");
135
137
  const nonInteractive =
136
138
  flags.has("--non-interactive") || !process.stdin.isTTY || !process.stdout.isTTY;
137
139
 
@@ -190,23 +192,23 @@ const main = async () => {
190
192
  }
191
193
 
192
194
  if (command === "build") {
193
- const result = await build();
195
+ const result = await build({ skipInstances });
194
196
  if (!result.ok) process.exit(1);
195
197
  return;
196
198
  }
197
199
 
198
200
  if (command === "push") {
199
- await push({ skipBuild, previewKey: positionals[1] });
201
+ await push({ skipBuild, previewKey: positionals[1], skipInstances });
200
202
  return;
201
203
  }
202
204
 
203
205
  if (command === "dev") {
204
- await dev();
206
+ await dev({ skipInstances });
205
207
  return;
206
208
  }
207
209
 
208
210
  if (command === "publish") {
209
- await publish({ skipBuild, previewKey: positionals[1] });
211
+ await publish({ skipBuild, previewKey: positionals[1], skipInstances });
210
212
  return;
211
213
  }
212
214
 
package/lib/api.mjs CHANGED
@@ -219,23 +219,25 @@ export const downloadPreviewArchive = async (
219
219
  * @returns {Promise<{ ok: true } | { ok: false, error: string, retriable?: boolean }>}
220
220
  */
221
221
  export const uploadPreviewZip = async (
222
- apiBaseUrl,
223
- apiKey,
224
- storeId,
225
- previewKey,
226
- zipBuffer,
222
+ apiBaseUrl,
223
+ apiKey,
224
+ storeId,
225
+ previewKey,
226
+ zipBuffer,
227
+ preserveInstances = false,
227
228
  ) => {
228
- try {
229
- const response = await apiFetch(
230
- apiBaseUrl,
231
- apiKey,
232
- `/api/admin/stores/${storeId}/theme-previews/${previewKey}/upload`,
233
- {
234
- method: "POST",
235
- body: zipBuffer,
236
- contentType: "application/zip",
237
- },
238
- );
229
+ try {
230
+ const query = preserveInstances ? "?preserveInstances=true" : "";
231
+ const response = await apiFetch(
232
+ apiBaseUrl,
233
+ apiKey,
234
+ `/api/admin/stores/${storeId}/theme-previews/${previewKey}/upload${query}`,
235
+ {
236
+ method: "POST",
237
+ body: zipBuffer,
238
+ contentType: "application/zip",
239
+ },
240
+ );
239
241
 
240
242
  if (!response.ok) {
241
243
  const body = await response.text().catch(() => "");
package/lib/archive.mjs CHANGED
@@ -13,7 +13,7 @@ export const listAllFiles = async (rootDir) => listFilesRecursive(rootDir);
13
13
  * @param {string} rootDir
14
14
  * @returns {Promise<Buffer>}
15
15
  */
16
- export const createZipFromDirectory = async (rootDir) => {
16
+ export const createZipFromDirectory = async (rootDir, shouldInclude) => {
17
17
  const absoluteFiles = await listAllFiles(rootDir);
18
18
  /** @type {Record<string, Uint8Array>} */
19
19
  const entries = {};
@@ -23,6 +23,7 @@ export const createZipFromDirectory = async (rootDir) => {
23
23
  .relative(rootDir, absolutePath)
24
24
  .split(path.sep)
25
25
  .join("/");
26
+ if (shouldInclude && !shouldInclude(relativePath)) continue;
26
27
  entries[relativePath] = new Uint8Array(await readFile(absolutePath));
27
28
  }
28
29
 
package/lib/build.mjs CHANGED
@@ -36,6 +36,17 @@ const ENTRY_SOURCE_EXTENSIONS = new Set([".js", ".ts", ".css"]);
36
36
  const NESTED_ASSET_PATH_PATTERN =
37
37
  /\/assets\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._/-]+)+)([?#][A-Za-z0-9=&._-]+)?/g;
38
38
 
39
+ const INSTANCE_FILE_PATTERNS = [
40
+ /^templates\/[^/]+\.json$/,
41
+ /^sections\/[^/]+\.json$/,
42
+ /^config\/settings_data\.json$/,
43
+ ];
44
+
45
+ export const isInstanceFile = (relativePath) => {
46
+ const normalized = relativePath.split(path.sep).join("/");
47
+ return INSTANCE_FILE_PATTERNS.some((pattern) => pattern.test(normalized));
48
+ };
49
+
39
50
  /**
40
51
  * Discover optional JS/TS and CSS entry points from src/layout/templates or layout/templates.
41
52
  * Returns separate maps for JS and CSS to avoid key collisions.
@@ -118,9 +129,11 @@ const rewriteDirectAssetPaths = (source, knownAssetLogicalPaths) =>
118
129
  return flattened ? `/assets/${flattened}${suffix}` : match;
119
130
  });
120
131
 
121
- const shouldCopyThemeSourceFile = (sourceRelativePath) => {
132
+ const shouldCopyThemeSourceFile = (sourceRelativePath, outputRelativePath, skipInstances = false) => {
122
133
  const extension = path.extname(sourceRelativePath).toLowerCase();
123
- return !ENTRY_SOURCE_EXTENSIONS.has(extension);
134
+ if (ENTRY_SOURCE_EXTENSIONS.has(extension)) return false;
135
+ if (skipInstances && isInstanceFile(outputRelativePath)) return false;
136
+ return true;
124
137
  };
125
138
 
126
139
  const copyThemeSourceFile = async (
@@ -152,6 +165,7 @@ const copyThemeFiles = async (
152
165
  distDir,
153
166
  themeSourceDirs,
154
167
  knownAssetLogicalPaths,
168
+ skipInstances = false,
155
169
  ) => {
156
170
  for (const sourceDir of themeSourceDirs) {
157
171
  const absoluteSourceDir = path.join(rootDir, sourceDir.sourceRelativeDir);
@@ -167,7 +181,7 @@ const copyThemeFiles = async (
167
181
  sourceDir.outputRelativeDir,
168
182
  nestedRelativePath,
169
183
  );
170
- if (!shouldCopyThemeSourceFile(sourceRelativePath)) continue;
184
+ if (!shouldCopyThemeSourceFile(sourceRelativePath, outputRelativePath, skipInstances)) continue;
171
185
  await copyThemeSourceFile(
172
186
  rootDir,
173
187
  distDir,
@@ -247,7 +261,7 @@ const runEntryBuilds = async (jsBuildOptions, cssBuildOptions) => {
247
261
  * @param {{ watch?: boolean }} options
248
262
  * @returns {Promise<{ ok: boolean, cleanup?: () => Promise<void> }>}
249
263
  */
250
- export const build = async ({ watch: watchMode = false } = {}) => {
264
+ export const build = async ({ watch: watchMode = false, skipInstances = false } = {}) => {
251
265
  const rootDir = process.cwd();
252
266
  const distDir = path.join(rootDir, "dist");
253
267
 
@@ -310,6 +324,7 @@ export const build = async ({ watch: watchMode = false } = {}) => {
310
324
  distDir,
311
325
  themeSourceDirs,
312
326
  knownAssetLogicalPaths,
327
+ skipInstances,
313
328
  );
314
329
 
315
330
  if (cssCount > 0 && pipeline.postcss) {
@@ -497,7 +512,7 @@ export const build = async ({ watch: watchMode = false } = {}) => {
497
512
  const timer = setTimeout(async () => {
498
513
  debounceMap.delete(sourceRelativePath);
499
514
  try {
500
- if (!shouldCopyThemeSourceFile(sourceRelativePath)) {
515
+ if (!shouldCopyThemeSourceFile(sourceRelativePath, outputRelativePath, skipInstances)) {
501
516
  return;
502
517
  }
503
518
 
package/lib/dev.mjs CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  deletePreviewFile,
11
11
  uploadPreviewFileMultipart,
12
12
  } from "./api.mjs";
13
- import { build } from "./build.mjs";
13
+ import { build, isInstanceFile } from "./build.mjs";
14
14
  import { isDotfile } from "./fs-utils.mjs";
15
15
  import { startLocalPreviewServer } from "./local-preview.mjs";
16
16
  import { pushPreparedDirectoryToPreview } from "./push.mjs";
@@ -115,7 +115,7 @@ const deleteFileWithRetries = (
115
115
  },
116
116
  );
117
117
 
118
- export const dev = async () => {
118
+ export const dev = async ({ skipInstances = false } = {}) => {
119
119
  const { config, credentials } = await loadConfigOrFail();
120
120
  const { apiBaseUrl, storeId } = config;
121
121
  const { apiKey } = credentials;
@@ -123,7 +123,7 @@ export const dev = async () => {
123
123
  let buildCleanup = null;
124
124
  let localPreviewServer = null;
125
125
 
126
- const buildResult = await build({ watch: true });
126
+ const buildResult = await build({ watch: true, skipInstances });
127
127
  if (!buildResult.ok) {
128
128
  ui.log.error("Initial build failed. Fix errors and try again.");
129
129
  process.exit(1);
@@ -161,6 +161,7 @@ export const dev = async () => {
161
161
  compressMessage: "Compressing files...",
162
162
  retryMessage: (result, nextAttempt) =>
163
163
  `Initial push failed. Retrying ${nextAttempt}/${RETRY_ATTEMPTS}... ${result.error}`,
164
+ skipInstances,
164
165
  });
165
166
 
166
167
  if (!uploadResult.ok) {
@@ -288,6 +289,7 @@ export const dev = async () => {
288
289
  if (shouldIgnoreWatchedPath(filename, true)) return;
289
290
 
290
291
  const relativePath = filename.split(path.sep).join("/");
292
+ if (skipInstances && isInstanceFile(relativePath)) return;
291
293
  queueSync(relativePath);
292
294
  });
293
295
 
package/lib/publish.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  import { push } from "./push.mjs";
8
8
  import * as ui from "./ui.mjs";
9
9
 
10
- export const publish = async ({ skipBuild = false, previewKey: previewKeyArg } = {}) => {
10
+ export const publish = async ({ skipBuild = false, previewKey: previewKeyArg, skipInstances = false } = {}) => {
11
11
  const { config, credentials } = await loadConfigOrFail();
12
12
 
13
13
  // Resolve preview key: explicit arg > interactive picker
@@ -42,7 +42,7 @@ export const publish = async ({ skipBuild = false, previewKey: previewKeyArg } =
42
42
  ? "Syncing existing dist/ output to the preview before publishing..."
43
43
  : "Building and syncing the latest dist/ output before publishing...",
44
44
  );
45
- await push({ skipBuild, previewKey });
45
+ await push({ skipBuild, previewKey, skipInstances });
46
46
 
47
47
  const spinner = ui.spinner();
48
48
  spinner.start("Publishing preview to live storefront...");
package/lib/push.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getDistDir, loadConfigOrFail } from "./config.mjs";
2
2
  import { uploadPreviewZip } from "./api.mjs";
3
3
  import { createZipFromDirectory } from "./archive.mjs";
4
- import { build } from "./build.mjs";
4
+ import { build, isInstanceFile } from "./build.mjs";
5
5
  import {
6
6
  fetchPreviewDetails,
7
7
  resolvePreviewKeyInteractively,
@@ -26,16 +26,21 @@ export const pushPreparedDirectoryToPreview = async ({
26
26
  compressMessage = "Compressing files...",
27
27
  uploadMessage,
28
28
  retryMessage,
29
+ skipInstances = false,
29
30
  }) => {
30
31
  spinner.message(compressMessage);
31
32
 
32
- const zipBuffer = await createZipFromDirectory(rootDir);
33
+ const shouldInclude = skipInstances
34
+ ? (relativePath) => !isInstanceFile(relativePath)
35
+ : undefined;
36
+
37
+ const zipBuffer = await createZipFromDirectory(rootDir, shouldInclude);
33
38
  spinner.message(
34
39
  uploadMessage ?? `Uploading to preview ${previewKey} (${formatBytes(zipBuffer.length)})...`,
35
40
  );
36
41
 
37
42
  return retryAsync(
38
- () => uploadPreviewZip(apiBaseUrl, apiKey, storeId, previewKey, zipBuffer),
43
+ () => uploadPreviewZip(apiBaseUrl, apiKey, storeId, previewKey, zipBuffer, skipInstances),
39
44
  {
40
45
  attempts: 3,
41
46
  shouldRetry: (uploadResult) => !uploadResult.ok && Boolean(uploadResult.retriable),
@@ -49,11 +54,11 @@ export const pushPreparedDirectoryToPreview = async ({
49
54
  );
50
55
  };
51
56
 
52
- export const push = async ({ skipBuild = false, previewKey: previewKeyArg } = {}) => {
57
+ export const push = async ({ skipBuild = false, previewKey: previewKeyArg, skipInstances = false } = {}) => {
53
58
  const { config, credentials } = await loadConfigOrFail();
54
59
 
55
60
  if (!skipBuild) {
56
- const result = await build();
61
+ const result = await build({ skipInstances });
57
62
  if (!result.ok) {
58
63
  process.exit(1);
59
64
  }
@@ -84,6 +89,7 @@ export const push = async ({ skipBuild = false, previewKey: previewKeyArg } = {}
84
89
  previewKey,
85
90
  rootDir,
86
91
  spinner,
92
+ skipInstances,
87
93
  });
88
94
 
89
95
  if (!result.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiendu",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "CLI para desarrollar y publicar temas en Tiendu",
5
5
  "type": "module",
6
6
  "bin": {