@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.
- package/CHANGELOG.md +34 -0
- package/UPGRADE.md +96 -0
- package/dist/cli.mjs +44 -25
- package/package.json +6 -3
- package/template/blank/sections/blank-section/BlankSection.vue +9 -0
- package/template/blank/sections/blank-section/assets/blank_section_showcase_1_preview.jpg +0 -0
- package/template/blank/sections/blank-section/client.ts +6 -0
- package/template/blank/sections/blank-section/server.ts +6 -0
- package/template/blank/sections/blank-section/settings/content.ts +2 -0
- package/template/blank/sections/blank-section/settings/design.ts +2 -0
- package/template/blank/sections/blank-section/settings/layout.ts +2 -0
- package/template/blank/sections/blank-section/settings/translations.ts +8 -0
- package/template/blank/sections/blank-section/showcases/1.ts +15 -0
- package/template/blank/sections/blank-section/showcases/translations.ts +10 -0
- package/template/blank/sections/blank-section/type.ts +5 -0
- package/template/collections/assets/collection_cover_image.png +0 -0
- package/template/collections/example-collection/configuration.ts +21 -0
- package/template/crane.config.json +1 -1
- package/template/footers/example-footer/ExampleFooter.vue +4 -4
- package/template/footers/example-footer/component/LegalLinks.vue +1 -1
- package/template/footers/example-footer/component/MadeWith.vue +1 -1
- package/template/footers/example-footer/component/ReportAbuse.vue +1 -1
- package/template/footers/example-footer/settings/design.ts +5 -4
- package/template/footers/example-footer/settings/translations.ts +4 -3
- package/template/footers/example-footer/showcases/1.ts +4 -2
- package/template/footers/example-footer/showcases/translations.ts +4 -2
- package/template/headers/example-header/ExampleHeader.vue +1 -1
- package/template/headers/example-header/component/Account.vue +1 -1
- package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
- package/template/headers/example-header/component/Logo.vue +7 -7
- package/template/headers/example-header/component/NavigationMenu.vue +1 -1
- package/template/headers/example-header/settings/content.ts +8 -1
- package/template/headers/example-header/settings/design.ts +7 -1
- package/template/headers/example-header/settings/layout.ts +1 -1
- package/template/headers/example-header/settings/translations.ts +4 -2
- package/template/headers/example-header/showcases/1.ts +15 -11
- package/template/headers/example-header/showcases/2.ts +12 -8
- package/template/headers/example-header/showcases/translations.ts +4 -2
- package/template/layouts/catalog/example-catalog/components/Icon.vue +14 -14
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/CustomBottomBar.vue +1 -1
- package/template/package.json +1 -0
- package/template/page-templates/example-template/configuration.ts +8 -10
- package/template/page-templates/example-template/pages/catalog.ts +3 -6
- package/template/page-templates/example-template/pages/category.ts +3 -6
- package/template/page-templates/example-template/pages/home.ts +42 -57
- package/template/page-templates/example-template/pages/product.ts +3 -6
- package/template/preview/sections/preview.html +10 -6
- package/template/preview/shared/api-routes.ts +235 -102
- package/template/preview/shared/logger.ts +9 -0
- package/template/preview/shared/preview.ts +108 -72
- package/template/preview/shared/utils.ts +63 -43
- package/template/preview/ssr-server.ts +1 -1
- package/template/reference/sections/about-us/AboutUs.vue +20 -22
- package/template/reference/sections/about-us/component/Image.vue +18 -18
- package/template/reference/sections/about-us/component/Stats.vue +40 -40
- package/template/reference/sections/about-us/component/Title.vue +1 -1
- package/template/reference/sections/about-us/settings/content.ts +15 -19
- package/template/reference/sections/about-us/settings/design.ts +14 -18
- package/template/reference/sections/about-us/settings/layout.ts +7 -5
- package/template/reference/sections/about-us/settings/translations.ts +4 -2
- package/template/reference/sections/about-us/showcases/1.ts +48 -62
- package/template/reference/sections/about-us/showcases/2.ts +44 -56
- package/template/reference/sections/about-us/showcases/translations.ts +4 -2
- package/template/reference/sections/featured-products/FeaturedProducts.vue +12 -6
- package/template/reference/sections/featured-products/component/ProductItem.vue +18 -1
- package/template/reference/sections/featured-products/component/ProductPlaceholder.vue +42 -0
- package/template/reference/sections/featured-products/component/Title.vue +1 -1
- package/template/reference/sections/featured-products/settings/content.ts +8 -10
- package/template/reference/sections/featured-products/settings/design.ts +7 -7
- package/template/reference/sections/featured-products/settings/translations.ts +4 -2
- package/template/reference/sections/featured-products/showcases/1.ts +8 -12
- package/template/reference/sections/featured-products/showcases/translations.ts +4 -2
- package/template/reference/sections/intro-slider/IntroSlider.vue +6 -6
- package/template/reference/sections/intro-slider/component/Slider.vue +42 -43
- package/template/reference/sections/intro-slider/component/Title.vue +7 -7
- package/template/reference/sections/intro-slider/settings/content.ts +33 -36
- package/template/reference/sections/intro-slider/settings/design.ts +17 -22
- package/template/reference/sections/intro-slider/settings/layout.ts +6 -4
- package/template/reference/sections/intro-slider/settings/translations.ts +4 -2
- package/template/reference/sections/intro-slider/showcases/1.ts +52 -75
- package/template/reference/sections/intro-slider/showcases/2.ts +50 -72
- package/template/reference/sections/intro-slider/showcases/translations.ts +4 -2
- package/template/reference/sections/tag-lines/TagLines.vue +41 -47
- package/template/reference/sections/tag-lines/component/HighlightedText.vue +1 -1
- package/template/reference/sections/tag-lines/component/SectionImage.vue +18 -18
- package/template/reference/sections/tag-lines/component/Title.vue +1 -1
- package/template/reference/sections/tag-lines/settings/content.ts +47 -47
- package/template/reference/sections/tag-lines/settings/design.ts +15 -19
- package/template/reference/sections/tag-lines/settings/layout.ts +6 -4
- package/template/reference/sections/tag-lines/settings/translations.ts +4 -2
- package/template/reference/sections/tag-lines/showcases/1.ts +40 -50
- package/template/reference/sections/tag-lines/showcases/2.ts +40 -50
- package/template/reference/sections/tag-lines/showcases/translations.ts +4 -2
- package/template/reference/sections/trending-categories/TrendingCategories.vue +1 -1
- package/template/reference/sections/trending-categories/component/CategoryItem.vue +18 -1
- package/template/reference/sections/trending-categories/component/Title.vue +1 -1
- package/template/reference/sections/trending-categories/settings/content.ts +8 -10
- package/template/reference/sections/trending-categories/settings/design.ts +7 -7
- package/template/reference/sections/trending-categories/settings/translations.ts +4 -2
- package/template/reference/sections/trending-categories/showcases/1.ts +14 -15
- package/template/reference/sections/trending-categories/showcases/translations.ts +4 -2
- package/template/reference/shared/components/Button.vue +6 -6
- package/template/reference/shared/components/SectionWrapper.vue +5 -5
- package/template/reference/shared/components/Tagline.vue +12 -11
- package/template/reference/templates/reference-template-apparel/configuration.ts +8 -8
- package/template/reference/templates/reference-template-apparel/pages/catalog.ts +3 -6
- package/template/reference/templates/reference-template-apparel/pages/category.ts +3 -6
- package/template/reference/templates/reference-template-apparel/pages/home.ts +14 -18
- package/template/reference/templates/reference-template-apparel/pages/product.ts +3 -6
- package/template/reference/templates/reference-template-bike/configuration.ts +9 -9
- package/template/reference/templates/reference-template-bike/pages/catalog.ts +3 -6
- package/template/reference/templates/reference-template-bike/pages/category.ts +3 -6
- package/template/reference/templates/reference-template-bike/pages/home.ts +14 -18
- package/template/reference/templates/reference-template-bike/pages/product.ts +3 -6
- package/template/sections/example-section/ExampleSection.vue +3 -5
- package/template/sections/example-section/component/button/Button.vue +1 -1
- package/template/sections/example-section/component/image/Image.vue +43 -43
- package/template/sections/example-section/component/image/ImagesGrid.vue +21 -32
- package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
- package/template/sections/example-section/component/title/Title.vue +1 -1
- package/template/sections/example-section/component/toggle/Toggle.vue +4 -4
- package/template/sections/example-section/settings/content.ts +25 -34
- package/template/sections/example-section/settings/design.ts +15 -19
- package/template/sections/example-section/settings/layout.ts +15 -14
- package/template/sections/example-section/settings/translations.ts +4 -2
- package/template/sections/example-section/showcases/1.ts +52 -79
- package/template/sections/example-section/showcases/2.ts +46 -62
- package/template/sections/example-section/showcases/3.ts +50 -76
- package/template/sections/example-section/showcases/translations.ts +4 -2
- package/template/shared/components/LanguageSelector.vue +1 -1
- 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
|
|
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 {
|
|
15
|
-
import {
|
|
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
|
-
|
|
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 -
|
|
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
|
|
346
|
+
return processDesignEditor(editor as ContentEditor, fieldName);
|
|
279
347
|
});
|
|
280
348
|
|
|
281
|
-
// Build defaults from design editors
|
|
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
|
-
|
|
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
|
-
|
|
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)}
|
|
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
|
|
434
|
+
* Get all available showcases for a block from the dist folder
|
|
378
435
|
*/
|
|
379
|
-
function getAvailableShowcases(
|
|
436
|
+
function getAvailableShowcases(blockType: BlockType, blockName: string, distPath: string): string[] {
|
|
380
437
|
try {
|
|
381
|
-
const showcasesPath = path.join(distPath,
|
|
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
|
|
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
|
|
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(
|
|
405
|
-
return getAvailableShowcases(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
515
|
+
blockType,
|
|
516
|
+
blockName,
|
|
433
517
|
showcaseId,
|
|
434
518
|
distPath,
|
|
435
519
|
);
|
|
436
|
-
return {
|
|
520
|
+
return { blockName, showcaseId, content, design };
|
|
437
521
|
});
|
|
438
522
|
});
|
|
439
523
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
//
|
|
631
|
-
const
|
|
632
|
-
.
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
873
|
+
blockName: string,
|
|
761
874
|
): Promise<void> {
|
|
762
|
-
await serveClientFile(res,
|
|
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
|
-
|
|
885
|
+
blockName: string,
|
|
773
886
|
): Promise<void> {
|
|
774
|
-
await serveClientFile(res,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
+
}
|