@bookshop/hugo-engine 3.17.1 → 3.18.0-alpha.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/lib/engine.js CHANGED
@@ -3,6 +3,65 @@ import { gunzipSync } from 'fflate';
3
3
  import translateTextTemplate from './translateTextTemplate.js';
4
4
  import { IdentifierParser } from './hugoIdentifierParser.js';
5
5
 
6
+ const verboseLog = (message, ...args) => {
7
+ if (typeof window !== 'undefined' && window?.bookshopLiveVerbose) {
8
+ console.log(message, ...args);
9
+ }
10
+ };
11
+
12
+ const UNIFIED_LAYOUT = [
13
+ `{{ if .Params.components }}`,
14
+ `{{ range $i, $c := .Params.components }}`,
15
+ `<script type="bookshop/batch" data-batch="{{ $.Params.batch_id }}" data-id="{{ $i }}" data-pos="start"></script>`,
16
+ `{{ if eq $c.type "component" }}`,
17
+ `{{ partial "bookshop" (slice $c.name $c.props) }}`,
18
+ `{{ else }}`,
19
+ `{{ partial "bookshop_partial" (slice $c.name $c.props) }}`,
20
+ `{{ end }}`,
21
+ `<script type="bookshop/batch" data-batch="{{ $.Params.batch_id }}" data-id="{{ $i }}" data-pos="end"></script>`,
22
+ `{{ end }}`,
23
+ `{{ else if .Params.bookshop_name }}`,
24
+ `{{ if eq .Params.bookshop_type "shared" }}`,
25
+ `{{ partial "bookshop_partial" (slice .Params.bookshop_name .Params.component) }}`,
26
+ `{{ else }}`,
27
+ `{{ partial "bookshop" (slice .Params.bookshop_name .Params.component) }}`,
28
+ `{{ end }}`,
29
+ `{{ end }}`
30
+ ].join('');
31
+
32
+ const ensureUnifiedLayoutInstalled = () => {
33
+ if (typeof window === 'undefined') return {};
34
+ if (window.__bookshop_unified_layout_installed) return {};
35
+
36
+ window.__bookshop_unified_layout_installed = true;
37
+ verboseLog('[hugo-engine] Installing unified static layout');
38
+ return { "layouts/index.html": UNIFIED_LAYOUT };
39
+ };
40
+
41
+ /**
42
+ * Build Hugo with retry logic using componentQuack for error recovery.
43
+ * @param {Engine} engine - The engine instance for componentQuack access
44
+ * @param {string} context - Description for logging (e.g., "rendering" or "batch rendering")
45
+ * @returns {Promise<string|null>} - The build error if unrecoverable, or null on success
46
+ */
47
+ const buildHugoWithRetry = async (engine, context = "rendering") => {
48
+ window.hugo_wasm_logging = [];
49
+ let render_attempts = 1;
50
+ let buildError = window.buildHugo();
51
+ while (buildError && render_attempts < 5) {
52
+ console.warn(`Hit a build error when ${context} Hugo:\n${window.hugo_wasm_logging.map(l => ` ${l}`).join('\n')}`);
53
+ if (await engine.componentQuack(buildError, window.hugo_wasm_logging) === null) {
54
+ // Can't find a template to overwrite and re-render
55
+ break;
56
+ }
57
+ // Try render again with the problem template stubbed out
58
+ window.hugo_wasm_logging = [];
59
+ buildError = window.buildHugo();
60
+ render_attempts += 1;
61
+ }
62
+ return buildError;
63
+ };
64
+
6
65
  const sleep = (ms = 0) => {
7
66
  return new Promise(r => setTimeout(r, ms));
8
67
  }
@@ -292,17 +351,19 @@ export class Engine {
292
351
  };
293
352
 
294
353
  let writeFiles = {};
354
+ let componentType = null;
295
355
 
296
356
  if (this.hasComponent(name)) {
297
- writeFiles["layouts/index.html"] = `{{ partial "bookshop" (slice "${name}" .Params.component) }}`
357
+ componentType = "component";
298
358
  } else if (this.hasShared(name)) {
299
- writeFiles["layouts/index.html"] = `{{ partial "bookshop_partial" (slice "${name}" .Params.component) }}`
359
+ componentType = "shared";
300
360
  } else {
301
361
  console.warn(`[hugo-engine] No component found for ${name}`);
302
362
  return "";
303
363
  }
304
- logger?.log?.(`Going to render ${name}, with layout:`);
305
- logger?.log?.(writeFiles["layouts/index.html"]);
364
+
365
+ Object.assign(writeFiles, ensureUnifiedLayoutInstalled());
366
+ logger?.log?.(`Going to render ${name}`);
306
367
 
307
368
  if (!globals || typeof globals !== "object") globals = {};
308
369
  props = {
@@ -313,25 +374,13 @@ export class Engine {
313
374
  // If we have assigned a root scope we need to pass that in as the context
314
375
  if (props["."]) props = props["."];
315
376
  writeFiles["content/_index.md"] = JSON.stringify({
377
+ bookshop_name: name,
378
+ bookshop_type: componentType,
316
379
  component: props
317
380
  }, null, 2) + "\n";
318
381
  window.writeHugoFiles(JSON.stringify(writeFiles));
319
382
 
320
- window.hugo_wasm_logging = [];
321
- let render_attempts = 1;
322
- let buildError = window.buildHugo();
323
- while (buildError && render_attempts < 5) {
324
- console.warn(`Hit a build error when rendering Hugo:\n${window.hugo_wasm_logging.map(l => ` ${l}`).join('\n')}`);
325
- if (this.componentQuack(buildError, window.hugo_wasm_logging) === null) {
326
- // Can't find a template to overwrite and re-render
327
- break;
328
- }
329
- // Try render again with the problem template stubbed out
330
- window.hugo_wasm_logging = [];
331
- buildError = window.buildHugo();
332
- render_attempts += 1;
333
- }
334
-
383
+ const buildError = await buildHugoWithRetry(this, "rendering");
335
384
  if (buildError) {
336
385
  console.error(buildError);
337
386
  return;
@@ -342,7 +391,87 @@ export class Engine {
342
391
  ]));
343
392
 
344
393
  target.innerHTML = output["public/index.html"];
345
- return;
394
+ }
395
+
396
+ async renderBatch(components, logger) {
397
+ if (!components || components.length === 0) return;
398
+
399
+ if (components.length === 1) {
400
+ const c = components[0];
401
+ return this.render(c.target, c.name, c.props, c.globals, logger);
402
+ }
403
+
404
+ while (!window.buildHugo) {
405
+ logger?.log?.(`Waiting for the Hugo WASM to be available...`);
406
+ await sleep(100);
407
+ }
408
+
409
+ verboseLog(`[hugo-engine] Batch rendering ${components.length} components`);
410
+
411
+ // Generate a unique batch ID to prevent marker collisions with component content
412
+ const batchId = `b${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
413
+
414
+ const componentData = components.map((c, index) => {
415
+ const isComponent = this.hasComponent(c.name);
416
+ const isShared = this.hasShared(c.name);
417
+
418
+ if (!isComponent && !isShared) {
419
+ console.warn(`[hugo-engine] No component found for ${c.name}`);
420
+ return null;
421
+ }
422
+
423
+ let props = { ...(c.globals || {}), ...(c.props || {}), env_bookshop_live: true };
424
+ if (props["."]) props = props["."];
425
+
426
+ return {
427
+ index,
428
+ name: c.name,
429
+ type: isComponent ? 'component' : 'shared',
430
+ props
431
+ };
432
+ }).filter(Boolean);
433
+
434
+ if (componentData.length === 0) return;
435
+
436
+ let writeFiles = {
437
+ "content/_index.md": JSON.stringify({
438
+ batch_id: batchId,
439
+ components: componentData.map(c => ({ name: c.name, type: c.type, props: c.props }))
440
+ }, null, 2) + "\n",
441
+ ...ensureUnifiedLayoutInstalled()
442
+ };
443
+
444
+ window.writeHugoFiles(JSON.stringify(writeFiles));
445
+
446
+ const buildError = await buildHugoWithRetry(this, "batch rendering");
447
+ if (buildError) {
448
+ console.error(`Batch render error: ${buildError}`);
449
+ verboseLog('[hugo-engine] Falling back to individual renders');
450
+ for (const c of components) {
451
+ await this.render(c.target, c.name, c.props, c.globals, logger);
452
+ }
453
+ return;
454
+ }
455
+
456
+ const output = window.readHugoFiles(JSON.stringify(["public/index.html"]));
457
+ const html = output["public/index.html"];
458
+
459
+ for (let i = 0; i < componentData.length; i++) {
460
+ const originalIndex = componentData[i].index;
461
+ const startMarker = `<script type="bookshop/batch" data-batch="${batchId}" data-id="${i}" data-pos="start"></script>`;
462
+ const endMarker = `<script type="bookshop/batch" data-batch="${batchId}" data-id="${i}" data-pos="end"></script>`;
463
+ const startIdx = html.indexOf(startMarker);
464
+ const endIdx = html.indexOf(endMarker);
465
+
466
+ if (startIdx !== -1 && endIdx !== -1) {
467
+ const componentHtml = html.substring(startIdx + startMarker.length, endIdx);
468
+ components[originalIndex].target.innerHTML = componentHtml;
469
+ } else {
470
+ verboseLog(`[hugo-engine] Could not find markers for component ${i}, falling back to individual render`);
471
+ const original = components[originalIndex];
472
+ await this.render(original.target, original.name, original.props, original.globals, logger);
473
+ }
474
+ }
346
475
  }
347
476
 
348
477
  async eval(str, props = [{}], logger) {
@@ -433,6 +562,12 @@ export class Engine {
433
562
  return null;
434
563
  }
435
564
 
565
+ // This is the SLOW PATH - we're hitting Hugo WASM because no short-circuit worked
566
+ verboseLog(`[hugo-engine] eval requires Hugo WASM: "${str.substring(0, 80)}${str.length > 80 ? '...' : ''}"`);
567
+
568
+ // Reset the unified layout flag since we're about to overwrite the layout
569
+ window.__bookshop_unified_layout_installed = false;
570
+
436
571
  window.writeHugoFiles(JSON.stringify({
437
572
  "layouts/index.html": eval_str,
438
573
  "content/_index.md": JSON.stringify({ props: props_obj, full_props: full_props }, null, 2)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bookshop/hugo-engine",
3
- "version": "3.17.1",
3
+ "version": "3.18.0-alpha.1",
4
4
  "description": "Bookshop frontend Hugo renderer",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -31,7 +31,7 @@
31
31
  "esbuild": "^0.19.3",
32
32
  "fflate": "^0.7.3",
33
33
  "liquidjs": "10.17.0",
34
- "@bookshop/helpers": "3.17.1"
34
+ "@bookshop/helpers": "3.18.0-alpha.1"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=14.16"