tiendu 0.6.1 → 0.8.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/build.mjs CHANGED
@@ -10,8 +10,7 @@ import {
10
10
  } from "node:fs/promises";
11
11
  import path from "node:path";
12
12
  import * as esbuild from "esbuild";
13
- import * as p from "@clack/prompts";
14
- import { readThemeConfig } from "./config.mjs";
13
+ import { getThemePipelineConfig, readThemeConfig } from "./config.mjs";
15
14
  import {
16
15
  flattenAssetLogicalPath,
17
16
  getAssetImportFilter,
@@ -22,17 +21,37 @@ import {
22
21
  } from "./assets.mjs";
23
22
  import { listFilesRecursive } from "./fs-utils.mjs";
24
23
  import { createCssPostCssPlugin } from "./postcss.mjs";
25
-
26
- const THEME_SOURCE_OUTPUT_DIRS = ["layout", "templates", "sections", "snippets", "config"];
24
+ import * as ui from "./ui.mjs";
25
+
26
+ const THEME_SOURCE_OUTPUT_DIRS = [
27
+ "layout",
28
+ "templates",
29
+ "sections",
30
+ "blocks",
31
+ "snippets",
32
+ "config",
33
+ ];
27
34
  const LIQUID_LIKE_EXTENSIONS = new Set([".liquid", ".html", ".htm"]);
28
35
  const ENTRY_SOURCE_EXTENSIONS = new Set([".js", ".ts", ".css"]);
29
- const NESTED_ASSET_PATH_PATTERN = /\/assets\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._/-]+)+)([?#][A-Za-z0-9=&._-]+)?/g;
36
+ const NESTED_ASSET_PATH_PATTERN =
37
+ /\/assets\/([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._/-]+)+)([?#][A-Za-z0-9=&._-]+)?/g;
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
+ };
30
49
 
31
50
  /**
32
- * Discover JS/TS and CSS entry points from src/layout and src/templates.
51
+ * Discover optional JS/TS and CSS entry points from src/layout/templates or layout/templates.
33
52
  * Returns separate maps for JS and CSS to avoid key collisions.
34
53
  */
35
- const discoverEntryPoints = async (srcDir) => {
54
+ const discoverEntryPoints = async (rootDir) => {
36
55
  const jsEntries = {};
37
56
  const cssEntries = {};
38
57
 
@@ -40,24 +59,29 @@ const discoverEntryPoints = async (srcDir) => {
40
59
  ["layout", "layout"],
41
60
  ["templates", "template"],
42
61
  ]) {
43
- const dirPath = path.join(srcDir, dir);
44
- let files;
45
- try {
46
- files = await readdir(dirPath);
47
- } catch {
48
- continue;
49
- }
50
- for (const file of files) {
51
- const ext = path.extname(file);
52
- if (![".js", ".ts", ".css"].includes(ext)) continue;
53
- const name = path.basename(file, ext);
54
- const key = `${prefix}-${name}.bundle`;
55
- const fullPath = path.join(dirPath, file);
56
- if (ext === ".css") {
57
- cssEntries[key] = fullPath;
58
- } else {
59
- jsEntries[key] = fullPath;
62
+ const sourceCandidates = [path.join(rootDir, "src", dir), path.join(rootDir, dir)];
63
+
64
+ for (const dirPath of sourceCandidates) {
65
+ let files;
66
+ try {
67
+ files = await readdir(dirPath);
68
+ } catch {
69
+ continue;
70
+ }
71
+ for (const file of files) {
72
+ const ext = path.extname(file);
73
+ if (![".js", ".ts", ".css"].includes(ext)) continue;
74
+ const name = path.basename(file, ext);
75
+ const key = `${prefix}-${name}.bundle`;
76
+ const fullPath = path.join(dirPath, file);
77
+ if (ext === ".css") {
78
+ cssEntries[key] = fullPath;
79
+ } else {
80
+ jsEntries[key] = fullPath;
81
+ }
60
82
  }
83
+
84
+ break;
61
85
  }
62
86
  }
63
87
 
@@ -105,9 +129,11 @@ const rewriteDirectAssetPaths = (source, knownAssetLogicalPaths) =>
105
129
  return flattened ? `/assets/${flattened}${suffix}` : match;
106
130
  });
107
131
 
108
- const shouldCopyThemeSourceFile = (sourceRelativePath) => {
132
+ const shouldCopyThemeSourceFile = (sourceRelativePath, outputRelativePath, skipInstances = false) => {
109
133
  const extension = path.extname(sourceRelativePath).toLowerCase();
110
- 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;
111
137
  };
112
138
 
113
139
  const copyThemeSourceFile = async (
@@ -123,23 +149,39 @@ const copyThemeSourceFile = async (
123
149
 
124
150
  if (LIQUID_LIKE_EXTENSIONS.has(path.extname(src).toLowerCase())) {
125
151
  const source = await readFile(src, "utf-8");
126
- await writeFile(dest, rewriteDirectAssetPaths(source, knownAssetLogicalPaths), "utf-8");
152
+ await writeFile(
153
+ dest,
154
+ rewriteDirectAssetPaths(source, knownAssetLogicalPaths),
155
+ "utf-8",
156
+ );
127
157
  return;
128
158
  }
129
159
 
130
160
  await copyFile(src, dest);
131
161
  };
132
162
 
133
- const copyThemeFiles = async (rootDir, distDir, themeSourceDirs, knownAssetLogicalPaths) => {
163
+ const copyThemeFiles = async (
164
+ rootDir,
165
+ distDir,
166
+ themeSourceDirs,
167
+ knownAssetLogicalPaths,
168
+ skipInstances = false,
169
+ ) => {
134
170
  for (const sourceDir of themeSourceDirs) {
135
171
  const absoluteSourceDir = path.join(rootDir, sourceDir.sourceRelativeDir);
136
172
  const absoluteFiles = await listFilesRecursive(absoluteSourceDir);
137
173
 
138
174
  for (const absolutePath of absoluteFiles) {
139
175
  const nestedRelativePath = path.relative(absoluteSourceDir, absolutePath);
140
- const sourceRelativePath = path.join(sourceDir.sourceRelativeDir, nestedRelativePath);
141
- const outputRelativePath = path.join(sourceDir.outputRelativeDir, nestedRelativePath);
142
- if (!shouldCopyThemeSourceFile(sourceRelativePath)) continue;
176
+ const sourceRelativePath = path.join(
177
+ sourceDir.sourceRelativeDir,
178
+ nestedRelativePath,
179
+ );
180
+ const outputRelativePath = path.join(
181
+ sourceDir.outputRelativeDir,
182
+ nestedRelativePath,
183
+ );
184
+ if (!shouldCopyThemeSourceFile(sourceRelativePath, outputRelativePath, skipInstances)) continue;
143
185
  await copyThemeSourceFile(
144
186
  rootDir,
145
187
  distDir,
@@ -155,11 +197,17 @@ const shouldTriggerTailwindCssRebuild = (relativePath) => {
155
197
  const normalizedPath = relativePath.split(path.sep).join("/");
156
198
  const extension = path.extname(normalizedPath).toLowerCase();
157
199
 
158
- if (!["layout/", "templates/", "sections/", "snippets/"].some((prefix) => normalizedPath.startsWith(prefix))) {
200
+ if (
201
+ !["layout/", "templates/", "sections/", "blocks/", "snippets/"].some(
202
+ (prefix) => normalizedPath.startsWith(prefix),
203
+ )
204
+ ) {
159
205
  return false;
160
206
  }
161
207
 
162
- return [".liquid", ".html", ".htm", ".js", ".ts", ".json", ".md"].includes(extension);
208
+ return [".liquid", ".html", ".htm", ".js", ".ts", ".json", ".md"].includes(
209
+ extension,
210
+ );
163
211
  };
164
212
 
165
213
  const createSourceAssetImportPlugin = (rootDir, sourceDirs) => ({
@@ -173,7 +221,11 @@ const createSourceAssetImportPlugin = (rootDir, sourceDirs) => ({
173
221
  const candidatePath = path.isAbsolute(args.path)
174
222
  ? args.path
175
223
  : path.resolve(args.resolveDir, args.path);
176
- const assetInfo = await getAssetSourceInfo(rootDir, candidatePath, sourceDirs);
224
+ const assetInfo = await getAssetSourceInfo(
225
+ rootDir,
226
+ candidatePath,
227
+ sourceDirs,
228
+ );
177
229
  if (!assetInfo) return null;
178
230
 
179
231
  return {
@@ -183,14 +235,17 @@ const createSourceAssetImportPlugin = (rootDir, sourceDirs) => ({
183
235
  };
184
236
  });
185
237
 
186
- build.onLoad({ filter: /.*/, namespace: "tiendu-source-asset" }, async (args) => {
187
- const assetInfo = args.pluginData;
188
- return {
189
- contents: `export default ${JSON.stringify(`/${assetInfo.outputRelativePath}`)};`,
190
- loader: "js",
191
- watchFiles: [assetInfo.absolutePath],
192
- };
193
- });
238
+ build.onLoad(
239
+ { filter: /.*/, namespace: "tiendu-source-asset" },
240
+ async (args) => {
241
+ const assetInfo = args.pluginData;
242
+ return {
243
+ contents: `export default ${JSON.stringify(`/${assetInfo.outputRelativePath}`)};`,
244
+ loader: "js",
245
+ watchFiles: [assetInfo.absolutePath],
246
+ };
247
+ },
248
+ );
194
249
  },
195
250
  });
196
251
 
@@ -206,14 +261,17 @@ const runEntryBuilds = async (jsBuildOptions, cssBuildOptions) => {
206
261
  * @param {{ watch?: boolean }} options
207
262
  * @returns {Promise<{ ok: boolean, cleanup?: () => Promise<void> }>}
208
263
  */
209
- export const build = async ({ watch: watchMode = false } = {}) => {
264
+ export const build = async ({ watch: watchMode = false, skipInstances = false } = {}) => {
210
265
  const rootDir = process.cwd();
211
- const srcDir = path.join(rootDir, "src");
212
266
  const distDir = path.join(rootDir, "dist");
213
267
 
214
268
  const themeConfig = await readThemeConfig();
215
- if (!themeConfig) {
216
- p.log.error("No tiendu.config.json found. This is not a built theme.");
269
+ const pipeline = getThemePipelineConfig(themeConfig);
270
+
271
+ if (pipeline.postcss && !pipeline.compileStyles) {
272
+ ui.log.error(
273
+ "Invalid tiendu.config.json: pipeline.postcss requires pipeline.compileStyles to be enabled.",
274
+ );
217
275
  return { ok: false };
218
276
  }
219
277
 
@@ -222,10 +280,14 @@ export const build = async ({ watch: watchMode = false } = {}) => {
222
280
  await mkdir(distDir, { recursive: true });
223
281
 
224
282
  const themeSourceDirs = await getThemeSourceDirs(rootDir);
225
- const staticAssetSourceDirs = await getStaticAssetSourceDirs(rootDir, { refresh: true });
283
+ const staticAssetSourceDirs = await getStaticAssetSourceDirs(rootDir, {
284
+ refresh: true,
285
+ });
226
286
 
227
287
  // Discover entry points (JS and CSS separately to avoid key collisions)
228
- const { jsEntries, cssEntries } = await discoverEntryPoints(srcDir);
288
+ const discoveredEntries = await discoverEntryPoints(rootDir);
289
+ const jsEntries = pipeline.compileScripts ? discoveredEntries.jsEntries : {};
290
+ const cssEntries = pipeline.compileStyles ? discoveredEntries.cssEntries : {};
229
291
  const jsCount = Object.keys(jsEntries).length;
230
292
  const cssCount = Object.keys(cssEntries).length;
231
293
  const entryCount = jsCount + cssCount;
@@ -236,7 +298,9 @@ export const build = async ({ watch: watchMode = false } = {}) => {
236
298
  let staticAssetOwners = new Map();
237
299
  const knownAssetLogicalPaths = new Set();
238
300
  const cssPlugins = [];
239
- const jsPlugins = [createSourceAssetImportPlugin(rootDir, staticAssetSourceDirs)];
301
+ const jsPlugins = pipeline.compileScripts
302
+ ? [createSourceAssetImportPlugin(rootDir, staticAssetSourceDirs)]
303
+ : [];
240
304
 
241
305
  try {
242
306
  staticAssetOwners = await syncStaticAssets(
@@ -246,7 +310,7 @@ export const build = async ({ watch: watchMode = false } = {}) => {
246
310
  staticAssetSourceDirs,
247
311
  );
248
312
  } catch (error) {
249
- p.log.error(`Static asset build failed: ${error.message}`);
313
+ ui.log.error(`Static asset build failed: ${error.message}`);
250
314
  return { ok: false };
251
315
  }
252
316
 
@@ -255,42 +319,83 @@ export const build = async ({ watch: watchMode = false } = {}) => {
255
319
  }
256
320
 
257
321
  // Copy theme files after asset paths are known
258
- await copyThemeFiles(rootDir, distDir, themeSourceDirs, knownAssetLogicalPaths);
322
+ await copyThemeFiles(
323
+ rootDir,
324
+ distDir,
325
+ themeSourceDirs,
326
+ knownAssetLogicalPaths,
327
+ skipInstances,
328
+ );
259
329
 
260
- if (cssCount > 0) {
330
+ if (cssCount > 0 && pipeline.postcss) {
261
331
  try {
262
332
  cssPlugins.push(
263
- await createCssPostCssPlugin(rootDir, { sourceDirs: staticAssetSourceDirs }),
333
+ await createCssPostCssPlugin(rootDir, {
334
+ sourceDirs: staticAssetSourceDirs,
335
+ }),
264
336
  );
265
337
  } catch (error) {
266
- p.log.error(`CSS pipeline failed to initialize: ${error.message}`);
338
+ ui.log.error(`CSS pipeline failed to initialize: ${error.message}`);
267
339
  return { ok: false };
268
340
  }
269
341
  }
270
342
 
271
343
  if (entryCount === 0) {
272
- p.log.warn("No entry points found in src/layout or src/templates.");
273
- return { ok: true };
344
+ const hasThemeFiles =
345
+ themeSourceDirs.length > 0 || staticAssetSourceDirs.length > 0;
346
+
347
+ if (!hasThemeFiles) {
348
+ ui.log.error("No theme source files or entry points found.");
349
+ return { ok: false };
350
+ }
351
+
352
+ if (!watchMode) {
353
+ ui.log.success("Prepared theme files to dist/.");
354
+ return { ok: true };
355
+ }
274
356
  }
275
357
 
276
358
  const outdir = path.join(distDir, "assets");
277
- const jsBuildOptions = jsCount > 0
278
- ? { entryPoints: jsEntries, bundle: true, format: "esm", target: "es2020", outdir, logLevel: "warning", write: true, resolveExtensions: [".ts", ".tsx", ".js", ".jsx", ".json"], plugins: jsPlugins }
279
- : null;
280
- const cssBuildOptions = cssCount > 0
281
- ? { entryPoints: cssEntries, bundle: true, outdir, logLevel: "warning", write: true, plugins: cssPlugins }
282
- : null;
359
+ const jsBuildOptions =
360
+ jsCount > 0
361
+ ? {
362
+ entryPoints: jsEntries,
363
+ bundle: true,
364
+ format: "esm",
365
+ target: "es2020",
366
+ outdir,
367
+ logLevel: "warning",
368
+ write: true,
369
+ resolveExtensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
370
+ plugins: jsPlugins,
371
+ }
372
+ : null;
373
+ const cssBuildOptions =
374
+ cssCount > 0
375
+ ? {
376
+ entryPoints: cssEntries,
377
+ bundle: true,
378
+ outdir,
379
+ logLevel: "warning",
380
+ write: true,
381
+ plugins: cssPlugins,
382
+ }
383
+ : null;
283
384
 
284
385
  if (!watchMode) {
285
386
  // One-shot build
286
387
  try {
287
388
  await runEntryBuilds(jsBuildOptions, cssBuildOptions);
288
- p.log.success(
289
- `Built ${entryCount} entry point${entryCount === 1 ? "" : "s"} to dist/`,
290
- );
389
+ if (entryCount === 0) {
390
+ ui.log.success("Prepared theme files to dist/.");
391
+ } else {
392
+ ui.log.success(
393
+ `Built ${entryCount} entry point${entryCount === 1 ? "" : "s"} to dist/`,
394
+ );
395
+ }
291
396
  return { ok: true };
292
397
  } catch (error) {
293
- p.log.error(`Build failed: ${error.message}`);
398
+ ui.log.error(`Build failed: ${error.message}`);
294
399
  return { ok: false };
295
400
  }
296
401
  }
@@ -312,14 +417,18 @@ export const build = async ({ watch: watchMode = false } = {}) => {
312
417
  contexts.push(cssCtx);
313
418
  }
314
419
  } catch (error) {
315
- p.log.error(`Build failed: ${error.message}`);
420
+ ui.log.error(`Build failed: ${error.message}`);
316
421
  for (const ctx of contexts) await ctx.dispose();
317
422
  return { ok: false };
318
423
  }
319
424
 
320
- p.log.success(
321
- `Built ${entryCount} entry point${entryCount === 1 ? "" : "s"}. Watching for changes...`,
322
- );
425
+ if (entryCount === 0) {
426
+ ui.log.success("Prepared theme files. Watching for changes...");
427
+ } else {
428
+ ui.log.success(
429
+ `Built ${entryCount} entry point${entryCount === 1 ? "" : "s"}. Watching for changes...`,
430
+ );
431
+ }
323
432
 
324
433
  // Watch theme directories for Liquid/static asset changes and copy to dist
325
434
  const themeWatchers = [];
@@ -340,9 +449,9 @@ export const build = async ({ watch: watchMode = false } = {}) => {
340
449
 
341
450
  try {
342
451
  await cssCtx.rebuild();
343
- console.log("CSS bundles");
452
+ console.log("CSS bundles updated");
344
453
  } catch (error) {
345
- p.log.warn(`Error rebuilding CSS: ${error.message}`);
454
+ ui.log.warn(`Error rebuilding CSS: ${error.message}`);
346
455
  } finally {
347
456
  cssRebuildInFlight = false;
348
457
  if (cssRebuildQueued) {
@@ -380,83 +489,98 @@ export const build = async ({ watch: watchMode = false } = {}) => {
380
489
  knownAssetLogicalPaths.delete(result.logicalPath);
381
490
  }
382
491
 
383
- console.log(`${result.type === "delete" ? "✕" : "⟳"} ${result.outputRelativePath}`);
492
+ console.log(
493
+ `${result.type === "delete" ? "DELETE" : "UPDATE"} ${result.outputRelativePath}`,
494
+ );
384
495
  };
385
496
 
386
497
  for (const sourceDir of themeSourceDirs) {
387
498
  const dirPath = path.join(rootDir, sourceDir.sourceRelativeDir);
388
499
 
389
- const watcher = watch(dirPath, { recursive: true }, (eventType, filename) => {
390
- if (!filename) return;
391
- const normalizedFilename = filename.split(path.sep).join("/");
392
- const sourceRelativePath = `${sourceDir.sourceRelativeDir}/${normalizedFilename}`;
393
- const outputRelativePath = `${sourceDir.outputRelativeDir}/${normalizedFilename}`;
394
-
395
- const existing = debounceMap.get(sourceRelativePath);
396
- if (existing) clearTimeout(existing);
397
-
398
- const timer = setTimeout(async () => {
399
- debounceMap.delete(sourceRelativePath);
400
- try {
401
- if (!shouldCopyThemeSourceFile(sourceRelativePath)) {
402
- return;
403
- }
404
-
405
- const fileStat = await stat(path.join(rootDir, sourceRelativePath)).catch(
406
- () => null,
407
- );
408
- if (fileStat && fileStat.isFile()) {
409
- await copyThemeSourceFile(
410
- rootDir,
411
- distDir,
412
- sourceRelativePath,
413
- outputRelativePath,
414
- knownAssetLogicalPaths,
500
+ const watcher = watch(
501
+ dirPath,
502
+ { recursive: true },
503
+ (eventType, filename) => {
504
+ if (!filename) return;
505
+ const normalizedFilename = filename.split(path.sep).join("/");
506
+ const sourceRelativePath = `${sourceDir.sourceRelativeDir}/${normalizedFilename}`;
507
+ const outputRelativePath = `${sourceDir.outputRelativeDir}/${normalizedFilename}`;
508
+
509
+ const existing = debounceMap.get(sourceRelativePath);
510
+ if (existing) clearTimeout(existing);
511
+
512
+ const timer = setTimeout(async () => {
513
+ debounceMap.delete(sourceRelativePath);
514
+ try {
515
+ if (!shouldCopyThemeSourceFile(sourceRelativePath, outputRelativePath, skipInstances)) {
516
+ return;
517
+ }
518
+
519
+ const fileStat = await stat(
520
+ path.join(rootDir, sourceRelativePath),
521
+ ).catch(() => null);
522
+ if (fileStat && fileStat.isFile()) {
523
+ await copyThemeSourceFile(
524
+ rootDir,
525
+ distDir,
526
+ sourceRelativePath,
527
+ outputRelativePath,
528
+ knownAssetLogicalPaths,
529
+ );
530
+ console.log(`UPDATE ${outputRelativePath}`);
531
+ } else if (!fileStat) {
532
+ // File deleted — remove from dist
533
+ const dest = path.join(distDir, outputRelativePath);
534
+ await rm(dest, { force: true });
535
+ console.log(`DELETE ${outputRelativePath}`);
536
+ }
537
+
538
+ if (
539
+ pipeline.compileStyles &&
540
+ shouldTriggerTailwindCssRebuild(outputRelativePath)
541
+ ) {
542
+ queueCssRebuild();
543
+ }
544
+ } catch (error) {
545
+ ui.log.warn(
546
+ `Error copying ${sourceRelativePath}: ${error.message}`,
415
547
  );
416
- console.log(`⟳ ${outputRelativePath}`);
417
- } else if (!fileStat) {
418
- // File deleted — remove from dist
419
- const dest = path.join(distDir, outputRelativePath);
420
- await rm(dest, { force: true });
421
- console.log(`✕ ${outputRelativePath}`);
422
- }
423
-
424
- if (shouldTriggerTailwindCssRebuild(outputRelativePath)) {
425
- queueCssRebuild();
426
548
  }
427
- } catch (error) {
428
- p.log.warn(`Error copying ${sourceRelativePath}: ${error.message}`);
429
- }
430
- }, DEBOUNCE_MS);
549
+ }, DEBOUNCE_MS);
431
550
 
432
- debounceMap.set(sourceRelativePath, timer);
433
- });
551
+ debounceMap.set(sourceRelativePath, timer);
552
+ },
553
+ );
434
554
 
435
555
  themeWatchers.push(watcher);
436
556
  }
437
557
 
438
558
  for (const assetDir of await getStaticAssetSourceDirs(rootDir)) {
439
- const watcher = watch(assetDir.absoluteDir, { recursive: true }, (eventType, filename) => {
440
- if (!filename) return;
441
- const relativePath = `${assetDir.relativeDir}/${filename.split(path.sep).join("/")}`;
442
-
443
- const existing = debounceMap.get(relativePath);
444
- if (existing) clearTimeout(existing);
445
-
446
- const timer = setTimeout(async () => {
447
- debounceMap.delete(relativePath);
448
- try {
449
- await handleStaticAssetChange(relativePath);
450
- } catch (error) {
451
- const errorLabel = error.message.includes("Asset collision")
452
- ? "Asset collision"
453
- : "Error compiling";
454
- p.log.warn(`${errorLabel} ${relativePath}: ${error.message}`);
455
- }
456
- }, DEBOUNCE_MS);
559
+ const watcher = watch(
560
+ assetDir.absoluteDir,
561
+ { recursive: true },
562
+ (eventType, filename) => {
563
+ if (!filename) return;
564
+ const relativePath = `${assetDir.relativeDir}/${filename.split(path.sep).join("/")}`;
565
+
566
+ const existing = debounceMap.get(relativePath);
567
+ if (existing) clearTimeout(existing);
568
+
569
+ const timer = setTimeout(async () => {
570
+ debounceMap.delete(relativePath);
571
+ try {
572
+ await handleStaticAssetChange(relativePath);
573
+ } catch (error) {
574
+ const errorLabel = error.message.includes("Asset collision")
575
+ ? "Asset collision"
576
+ : "Error compiling";
577
+ ui.log.warn(`${errorLabel} ${relativePath}: ${error.message}`);
578
+ }
579
+ }, DEBOUNCE_MS);
457
580
 
458
- debounceMap.set(relativePath, timer);
459
- });
581
+ debounceMap.set(relativePath, timer);
582
+ },
583
+ );
460
584
 
461
585
  themeWatchers.push(watcher);
462
586
  }