@lightspeed/crane 2.0.5 → 3.0.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/UPGRADE.md +96 -0
  3. package/dist/cli.mjs +44 -25
  4. package/package.json +6 -3
  5. package/template/blank/sections/blank-section/BlankSection.vue +9 -0
  6. package/template/blank/sections/blank-section/assets/blank_section_showcase_1_preview.jpg +0 -0
  7. package/template/blank/sections/blank-section/client.ts +6 -0
  8. package/template/blank/sections/blank-section/server.ts +6 -0
  9. package/template/blank/sections/blank-section/settings/content.ts +2 -0
  10. package/template/blank/sections/blank-section/settings/design.ts +2 -0
  11. package/template/blank/sections/blank-section/settings/layout.ts +2 -0
  12. package/template/blank/sections/blank-section/settings/translations.ts +8 -0
  13. package/template/blank/sections/blank-section/showcases/1.ts +15 -0
  14. package/template/blank/sections/blank-section/showcases/translations.ts +10 -0
  15. package/template/blank/sections/blank-section/type.ts +5 -0
  16. package/template/collections/assets/collection_cover_image.png +0 -0
  17. package/template/collections/example-collection/configuration.ts +21 -0
  18. package/template/crane.config.json +1 -1
  19. package/template/footers/example-footer/ExampleFooter.vue +4 -4
  20. package/template/footers/example-footer/component/LegalLinks.vue +1 -1
  21. package/template/footers/example-footer/component/MadeWith.vue +1 -1
  22. package/template/footers/example-footer/component/ReportAbuse.vue +1 -1
  23. package/template/footers/example-footer/settings/design.ts +5 -4
  24. package/template/footers/example-footer/settings/translations.ts +4 -3
  25. package/template/footers/example-footer/showcases/1.ts +4 -2
  26. package/template/footers/example-footer/showcases/translations.ts +4 -2
  27. package/template/headers/example-header/ExampleHeader.vue +1 -1
  28. package/template/headers/example-header/component/Account.vue +1 -1
  29. package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
  30. package/template/headers/example-header/component/Logo.vue +7 -7
  31. package/template/headers/example-header/component/NavigationMenu.vue +1 -1
  32. package/template/headers/example-header/settings/content.ts +8 -1
  33. package/template/headers/example-header/settings/design.ts +7 -1
  34. package/template/headers/example-header/settings/layout.ts +1 -1
  35. package/template/headers/example-header/settings/translations.ts +4 -2
  36. package/template/headers/example-header/showcases/1.ts +15 -11
  37. package/template/headers/example-header/showcases/2.ts +12 -8
  38. package/template/headers/example-header/showcases/translations.ts +4 -2
  39. package/template/layouts/catalog/example-catalog/components/Icon.vue +14 -14
  40. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/CustomBottomBar.vue +1 -1
  41. package/template/package.json +1 -0
  42. package/template/page-templates/example-template/configuration.ts +8 -10
  43. package/template/page-templates/example-template/pages/catalog.ts +3 -6
  44. package/template/page-templates/example-template/pages/category.ts +3 -6
  45. package/template/page-templates/example-template/pages/home.ts +42 -57
  46. package/template/page-templates/example-template/pages/product.ts +3 -6
  47. package/template/preview/sections/preview.html +10 -6
  48. package/template/preview/shared/api-routes.ts +235 -102
  49. package/template/preview/shared/logger.ts +9 -0
  50. package/template/preview/shared/preview.ts +108 -72
  51. package/template/preview/shared/utils.ts +63 -43
  52. package/template/preview/ssr-server.ts +1 -1
  53. package/template/reference/sections/about-us/AboutUs.vue +20 -22
  54. package/template/reference/sections/about-us/component/Image.vue +18 -18
  55. package/template/reference/sections/about-us/component/Stats.vue +40 -40
  56. package/template/reference/sections/about-us/component/Title.vue +1 -1
  57. package/template/reference/sections/about-us/settings/content.ts +15 -19
  58. package/template/reference/sections/about-us/settings/design.ts +14 -18
  59. package/template/reference/sections/about-us/settings/layout.ts +7 -5
  60. package/template/reference/sections/about-us/settings/translations.ts +4 -2
  61. package/template/reference/sections/about-us/showcases/1.ts +48 -62
  62. package/template/reference/sections/about-us/showcases/2.ts +44 -56
  63. package/template/reference/sections/about-us/showcases/translations.ts +4 -2
  64. package/template/reference/sections/featured-products/FeaturedProducts.vue +12 -6
  65. package/template/reference/sections/featured-products/component/ProductItem.vue +18 -1
  66. package/template/reference/sections/featured-products/component/ProductPlaceholder.vue +42 -0
  67. package/template/reference/sections/featured-products/component/Title.vue +1 -1
  68. package/template/reference/sections/featured-products/settings/content.ts +8 -10
  69. package/template/reference/sections/featured-products/settings/design.ts +7 -7
  70. package/template/reference/sections/featured-products/settings/translations.ts +4 -2
  71. package/template/reference/sections/featured-products/showcases/1.ts +8 -12
  72. package/template/reference/sections/featured-products/showcases/translations.ts +4 -2
  73. package/template/reference/sections/intro-slider/IntroSlider.vue +6 -6
  74. package/template/reference/sections/intro-slider/component/Slider.vue +42 -43
  75. package/template/reference/sections/intro-slider/component/Title.vue +7 -7
  76. package/template/reference/sections/intro-slider/settings/content.ts +33 -36
  77. package/template/reference/sections/intro-slider/settings/design.ts +17 -22
  78. package/template/reference/sections/intro-slider/settings/layout.ts +6 -4
  79. package/template/reference/sections/intro-slider/settings/translations.ts +4 -2
  80. package/template/reference/sections/intro-slider/showcases/1.ts +52 -75
  81. package/template/reference/sections/intro-slider/showcases/2.ts +50 -72
  82. package/template/reference/sections/intro-slider/showcases/translations.ts +4 -2
  83. package/template/reference/sections/tag-lines/TagLines.vue +41 -47
  84. package/template/reference/sections/tag-lines/component/HighlightedText.vue +1 -1
  85. package/template/reference/sections/tag-lines/component/SectionImage.vue +18 -18
  86. package/template/reference/sections/tag-lines/component/Title.vue +1 -1
  87. package/template/reference/sections/tag-lines/settings/content.ts +47 -47
  88. package/template/reference/sections/tag-lines/settings/design.ts +15 -19
  89. package/template/reference/sections/tag-lines/settings/layout.ts +6 -4
  90. package/template/reference/sections/tag-lines/settings/translations.ts +4 -2
  91. package/template/reference/sections/tag-lines/showcases/1.ts +40 -50
  92. package/template/reference/sections/tag-lines/showcases/2.ts +40 -50
  93. package/template/reference/sections/tag-lines/showcases/translations.ts +4 -2
  94. package/template/reference/sections/trending-categories/TrendingCategories.vue +1 -1
  95. package/template/reference/sections/trending-categories/component/CategoryItem.vue +18 -1
  96. package/template/reference/sections/trending-categories/component/Title.vue +1 -1
  97. package/template/reference/sections/trending-categories/settings/content.ts +8 -10
  98. package/template/reference/sections/trending-categories/settings/design.ts +7 -7
  99. package/template/reference/sections/trending-categories/settings/translations.ts +4 -2
  100. package/template/reference/sections/trending-categories/showcases/1.ts +14 -15
  101. package/template/reference/sections/trending-categories/showcases/translations.ts +4 -2
  102. package/template/reference/shared/components/Button.vue +6 -6
  103. package/template/reference/shared/components/SectionWrapper.vue +5 -5
  104. package/template/reference/shared/components/Tagline.vue +12 -11
  105. package/template/reference/templates/reference-template-apparel/configuration.ts +8 -8
  106. package/template/reference/templates/reference-template-apparel/pages/catalog.ts +3 -6
  107. package/template/reference/templates/reference-template-apparel/pages/category.ts +3 -6
  108. package/template/reference/templates/reference-template-apparel/pages/home.ts +14 -18
  109. package/template/reference/templates/reference-template-apparel/pages/product.ts +3 -6
  110. package/template/reference/templates/reference-template-bike/configuration.ts +9 -9
  111. package/template/reference/templates/reference-template-bike/pages/catalog.ts +3 -6
  112. package/template/reference/templates/reference-template-bike/pages/category.ts +3 -6
  113. package/template/reference/templates/reference-template-bike/pages/home.ts +14 -18
  114. package/template/reference/templates/reference-template-bike/pages/product.ts +3 -6
  115. package/template/sections/example-section/ExampleSection.vue +3 -5
  116. package/template/sections/example-section/component/button/Button.vue +1 -1
  117. package/template/sections/example-section/component/image/Image.vue +43 -43
  118. package/template/sections/example-section/component/image/ImagesGrid.vue +21 -32
  119. package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
  120. package/template/sections/example-section/component/title/Title.vue +1 -1
  121. package/template/sections/example-section/component/toggle/Toggle.vue +4 -4
  122. package/template/sections/example-section/settings/content.ts +25 -34
  123. package/template/sections/example-section/settings/design.ts +15 -19
  124. package/template/sections/example-section/settings/layout.ts +15 -14
  125. package/template/sections/example-section/settings/translations.ts +4 -2
  126. package/template/sections/example-section/showcases/1.ts +52 -79
  127. package/template/sections/example-section/showcases/2.ts +46 -62
  128. package/template/sections/example-section/showcases/3.ts +50 -76
  129. package/template/sections/example-section/showcases/translations.ts +4 -2
  130. package/template/shared/components/LanguageSelector.vue +1 -1
  131. package/template/shared/components/SectionWrapper.vue +5 -5
@@ -3,16 +3,18 @@ import type { IncomingMessage, ServerResponse } from 'http';
3
3
  import * as path from 'path';
4
4
  import { URL } from 'url';
5
5
 
6
- import type {
6
+ import {
7
7
  ContentEditor,
8
8
  ContentSettings,
9
9
  DeckContentEditor,
10
10
  DesignSettings,
11
11
  LayoutSettings,
12
+ DesignEditorDefaults, DesignEditor,
12
13
  } from '@lightspeed/crane-api';
13
14
 
14
- import { getShowcaseData } from './preview';
15
- import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
15
+ import { debugLog } from './logger';
16
+ import { getShowcaseData, BlockType, BLOCK_TYPES } from './preview';
17
+ import { fetchTiles, updateTilesSection, updateCustomContent, getBlockType } from './utils';
16
18
 
17
19
  /**
18
20
  * AppBlock type definition for block-config response
@@ -51,7 +53,6 @@ interface LayoutConfigItem {
51
53
  svgIconText: string;
52
54
  }
53
55
 
54
-
55
56
  /**
56
57
  * Extract query parameter from URL
57
58
  */
@@ -139,6 +140,30 @@ function extractEnglishTranslations(value: unknown): unknown {
139
140
  return value;
140
141
  }
141
142
 
143
+ function transformDesignDefaults(defaults: DesignEditor): DesignEditorDefaults | string {
144
+ // Process nested properties first
145
+ const cleanedEntries = Object.entries(defaults).map(([key, value]) => {
146
+ const processedValue = (value && typeof value === 'object' && !Array.isArray(value))
147
+ ? transformDesignDefaults(value)
148
+ : value;
149
+
150
+ return [key, processedValue];
151
+ });
152
+
153
+ // Final default values don't need a type
154
+ const remainingEntries = cleanedEntries.filter(
155
+ ([key]) => key.toLowerCase() !== 'type',
156
+ );
157
+
158
+ // If we just have 1 key left, return the value
159
+ if (remainingEntries.length === 1) {
160
+ return remainingEntries[0][1] as string;
161
+ }
162
+
163
+ // Rebuild the object
164
+ return Object.fromEntries(remainingEntries);
165
+ }
166
+
142
167
  /**
143
168
  * Transform editor type to the expected format
144
169
  * INPUTBOX -> TEXT_EDITOR
@@ -154,6 +179,12 @@ function transformEditorType(type: string): string {
154
179
  return 'MULTILINE_TEXT_EDITOR';
155
180
  case 'BUTTON':
156
181
  return 'ACTION_LINK_EDITOR';
182
+ case 'PRODUCT_SELECTOR':
183
+ return 'CUSTOM_CATEGORY_PRODUCTS_SELECTOR_EDITOR';
184
+ case 'CATEGORY_SELECTOR':
185
+ return 'CUSTOM_CATEGORY_SELECTOR_EDITOR';
186
+ case 'BACKGROUND':
187
+ return 'GENERAL_EDITOR';
157
188
  default:
158
189
  return `${upperType}_EDITOR`;
159
190
  }
@@ -233,19 +264,56 @@ function processEditor(editor: ContentEditor, fieldName: string): Record<string,
233
264
  return processEditorExceptDeck(editor, fieldName);
234
265
  }
235
266
 
267
+ /**
268
+ * Process a design editor with additional properties for vuega compatibility
269
+ */
270
+ function processDesignEditor(editor: ContentEditor, fieldName: string): Record<string, unknown> {
271
+ const editorType = (editor.type as string || '').toUpperCase();
272
+ const transformedType = transformEditorType(editorType);
273
+
274
+ // Start with field and transformed type
275
+ const result: Record<string, unknown> = {
276
+ field: fieldName,
277
+ type: transformedType,
278
+ };
279
+
280
+ // Copy all original properties except 'type', 'sizes', and 'defaults'
281
+ for (const [key, value] of Object.entries(editor)) {
282
+ if (key === 'type' || key === 'defaults') {
283
+ continue;
284
+ }
285
+ if (key === 'sizes') {
286
+ // Rename 'sizes' to 'textSizes' for vuega compatibility
287
+ result.textSizes = value;
288
+ continue;
289
+ }
290
+ result[key] = extractEnglishTranslations(value);
291
+ }
292
+
293
+ // Add contentSourceField (links design editor to content field)
294
+ result.contentSourceField = fieldName;
295
+ return result;
296
+ }
236
297
 
237
298
  /**
238
299
  * Build layout config from appBlock data
239
300
  * Creates the config.layoutConfigList structure from appBlock's layouts, contentEditors, and designEditors
240
301
  */
241
- function buildLayoutConfig(appBlock: AppBlock): { type: string; layoutConfigList: LayoutConfigItem[] } {
302
+ const DEFAULT_LAYOUT_CONFIG: LayoutSettings = {
303
+ layoutId: 'CUSTOM_LAYOUT',
304
+ selectedContentSettings: [],
305
+ selectedDesignSettings: [],
306
+ };
307
+
308
+ function buildLayoutConfig(appBlock: AppBlock): { type: string; config: { layoutConfigList: LayoutConfigItem[] } } {
242
309
  const { layouts, contentEditors, designEditors } = appBlock;
243
310
 
311
+ const layoutsToProcess = layouts?.length > 0 ? layouts : [DEFAULT_LAYOUT_CONFIG];
312
+
244
313
  // contentEditors and designEditors are objects with field names as keys
245
314
  const contentEditorKeys = Object.keys(contentEditors);
246
315
  const designEditorKeys = Object.keys(designEditors);
247
-
248
- const layoutConfigList = layouts.map((layout, index) => {
316
+ const layoutConfigList = layoutsToProcess.map((layout, index) => {
249
317
  const { layoutId, selectedContentSettings, selectedDesignSettings } = layout;
250
318
 
251
319
  const contentFieldsToInclude = selectedContentSettings.length === 0
@@ -267,7 +335,7 @@ function buildLayoutConfig(appBlock: AppBlock): { type: string; layoutConfigList
267
335
  ? designEditorKeys
268
336
  : selectedDesignSettings.map(s => s.fieldName);
269
337
 
270
- // Build design mainEditors - include all design editor types with all their fields
338
+ // Build design mainEditors - use processDesignEditor for vuega-compatible output
271
339
  const designMainEditors = designFieldsToInclude
272
340
  .filter((fieldName) => {
273
341
  const editor = designEditors[fieldName];
@@ -275,25 +343,16 @@ function buildLayoutConfig(appBlock: AppBlock): { type: string; layoutConfigList
275
343
  })
276
344
  .map((fieldName) => {
277
345
  const editor = designEditors[fieldName];
278
- return processEditor(editor as ContentEditor, fieldName);
346
+ return processDesignEditor(editor as ContentEditor, fieldName);
279
347
  });
280
348
 
281
- // Build defaults from design editors (extract English translations)
349
+ // Build defaults from design editors - only transform color to full object
282
350
  const defaults: Record<string, unknown> = {};
283
351
  designFieldsToInclude.forEach((fieldName) => {
284
352
  const editor = designEditors[fieldName];
285
353
  if (editor && editor.defaults) {
286
- defaults[fieldName] = extractEnglishTranslations(editor.defaults);
287
- }
288
- });
289
-
290
- // Apply layout-specific design overrides from selectedDesignSettings
291
- selectedDesignSettings.forEach((setting) => {
292
- if (typeof setting === 'object' && setting.fieldName && setting.defaults) {
293
- defaults[setting.fieldName] = {
294
- ...(defaults[setting.fieldName] as Record<string, unknown> || {}),
295
- ...(extractEnglishTranslations(setting.defaults) as Record<string, unknown>),
296
- };
354
+ const extracted = extractEnglishTranslations(editor.defaults) as DesignEditor;
355
+ defaults[fieldName] = transformDesignDefaults(extracted);
297
356
  }
298
357
  });
299
358
 
@@ -318,7 +377,9 @@ function buildLayoutConfig(appBlock: AppBlock): { type: string; layoutConfigList
318
377
 
319
378
  return {
320
379
  type: 'CUSTOM',
321
- layoutConfigList,
380
+ config: {
381
+ layoutConfigList,
382
+ },
322
383
  };
323
384
  }
324
385
 
@@ -330,14 +391,12 @@ async function handleGetBlockConfigFull(
330
391
  req: IncomingMessage,
331
392
  res: ServerResponse,
332
393
  sectionName: string,
333
- typeParam: string,
334
394
  ): Promise<void> {
335
395
  try {
336
396
  // Fetch appBlock from the CLI's /api/v1/block-config endpoint
337
397
  const host = req.headers.host || 'localhost:5173';
338
398
  const protocol = 'http';
339
- const blockConfigUrl = `${protocol}://${host}/api/v1/block-config?section=${encodeURIComponent(sectionName)}&type=${encodeURIComponent(typeParam)}`;
340
-
399
+ const blockConfigUrl = `${protocol}://${host}/api/v1/block-config?section=${encodeURIComponent(sectionName)}`;
341
400
  const response = await fetch(blockConfigUrl);
342
401
 
343
402
  if (!response.ok) {
@@ -358,9 +417,7 @@ async function handleGetBlockConfigFull(
358
417
  res.statusCode = 200;
359
418
  res.setHeader('Content-Type', 'application/json');
360
419
  setNoCacheHeaders(res);
361
- res.end(JSON.stringify({
362
- config,
363
- }, null, 2));
420
+ res.end(JSON.stringify(config, null, 2));
364
421
  } catch (error) {
365
422
  console.error('[API Routes] ❌ Error fetching block config:', error);
366
423
  res.statusCode = 500;
@@ -374,14 +431,14 @@ async function handleGetBlockConfigFull(
374
431
  }
375
432
 
376
433
  /**
377
- * Get all available showcases for a section from the dist folder
434
+ * Get all available showcases for a block from the dist folder
378
435
  */
379
- function getAvailableShowcases(sectionName: string, distPath: string): string[] {
436
+ function getAvailableShowcases(blockType: BlockType, blockName: string, distPath: string): string[] {
380
437
  try {
381
- const showcasesPath = path.join(distPath, 'sections', sectionName, 'js', 'showcases');
438
+ const showcasesPath = path.join(distPath, blockType, blockName, 'js', 'showcases');
382
439
 
383
440
  if (!fs.existsSync(showcasesPath)) {
384
- console.warn(`[API Routes] Showcases directory not found for section "${sectionName}": ${showcasesPath}`);
441
+ console.warn(`[API Routes] Showcases directory not found for ${blockType}/"${blockName}": ${showcasesPath}`);
385
442
  return [];
386
443
  }
387
444
 
@@ -393,7 +450,27 @@ function getAvailableShowcases(sectionName: string, distPath: string): string[]
393
450
 
394
451
  return showcases;
395
452
  } catch (error) {
396
- console.error(`[API Routes] Error reading showcases for section "${sectionName}":`, error);
453
+ console.error(`[API Routes] Error reading showcases for ${blockType}/"${blockName}":`, error);
454
+ return [];
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Get all available blocks of a specific type (sections, headers, footers)
460
+ */
461
+ function getAvailableBlocks(blockType: BlockType, distPath: string): string[] {
462
+ try {
463
+ const blocksPath = path.join(distPath, blockType);
464
+
465
+ if (!fs.existsSync(blocksPath)) {
466
+ return [];
467
+ }
468
+
469
+ return fs.readdirSync(blocksPath, { withFileTypes: true })
470
+ .filter(dirent => dirent.isDirectory())
471
+ .map(dirent => dirent.name);
472
+ } catch (error) {
473
+ console.error(`[API Routes] Error reading ${blockType}:`, error);
397
474
  return [];
398
475
  }
399
476
  }
@@ -401,8 +478,8 @@ function getAvailableShowcases(sectionName: string, distPath: string): string[]
401
478
  /**
402
479
  * Get all showcase IDs for a given section
403
480
  */
404
- function getShowcaseIds(sectionName: string, distPath: string): string[] {
405
- return getAvailableShowcases(sectionName, distPath);
481
+ function getShowcaseIds(blockType: BlockType, blockName: string, distPath: string): string[] {
482
+ return getAvailableShowcases(blockType, blockName, distPath);
406
483
  }
407
484
 
408
485
  /**
@@ -414,7 +491,7 @@ function getShowcaseIds(sectionName: string, distPath: string): string[] {
414
491
  async function handleGetTile(
415
492
  _req: IncomingMessage,
416
493
  res: ServerResponse,
417
- sectionNames: string[],
494
+ blockNames: string[],
418
495
  authToken?: string,
419
496
  tileUrl?: string,
420
497
  ): Promise<void> {
@@ -423,25 +500,32 @@ async function handleGetTile(
423
500
 
424
501
  // Fetch tiles from remote once
425
502
  let responseData = await fetchTiles(authToken, tileUrl);
503
+ debugLog('[API Routes] Processing tile data');
504
+ // Process all selected blocks (sections, headers, or footers)
505
+ const blockPromises = blockNames.flatMap((blockName) => {
506
+ const blockType = getBlockType(blockName, distPath);
507
+ if (!blockType) {
508
+ console.warn(`[API Routes] Block not found: ${blockName}`);
509
+ return [];
510
+ }
426
511
 
427
- // Fetch all showcase data in parallel
428
- const showcaseDataPromises = sectionNames.flatMap((sectionName) => {
429
- const showcaseIds = getShowcaseIds(sectionName, distPath);
512
+ const showcaseIds = getShowcaseIds(blockType, blockName, distPath);
430
513
  return showcaseIds.map(async (showcaseId) => {
431
514
  const { content, design } = await getShowcaseData(
432
- sectionName,
515
+ blockType,
516
+ blockName,
433
517
  showcaseId,
434
518
  distPath,
435
519
  );
436
- return { sectionName, showcaseId, content, design };
520
+ return { blockName, showcaseId, content, design };
437
521
  });
438
522
  });
439
523
 
440
- const allShowcaseData = await Promise.all(showcaseDataPromises);
441
- for (const { sectionName, showcaseId, content, design } of allShowcaseData) {
442
- responseData = updateTilesSection(responseData, sectionName, showcaseId, content, design);
524
+ // Wait for all showcase data in parallel
525
+ const allShowcaseData = await Promise.all(blockPromises);
526
+ for (const { blockName, showcaseId, content, design } of allShowcaseData) {
527
+ responseData = updateTilesSection(responseData, blockName, showcaseId, content, design);
443
528
  }
444
-
445
529
  res.statusCode = 200;
446
530
  res.setHeader('Content-Type', 'application/json');
447
531
  setNoCacheHeaders(res);
@@ -458,12 +542,54 @@ async function handleGetTile(
458
542
  }
459
543
  }
460
544
 
545
+ async function updateTile(url: string, authToken?: string, body?: unknown) {
546
+ const headers: Record<string, string> = {
547
+ 'Content-Type': 'application/json',
548
+ };
549
+
550
+ if (authToken) {
551
+ headers['Authorization'] = authToken;
552
+ }
553
+
554
+ const response = await fetch(url, {
555
+ method: 'PUT',
556
+ headers,
557
+ body: JSON.stringify(body),
558
+ });
559
+
560
+ if (!response.ok) {
561
+ console.error('[API Routes] Failed to update tiles at original_url:', response.status, response.statusText);
562
+ return null;
563
+ }
564
+
565
+ return response.json();
566
+ }
567
+
461
568
  /**
462
569
  * Handle PUT /api/v1/tile
463
570
  * Updates tiles with content and design from remote
464
571
  * Accepts an array of tiles in the request body
465
572
  * For each tile, fetches remote data, updates content and design, and sends to original_url
466
573
  */
574
+
575
+ async function handleIndividualTileUpdate(url: string, authToken?: string, body?: unknown) {
576
+ const remoteTile = await fetchTiles(authToken, url);
577
+
578
+ const content = (body as Record<string, unknown>).content;
579
+ const design = (body as Record<string, unknown>).design;
580
+
581
+ //change content and design
582
+ const tileToUpdate = { ...remoteTile, content, design };
583
+
584
+ //get update external content
585
+ const updatedTile = await updateTile(url, authToken, tileToUpdate);
586
+
587
+ //restore to original
588
+ await updateTile(url, authToken, remoteTile);
589
+
590
+ return updatedTile;
591
+ }
592
+
467
593
  async function handleUpdateTile(
468
594
  req: IncomingMessage,
469
595
  res: ServerResponse,
@@ -477,7 +603,9 @@ async function handleUpdateTile(
477
603
  res.statusCode = 200;
478
604
  res.setHeader('Content-Type', 'application/json');
479
605
  setNoCacheHeaders(res);
480
- res.end(JSON.stringify(requestBody, null, 2));
606
+ const updatedTile = await handleIndividualTileUpdate(originalUrl, authToken, requestBody);
607
+ res.end(JSON.stringify(updatedTile, null, 2));
608
+
481
609
  return;
482
610
  }
483
611
 
@@ -540,31 +668,15 @@ async function handleUpdateTile(
540
668
  return;
541
669
  }
542
670
 
543
- const headers: Record<string, string> = {
544
- 'Content-Type': 'application/json',
545
- };
546
-
547
- if (authToken) {
548
- headers['Authorization'] = authToken;
549
- }
550
-
551
- const response = await fetch(originalUrl, {
552
- method: 'PUT',
553
- headers,
554
- body: JSON.stringify(updatedRequestBody),
555
- });
671
+ const responseData = await updateTile(originalUrl, authToken, updatedRequestBody);
556
672
 
557
- if (!response.ok) {
558
- console.error('[API Routes] Failed to update tiles at original_url:', response.status, response.statusText);
559
- res.statusCode = response.status;
673
+ if (responseData == null) {
560
674
  res.setHeader('Content-Type', 'application/json');
561
675
  setNoCacheHeaders(res);
562
676
  res.end(JSON.stringify(requestBody, null, 2));
563
677
  return;
564
678
  }
565
679
 
566
- const responseData = await response.json();
567
-
568
680
  res.statusCode = 200;
569
681
  res.setHeader('Content-Type', 'application/json');
570
682
  setNoCacheHeaders(res);
@@ -616,32 +728,22 @@ export function handleGetSections(
616
728
  ): void {
617
729
  try {
618
730
  const distPath = path.resolve(process.cwd(), 'dist');
619
- const sectionsPath = path.join(distPath, 'sections');
620
-
621
- // Check if sections directory exists
622
- if (!fs.existsSync(sectionsPath)) {
623
- res.statusCode = 200;
624
- res.setHeader('Content-Type', 'application/json');
625
- setNoCacheHeaders(res);
626
- res.end(JSON.stringify({ sections: [] }));
627
- return;
628
- }
629
731
 
630
- // Read all directories in the sections folder
631
- const sectionNames = fs.readdirSync(sectionsPath, { withFileTypes: true })
632
- .filter(dirent => dirent.isDirectory())
633
- .map(dirent => dirent.name);
634
-
635
- // Build sections array with showcase data
636
- const sections = sectionNames.map(sectionName => ({
637
- name: sectionName,
638
- showcases: getAvailableShowcases(sectionName, distPath),
639
- }));
732
+ // Build block lists for all types
733
+ const result = Object.fromEntries(
734
+ BLOCK_TYPES.map(blockType => [
735
+ blockType,
736
+ getAvailableBlocks(blockType, distPath).map(blockName => ({
737
+ name: blockName,
738
+ showcases: getAvailableShowcases(blockType, blockName, distPath),
739
+ })),
740
+ ]),
741
+ );
640
742
 
641
743
  res.statusCode = 200;
642
744
  res.setHeader('Content-Type', 'application/json');
643
745
  setNoCacheHeaders(res);
644
- res.end(JSON.stringify({ sections }, null, 2));
746
+ res.end(JSON.stringify(result, null, 2));
645
747
  } catch (error) {
646
748
  console.error('[API Routes] ❌ Error listing sections:', error);
647
749
  res.statusCode = 500;
@@ -706,17 +808,28 @@ export async function handleUpdateCustomContent(
706
808
  */
707
809
  async function serveClientFile(
708
810
  res: ServerResponse,
709
- sectionName: string,
811
+ blockName: string,
710
812
  filePath: string,
711
813
  contentType: string,
712
814
  ): Promise<void> {
713
815
  try {
714
816
  const distPath = path.resolve(process.cwd(), 'dist');
715
- const fullPath = path.join(distPath, 'sections', sectionName, 'js', 'main', 'client', filePath);
817
+
818
+ // Determine block type from folder
819
+ const blockType = getBlockType(blockName, distPath);
820
+ if (!blockType) {
821
+ res.statusCode = 404;
822
+ res.setHeader('Content-Type', 'application/json');
823
+ setNoCacheHeaders(res);
824
+ res.end(JSON.stringify({ error: 'Block not found', block: blockName }));
825
+ return;
826
+ }
827
+
828
+ const fullPath = path.join(distPath, blockType, blockName, 'js', 'main', 'client', filePath);
716
829
 
717
830
  // Security: prevent directory traversal
718
831
  const resolvedPath = path.resolve(fullPath);
719
- const basePath = path.resolve(distPath, 'sections', sectionName);
832
+ const basePath = path.resolve(distPath, blockType, blockName);
720
833
  if (!resolvedPath.startsWith(basePath)) {
721
834
  res.statusCode = 403;
722
835
  res.setHeader('Content-Type', 'application/json');
@@ -729,7 +842,7 @@ async function serveClientFile(
729
842
  res.statusCode = 404;
730
843
  res.setHeader('Content-Type', 'application/json');
731
844
  setNoCacheHeaders(res);
732
- res.end(JSON.stringify({ error: 'File not found', file: filePath, section: sectionName }));
845
+ res.end(JSON.stringify({ error: 'File not found', file: filePath, block: blockName }));
733
846
  return;
734
847
  }
735
848
 
@@ -752,36 +865,36 @@ async function serveClientFile(
752
865
 
753
866
  /**
754
867
  * Handle GET /api/v1/client-js
755
- * Returns client.js for a specific section
868
+ * Returns client.js for a specific block (section, header, or footer)
756
869
  */
757
870
  async function handleGetClientJs(
758
871
  _req: IncomingMessage,
759
872
  res: ServerResponse,
760
- sectionName: string,
873
+ blockName: string,
761
874
  ): Promise<void> {
762
- await serveClientFile(res, sectionName, 'client.js', 'application/javascript');
875
+ await serveClientFile(res, blockName, 'client.js', 'application/javascript');
763
876
  }
764
877
 
765
878
  /**
766
879
  * Handle GET /api/v1/client-css
767
- * Returns client.css for a specific section
880
+ * Returns client.css for a specific block (section, header, or footer)
768
881
  */
769
882
  async function handleGetClientCss(
770
883
  _req: IncomingMessage,
771
884
  res: ServerResponse,
772
- sectionName: string,
885
+ blockName: string,
773
886
  ): Promise<void> {
774
- await serveClientFile(res, sectionName, 'assets/client.css', 'text/css');
887
+ await serveClientFile(res, blockName, 'assets/client.css', 'text/css');
775
888
  }
776
889
 
777
890
  /**
778
891
  * Handle GET /api/v1/assets
779
- * Returns assets for a specific section
892
+ * Returns assets for a specific block (section, header, or footer)
780
893
  */
781
894
  async function handleGetAssets(
782
895
  req: IncomingMessage,
783
896
  res: ServerResponse,
784
- sectionName: string,
897
+ blockName: string,
785
898
  ): Promise<void> {
786
899
  try {
787
900
  const url = req.url || '';
@@ -805,9 +918,20 @@ async function handleGetAssets(
805
918
  }
806
919
 
807
920
  const distPath = path.resolve(process.cwd(), 'dist');
808
- const fullPath = path.join(distPath, 'sections', sectionName, 'assets', filePath);
921
+
922
+ // Determine block type from folder
923
+ const blockType = getBlockType(blockName, distPath);
924
+ if (!blockType) {
925
+ res.statusCode = 404;
926
+ res.setHeader('Content-Type', 'application/json');
927
+ setNoCacheHeaders(res);
928
+ res.end(JSON.stringify({ error: 'Block not found', block: blockName }));
929
+ return;
930
+ }
931
+
932
+ const fullPath = path.join(distPath, blockType, blockName, 'assets', filePath);
809
933
  const resolvedPath = path.resolve(fullPath);
810
- const basePath = path.resolve(distPath, 'sections', sectionName, 'assets');
934
+ const basePath = path.resolve(distPath, blockType, blockName, 'assets');
811
935
 
812
936
  if (!resolvedPath.startsWith(basePath)) {
813
937
  res.statusCode = 403;
@@ -821,7 +945,7 @@ async function handleGetAssets(
821
945
  res.statusCode = 404;
822
946
  res.setHeader('Content-Type', 'application/json');
823
947
  setNoCacheHeaders(res);
824
- res.end(JSON.stringify({ error: 'Asset not found', file: filePath, section: sectionName }));
948
+ res.end(JSON.stringify({ error: 'Asset not found', file: filePath, block: blockName }));
825
949
  return;
826
950
  }
827
951
 
@@ -881,6 +1005,16 @@ export function handleApiRequest(
881
1005
  return;
882
1006
  }
883
1007
 
1008
+ // Return WebSocket port for Chrome extension
1009
+ if (url.startsWith('/api/v1/extension-ws-port')) {
1010
+ const wsPort = process.env.CRANE_WEBSOCKET_PORT || null;
1011
+ res.statusCode = 200;
1012
+ res.setHeader('Content-Type', 'application/json');
1013
+ setNoCacheHeaders(res);
1014
+ res.end(JSON.stringify({ port: wsPort }));
1015
+ return;
1016
+ }
1017
+
884
1018
  // Extract section name from query parameter if provided
885
1019
  const sectionName = getQueryParam(url, 'section') || '';
886
1020
 
@@ -904,8 +1038,7 @@ export function handleApiRequest(
904
1038
  }
905
1039
 
906
1040
  if (url.startsWith('/api/v1/block-config-full')) {
907
- const typeParam = getQueryParam(url, 'type') || 'SECTION';
908
- handleGetBlockConfigFull(req, res, sectionName, typeParam);
1041
+ handleGetBlockConfigFull(req, res, sectionName);
909
1042
  return;
910
1043
  }
911
1044
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Debug logging for preview runtime.
3
+ * Checks CRANE_DEBUG_ENABLED env set by CLI --verbose flag.
4
+ */
5
+ export function debugLog(message: string, ...args: unknown[]): void {
6
+ if (process.env.CRANE_DEBUG_ENABLED === 'true') {
7
+ console.debug(`[DEBUG] ${message}`, ...args);
8
+ }
9
+ }