@lightspeed/crane 1.4.2 → 2.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 +33 -0
- package/UPGRADE.md +19 -0
- package/dist/app.d.mts +1 -1028
- package/dist/app.d.ts +1 -1028
- package/dist/app.mjs +1 -1
- package/dist/cli.mjs +20 -7
- package/package.json +3 -2
- package/template/footers/example-footer/ExampleFooter.vue +1 -1
- package/template/footers/example-footer/client.ts +1 -1
- 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/entity/color.ts +2 -2
- package/template/footers/example-footer/server.ts +1 -1
- package/template/headers/example-header/client.ts +1 -1
- package/template/headers/example-header/component/Account.vue +1 -1
- package/template/headers/example-header/component/Cart.vue +1 -1
- package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
- package/template/headers/example-header/component/Logo.vue +1 -1
- package/template/headers/example-header/component/NavigationMenu.vue +1 -1
- package/template/headers/example-header/component/SearchForm.vue +1 -1
- package/template/headers/example-header/server.ts +1 -1
- package/template/index.d.ts +1 -1
- package/template/layouts/catalog/example-catalog/Main.vue +1 -1
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/client.ts +1 -1
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +1 -1
- package/template/layouts/category/example-category/Main.vue +1 -1
- package/template/layouts/category/example-category/settings/content.ts +1 -1
- package/template/layouts/category/example-category/settings/design.ts +1 -1
- package/template/layouts/product/example-product/Main.vue +1 -1
- package/template/layouts/product/example-product/settings/content.ts +1 -1
- package/template/layouts/product/example-product/settings/design.ts +1 -1
- package/template/package.json +6 -3
- package/template/page-templates/example-template/pages/catalog.ts +1 -1
- package/template/page-templates/example-template/pages/category.ts +1 -1
- package/template/page-templates/example-template/pages/product.ts +1 -1
- package/template/preview/sections/preview.html +1 -1
- package/template/preview/shared/api-routes.ts +347 -39
- package/template/preview/shared/mock.ts +43 -41
- package/template/preview/shared/preview.ts +205 -126
- package/template/preview/shared/utils.ts +208 -62
- package/template/preview/ssr-server.ts +429 -0
- package/template/preview/vite.config.js +64 -65
- package/template/reference/sections/about-us/AboutUs.vue +1 -1
- package/template/reference/sections/about-us/client.ts +1 -1
- package/template/reference/sections/about-us/component/Image.vue +1 -1
- package/template/reference/sections/about-us/component/Stats.vue +2 -2
- package/template/reference/sections/about-us/component/Title.vue +1 -1
- package/template/reference/sections/about-us/server.ts +1 -1
- package/template/reference/sections/about-us/util/visibility-provider.ts +1 -1
- package/template/reference/sections/featured-products/FeaturedProducts.vue +65 -0
- package/template/reference/sections/featured-products/assets/arrow.svg +3 -0
- package/template/reference/sections/featured-products/assets/custom_section_showcase_1_preview.png +0 -0
- package/template/reference/sections/featured-products/client.ts +5 -0
- package/template/reference/sections/featured-products/component/ProductItem.vue +71 -0
- package/template/reference/sections/featured-products/component/Title.vue +31 -0
- package/template/reference/sections/featured-products/entity/color.ts +4 -0
- package/template/reference/sections/featured-products/server.ts +5 -0
- package/template/reference/sections/featured-products/settings/content.ts +14 -0
- package/template/reference/sections/featured-products/settings/design.ts +33 -0
- package/template/reference/sections/featured-products/settings/translations.ts +24 -0
- package/template/reference/sections/featured-products/showcases/1.ts +28 -0
- package/template/reference/sections/featured-products/showcases/translations.ts +16 -0
- package/template/reference/sections/featured-products/type.ts +5 -0
- package/template/reference/sections/intro-slider/IntroSlider.vue +1 -1
- package/template/reference/sections/intro-slider/client.ts +1 -1
- package/template/reference/sections/intro-slider/component/Slider.vue +8 -2
- package/template/reference/sections/intro-slider/component/Title.vue +1 -1
- package/template/reference/sections/intro-slider/entity/color.ts +2 -2
- package/template/reference/sections/intro-slider/server.ts +1 -1
- package/template/reference/sections/tag-lines/TagLines.vue +1 -1
- package/template/reference/sections/tag-lines/client.ts +1 -1
- package/template/reference/sections/tag-lines/component/SectionImage.vue +1 -1
- package/template/reference/sections/tag-lines/component/Title.vue +1 -1
- package/template/reference/sections/tag-lines/composables/highlighted-text-image-list.ts +2 -2
- package/template/reference/sections/tag-lines/server.ts +1 -1
- package/template/reference/sections/trending-categories/TrendingCategories.vue +70 -0
- package/template/reference/sections/trending-categories/assets/arrow.svg +3 -0
- package/template/reference/sections/trending-categories/assets/custom_section_showcase_1_preview.png +0 -0
- package/template/reference/sections/trending-categories/client.ts +5 -0
- package/template/reference/sections/trending-categories/component/CategoryItem.vue +62 -0
- package/template/reference/sections/trending-categories/component/Title.vue +32 -0
- package/template/reference/sections/trending-categories/entity/color.ts +4 -0
- package/template/reference/sections/trending-categories/server.ts +5 -0
- package/template/reference/sections/trending-categories/settings/content.ts +14 -0
- package/template/reference/sections/trending-categories/settings/design.ts +33 -0
- package/template/reference/sections/trending-categories/settings/translations.ts +24 -0
- package/template/reference/sections/trending-categories/showcases/1.ts +36 -0
- package/template/reference/sections/trending-categories/showcases/translations.ts +22 -0
- package/template/reference/sections/trending-categories/type.ts +5 -0
- package/template/reference/shared/components/Button.vue +1 -1
- package/template/reference/templates/reference-template-apparel/pages/catalog.ts +1 -1
- package/template/reference/templates/reference-template-apparel/pages/category.ts +1 -1
- package/template/reference/templates/reference-template-apparel/pages/home.ts +10 -0
- package/template/reference/templates/reference-template-apparel/pages/product.ts +1 -1
- package/template/reference/templates/reference-template-bike/pages/catalog.ts +1 -1
- package/template/reference/templates/reference-template-bike/pages/category.ts +1 -1
- package/template/reference/templates/reference-template-bike/pages/home.ts +10 -0
- package/template/reference/templates/reference-template-bike/pages/product.ts +1 -1
- package/template/sections/example-section/ExampleSection.vue +8 -1
- package/template/sections/example-section/client.ts +1 -1
- package/template/sections/example-section/component/button/Button.vue +1 -1
- package/template/sections/example-section/component/image/Image.vue +1 -1
- package/template/sections/example-section/component/image/ImagesGrid.vue +1 -1
- 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 +1 -1
- package/template/sections/example-section/entity/color.ts +2 -2
- package/template/sections/example-section/server.ts +1 -1
- package/template/sections/example-section/settings/translations.ts +1 -1
- package/template/sections/example-section/showcases/translations.ts +13 -13
- package/template/shared/components/LanguageSelector.vue +1 -1
- package/template/shared/translation.ts +16 -0
- package/template/shared/utils.ts +3 -1
- package/template/tsconfig.json +1 -0
- package/types.d.ts +6 -457
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
-
import
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
import { getShowcaseData } from './preview';
|
|
3
5
|
import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
|
|
4
6
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
5
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Extract query parameter from URL
|
|
10
|
+
*/
|
|
11
|
+
function getQueryParam(url: string, paramName: string): string | null {
|
|
12
|
+
try {
|
|
13
|
+
const urlObj = new URL(url, 'http://localhost');
|
|
14
|
+
return urlObj.searchParams.get(paramName);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* Set no-cache headers to prevent browser caching
|
|
8
22
|
*/
|
|
@@ -13,88 +27,352 @@ function setNoCacheHeaders(res: ServerResponse): void {
|
|
|
13
27
|
res.setHeader('Surrogate-Control', 'no-store');
|
|
14
28
|
}
|
|
15
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Get all available showcases for a section from the dist folder
|
|
32
|
+
*/
|
|
33
|
+
function getAvailableShowcases(sectionName: string, distPath: string): string[] {
|
|
34
|
+
try {
|
|
35
|
+
const showcasesPath = path.join(distPath, 'sections', sectionName, 'js', 'showcases');
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(showcasesPath)) {
|
|
38
|
+
console.warn(`[API Routes] Showcases directory not found for section "${sectionName}": ${showcasesPath}`);
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const showcases = fs.readdirSync(showcasesPath, { withFileTypes: true })
|
|
43
|
+
.filter(dirent => dirent.isFile() && dirent.name.endsWith('.mjs'))
|
|
44
|
+
.map(dirent => dirent.name.replace('.mjs', ''))
|
|
45
|
+
.sort((a, b) => {
|
|
46
|
+
// Sort numerically if both are numbers, otherwise alphabetically
|
|
47
|
+
const aNum = parseInt(a, 10);
|
|
48
|
+
const bNum = parseInt(b, 10);
|
|
49
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
50
|
+
return aNum - bNum;
|
|
51
|
+
}
|
|
52
|
+
return a.localeCompare(b);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return showcases;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`[API Routes] Error reading showcases for section "${sectionName}":`, error);
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all showcase IDs for a given section
|
|
64
|
+
*/
|
|
65
|
+
function getShowcaseIds(sectionName: string, distPath: string): string[] {
|
|
66
|
+
return getAvailableShowcases(sectionName, distPath);
|
|
67
|
+
}
|
|
68
|
+
|
|
16
69
|
/**
|
|
17
70
|
* Handle GET /api/v1/tile
|
|
18
|
-
* Returns tile data with content and design
|
|
71
|
+
* Returns tile data with content and design for all showcases in multiple sections
|
|
72
|
+
* Fetches tiles from remote once and updates them for each section/showcase
|
|
73
|
+
* Accepts section names as parameter from Chrome extension
|
|
19
74
|
*/
|
|
20
75
|
export async function handleGetTile(
|
|
21
76
|
_req: IncomingMessage,
|
|
22
77
|
res: ServerResponse,
|
|
23
|
-
|
|
24
|
-
|
|
78
|
+
sectionNames: string[],
|
|
79
|
+
authToken?: string,
|
|
80
|
+
tileUrl?: string,
|
|
25
81
|
): Promise<void> {
|
|
26
82
|
try {
|
|
27
83
|
const distPath = path.resolve(process.cwd(), 'dist');
|
|
28
84
|
|
|
29
|
-
//
|
|
30
|
-
|
|
85
|
+
// Fetch tiles from remote once
|
|
86
|
+
let responseData = await fetchTiles(authToken, tileUrl);
|
|
87
|
+
|
|
88
|
+
// Fetch all showcase data in parallel
|
|
89
|
+
const showcaseDataPromises = sectionNames.flatMap((sectionName) => {
|
|
90
|
+
const showcaseIds = getShowcaseIds(sectionName, distPath);
|
|
91
|
+
return showcaseIds.map(async (showcaseId) => {
|
|
92
|
+
const { content, design } = await getShowcaseData(
|
|
93
|
+
sectionName,
|
|
94
|
+
showcaseId,
|
|
95
|
+
distPath,
|
|
96
|
+
);
|
|
97
|
+
return { sectionName, showcaseId, content, design };
|
|
98
|
+
});
|
|
99
|
+
});
|
|
31
100
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
101
|
+
const allShowcaseData = await Promise.all(showcaseDataPromises);
|
|
102
|
+
|
|
103
|
+
for (const { sectionName, showcaseId, content, design } of allShowcaseData) {
|
|
104
|
+
responseData = updateTilesSection(responseData, sectionName, showcaseId, content, design);
|
|
105
|
+
}
|
|
35
106
|
|
|
36
107
|
res.statusCode = 200;
|
|
37
108
|
res.setHeader('Content-Type', 'application/json');
|
|
38
109
|
setNoCacheHeaders(res);
|
|
39
110
|
res.end(JSON.stringify(responseData, null, 2));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('[API Routes] ❌ Error processing tile data:', error);
|
|
113
|
+
res.statusCode = 500;
|
|
114
|
+
res.setHeader('Content-Type', 'application/json');
|
|
115
|
+
setNoCacheHeaders(res);
|
|
116
|
+
res.end(JSON.stringify({
|
|
117
|
+
error: 'Failed to process tile data',
|
|
118
|
+
message: error instanceof Error ? error.message : String(error),
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handle GET /api/v1/sections
|
|
125
|
+
* Returns list of available sections with their showcases
|
|
126
|
+
*/
|
|
127
|
+
export function handleGetSections(
|
|
128
|
+
_req: IncomingMessage,
|
|
129
|
+
res: ServerResponse,
|
|
130
|
+
): void {
|
|
131
|
+
try {
|
|
132
|
+
const distPath = path.resolve(process.cwd(), 'dist');
|
|
133
|
+
const sectionsPath = path.join(distPath, 'sections');
|
|
134
|
+
|
|
135
|
+
// Check if sections directory exists
|
|
136
|
+
if (!fs.existsSync(sectionsPath)) {
|
|
137
|
+
res.statusCode = 200;
|
|
138
|
+
res.setHeader('Content-Type', 'application/json');
|
|
139
|
+
setNoCacheHeaders(res);
|
|
140
|
+
res.end(JSON.stringify({ sections: [] }));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Read all directories in the sections folder
|
|
145
|
+
const sectionNames = fs.readdirSync(sectionsPath, { withFileTypes: true })
|
|
146
|
+
.filter(dirent => dirent.isDirectory())
|
|
147
|
+
.map(dirent => dirent.name);
|
|
40
148
|
|
|
149
|
+
// Build sections array with showcase data
|
|
150
|
+
const sections = sectionNames.map(sectionName => ({
|
|
151
|
+
name: sectionName,
|
|
152
|
+
showcases: getAvailableShowcases(sectionName, distPath),
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
res.statusCode = 200;
|
|
156
|
+
res.setHeader('Content-Type', 'application/json');
|
|
157
|
+
setNoCacheHeaders(res);
|
|
158
|
+
res.end(JSON.stringify({ sections }, null, 2));
|
|
41
159
|
} catch (error) {
|
|
42
|
-
console.error('[API Routes] ❌ Error
|
|
160
|
+
console.error('[API Routes] ❌ Error listing sections:', error);
|
|
43
161
|
res.statusCode = 500;
|
|
44
162
|
res.setHeader('Content-Type', 'application/json');
|
|
45
163
|
setNoCacheHeaders(res);
|
|
46
164
|
res.end(JSON.stringify({
|
|
47
|
-
error: 'Failed to
|
|
165
|
+
error: 'Failed to list sections',
|
|
48
166
|
message: error instanceof Error ? error.message : String(error),
|
|
49
|
-
sectionName,
|
|
50
|
-
showcaseId
|
|
51
167
|
}));
|
|
52
168
|
}
|
|
53
169
|
}
|
|
54
170
|
|
|
55
171
|
/**
|
|
56
172
|
* Handle POST /api/v1/custom_content
|
|
57
|
-
* Updates custom content for
|
|
173
|
+
* Updates custom content for multiple sections
|
|
174
|
+
* Accepts section names as parameter from Chrome extension
|
|
58
175
|
*/
|
|
59
176
|
export async function handleUpdateCustomContent(
|
|
60
177
|
_req: IncomingMessage,
|
|
61
178
|
res: ServerResponse,
|
|
62
|
-
|
|
63
|
-
|
|
179
|
+
sectionNames: string[],
|
|
180
|
+
authToken?: string,
|
|
181
|
+
customContentUrl?: string,
|
|
64
182
|
): Promise<void> {
|
|
65
183
|
try {
|
|
66
184
|
const distPath = path.resolve(process.cwd(), 'dist');
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Use updateCustomContent function
|
|
70
|
-
const updatedCustomContent = await updateCustomContent(sectionName, showcaseId, distPath);
|
|
185
|
+
const tilesUrl = customContentUrl ? customContentUrl.replace('/custom_content', '/tile?published=false') : '';
|
|
71
186
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
187
|
+
// Update custom content for all sections
|
|
188
|
+
const updatedCustomContent = await updateCustomContent(
|
|
189
|
+
sectionNames,
|
|
190
|
+
distPath,
|
|
191
|
+
authToken,
|
|
192
|
+
customContentUrl,
|
|
193
|
+
tilesUrl,
|
|
194
|
+
);
|
|
75
195
|
|
|
76
196
|
// Build the response data
|
|
77
|
-
const responseData =
|
|
197
|
+
const responseData = updatedCustomContent || {
|
|
78
198
|
error: 'Failed to update custom content',
|
|
79
|
-
|
|
80
|
-
showcaseId
|
|
199
|
+
sectionNames,
|
|
81
200
|
};
|
|
82
201
|
|
|
83
|
-
res.statusCode =
|
|
202
|
+
res.statusCode = updatedCustomContent ? 200 : 500;
|
|
84
203
|
res.setHeader('Content-Type', 'application/json');
|
|
85
204
|
setNoCacheHeaders(res);
|
|
86
205
|
res.end(JSON.stringify(responseData, null, 2));
|
|
87
|
-
|
|
88
206
|
} catch (error) {
|
|
89
|
-
console.error('[API Routes] ❌ Error
|
|
207
|
+
console.error('[API Routes] ❌ Error processing custom content update:', error);
|
|
90
208
|
res.statusCode = 500;
|
|
91
209
|
res.setHeader('Content-Type', 'application/json');
|
|
92
210
|
setNoCacheHeaders(res);
|
|
93
211
|
res.end(JSON.stringify({
|
|
94
212
|
error: 'Failed to process request',
|
|
95
213
|
message: error instanceof Error ? error.message : String(error),
|
|
96
|
-
|
|
97
|
-
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Serve a client file (JS or CSS)
|
|
220
|
+
*/
|
|
221
|
+
async function serveClientFile(
|
|
222
|
+
res: ServerResponse,
|
|
223
|
+
sectionName: string,
|
|
224
|
+
filePath: string,
|
|
225
|
+
contentType: string,
|
|
226
|
+
): Promise<void> {
|
|
227
|
+
try {
|
|
228
|
+
const distPath = path.resolve(process.cwd(), 'dist');
|
|
229
|
+
const fullPath = path.join(distPath, 'sections', sectionName, 'js', 'main', 'client', filePath);
|
|
230
|
+
|
|
231
|
+
// Security: prevent directory traversal
|
|
232
|
+
const resolvedPath = path.resolve(fullPath);
|
|
233
|
+
const basePath = path.resolve(distPath, 'sections', sectionName);
|
|
234
|
+
if (!resolvedPath.startsWith(basePath)) {
|
|
235
|
+
res.statusCode = 403;
|
|
236
|
+
res.setHeader('Content-Type', 'application/json');
|
|
237
|
+
setNoCacheHeaders(res);
|
|
238
|
+
res.end(JSON.stringify({ error: 'Access denied' }));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
243
|
+
res.statusCode = 404;
|
|
244
|
+
res.setHeader('Content-Type', 'application/json');
|
|
245
|
+
setNoCacheHeaders(res);
|
|
246
|
+
res.end(JSON.stringify({ error: 'File not found', file: filePath, section: sectionName }));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const fileContent = fs.readFileSync(resolvedPath, 'utf-8');
|
|
251
|
+
res.statusCode = 200;
|
|
252
|
+
res.setHeader('Content-Type', contentType);
|
|
253
|
+
setNoCacheHeaders(res);
|
|
254
|
+
res.end(fileContent);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('[API Routes] ❌ Error serving client file:', error);
|
|
257
|
+
res.statusCode = 500;
|
|
258
|
+
res.setHeader('Content-Type', 'application/json');
|
|
259
|
+
setNoCacheHeaders(res);
|
|
260
|
+
res.end(JSON.stringify({
|
|
261
|
+
error: 'Failed to serve file',
|
|
262
|
+
message: error instanceof Error ? error.message : String(error),
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Handle GET /api/v1/client-js
|
|
269
|
+
* Returns client.js for a specific section
|
|
270
|
+
*/
|
|
271
|
+
async function handleGetClientJs(
|
|
272
|
+
_req: IncomingMessage,
|
|
273
|
+
res: ServerResponse,
|
|
274
|
+
sectionName: string,
|
|
275
|
+
): Promise<void> {
|
|
276
|
+
await serveClientFile(res, sectionName, 'client.js', 'application/javascript');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Handle GET /api/v1/client-css
|
|
281
|
+
* Returns client.css for a specific section
|
|
282
|
+
*/
|
|
283
|
+
async function handleGetClientCss(
|
|
284
|
+
_req: IncomingMessage,
|
|
285
|
+
res: ServerResponse,
|
|
286
|
+
sectionName: string,
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
await serveClientFile(res, sectionName, 'assets/client.css', 'text/css');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Handle GET /api/v1/assets
|
|
293
|
+
* Returns assets for a specific section
|
|
294
|
+
*/
|
|
295
|
+
async function handleGetAssets(
|
|
296
|
+
req: IncomingMessage,
|
|
297
|
+
res: ServerResponse,
|
|
298
|
+
sectionName: string,
|
|
299
|
+
): Promise<void> {
|
|
300
|
+
try {
|
|
301
|
+
const url = req.url || '';
|
|
302
|
+
const filePath = getQueryParam(url, 'file');
|
|
303
|
+
|
|
304
|
+
if (!filePath) {
|
|
305
|
+
res.statusCode = 400;
|
|
306
|
+
res.setHeader('Content-Type', 'application/json');
|
|
307
|
+
setNoCacheHeaders(res);
|
|
308
|
+
res.end(JSON.stringify({ error: 'Missing file parameter' }));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Security: prevent directory traversal
|
|
313
|
+
if (filePath.includes('..') || filePath.startsWith('/')) {
|
|
314
|
+
res.statusCode = 403;
|
|
315
|
+
res.setHeader('Content-Type', 'application/json');
|
|
316
|
+
setNoCacheHeaders(res);
|
|
317
|
+
res.end(JSON.stringify({ error: 'Access denied' }));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const distPath = path.resolve(process.cwd(), 'dist');
|
|
322
|
+
const fullPath = path.join(distPath, 'sections', sectionName, 'assets', filePath);
|
|
323
|
+
const resolvedPath = path.resolve(fullPath);
|
|
324
|
+
const basePath = path.resolve(distPath, 'sections', sectionName, 'assets');
|
|
325
|
+
|
|
326
|
+
if (!resolvedPath.startsWith(basePath)) {
|
|
327
|
+
res.statusCode = 403;
|
|
328
|
+
res.setHeader('Content-Type', 'application/json');
|
|
329
|
+
setNoCacheHeaders(res);
|
|
330
|
+
res.end(JSON.stringify({ error: 'Access denied' }));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
335
|
+
res.statusCode = 404;
|
|
336
|
+
res.setHeader('Content-Type', 'application/json');
|
|
337
|
+
setNoCacheHeaders(res);
|
|
338
|
+
res.end(JSON.stringify({ error: 'Asset not found', file: filePath, section: sectionName }));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Detect content type based on file extension
|
|
343
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
344
|
+
const contentTypeMap: { [key: string]: string } = {
|
|
345
|
+
'.png': 'image/png',
|
|
346
|
+
'.jpg': 'image/jpeg',
|
|
347
|
+
'.jpeg': 'image/jpeg',
|
|
348
|
+
'.gif': 'image/gif',
|
|
349
|
+
'.webp': 'image/webp',
|
|
350
|
+
'.svg': 'image/svg+xml',
|
|
351
|
+
'.ico': 'image/x-icon',
|
|
352
|
+
'.woff': 'font/woff',
|
|
353
|
+
'.woff2': 'font/woff2',
|
|
354
|
+
'.ttf': 'font/ttf',
|
|
355
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
356
|
+
'.json': 'application/json',
|
|
357
|
+
'.css': 'text/css',
|
|
358
|
+
'.js': 'application/javascript',
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const contentType = contentTypeMap[ext] || 'application/octet-stream';
|
|
362
|
+
const fileContent = fs.readFileSync(resolvedPath);
|
|
363
|
+
|
|
364
|
+
res.statusCode = 200;
|
|
365
|
+
res.setHeader('Content-Type', contentType);
|
|
366
|
+
setNoCacheHeaders(res);
|
|
367
|
+
res.end(fileContent);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error('[API Routes] ❌ Error serving asset:', error);
|
|
370
|
+
res.statusCode = 500;
|
|
371
|
+
res.setHeader('Content-Type', 'application/json');
|
|
372
|
+
setNoCacheHeaders(res);
|
|
373
|
+
res.end(JSON.stringify({
|
|
374
|
+
error: 'Failed to serve asset',
|
|
375
|
+
message: error instanceof Error ? error.message : String(error),
|
|
98
376
|
}));
|
|
99
377
|
}
|
|
100
378
|
}
|
|
@@ -102,24 +380,55 @@ export async function handleUpdateCustomContent(
|
|
|
102
380
|
/**
|
|
103
381
|
* Main API router
|
|
104
382
|
* Routes requests to appropriate handlers
|
|
383
|
+
* Extracts section name and original_url from query parameters if available
|
|
384
|
+
* Resolves showcase ID from dist folder based on available showcases
|
|
105
385
|
*/
|
|
106
386
|
export function handleApiRequest(
|
|
107
387
|
req: IncomingMessage,
|
|
108
388
|
res: ServerResponse,
|
|
109
|
-
|
|
110
|
-
showcaseId: string
|
|
389
|
+
authToken?: string,
|
|
111
390
|
): void {
|
|
112
391
|
const url = req.url || '';
|
|
113
392
|
|
|
114
|
-
|
|
393
|
+
if (url.startsWith('/api/v1/sections')) {
|
|
394
|
+
handleGetSections(req, res);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Extract section name from query parameter if provided
|
|
399
|
+
const sectionName = getQueryParam(url, 'section') || '';
|
|
400
|
+
|
|
401
|
+
// Extract original_url from query parameter if provided
|
|
402
|
+
const originalUrl = getQueryParam(url, 'original_url') || '';
|
|
403
|
+
|
|
115
404
|
if (url.startsWith('/api/v1/tile')) {
|
|
116
|
-
|
|
405
|
+
// Extract section names from query parameter (comma-separated)
|
|
406
|
+
const sectionNamesParam = getQueryParam(url, 'sections');
|
|
407
|
+
const sectionNames = sectionNamesParam ? sectionNamesParam.split(',').map(s => s.trim()) : [];
|
|
408
|
+
handleGetTile(req, res, sectionNames, authToken, originalUrl);
|
|
117
409
|
return;
|
|
118
410
|
}
|
|
119
411
|
|
|
120
|
-
// Route: POST /api/v1/custom_content
|
|
121
412
|
if (url.startsWith('/api/v1/custom_content')) {
|
|
122
|
-
|
|
413
|
+
// Extract section names from query parameter (comma-separated)
|
|
414
|
+
const sectionNamesParam = getQueryParam(url, 'sections');
|
|
415
|
+
const sectionNames = sectionNamesParam ? sectionNamesParam.split(',').map(s => s.trim()) : [];
|
|
416
|
+
handleUpdateCustomContent(req, res, sectionNames, authToken, originalUrl);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (url.startsWith('/api/v1/client-js')) {
|
|
421
|
+
handleGetClientJs(req, res, sectionName);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (url.startsWith('/api/v1/client-css')) {
|
|
426
|
+
handleGetClientCss(req, res, sectionName);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (url.startsWith('/api/v1/assets')) {
|
|
431
|
+
handleGetAssets(req, res, sectionName);
|
|
123
432
|
return;
|
|
124
433
|
}
|
|
125
434
|
|
|
@@ -129,7 +438,6 @@ export function handleApiRequest(
|
|
|
129
438
|
setNoCacheHeaders(res);
|
|
130
439
|
res.end(JSON.stringify({
|
|
131
440
|
error: 'API route not found',
|
|
132
|
-
url
|
|
441
|
+
url,
|
|
133
442
|
}));
|
|
134
443
|
}
|
|
135
|
-
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export type ExternalContentMock = ExternalContentData
|
|
1
|
+
export type ExternalContentMock = ExternalContentData;
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Default mock configuration
|
|
@@ -11,11 +10,11 @@ export const externalContentMock: ExternalContentMock = {
|
|
|
11
10
|
account: {
|
|
12
11
|
title: 'My Account',
|
|
13
12
|
url: '/account',
|
|
14
|
-
target: '_self'
|
|
13
|
+
target: '_self',
|
|
15
14
|
},
|
|
16
15
|
cart: {
|
|
17
16
|
url: '/cart',
|
|
18
|
-
count: 3
|
|
17
|
+
count: 3,
|
|
19
18
|
},
|
|
20
19
|
languages: [
|
|
21
20
|
{
|
|
@@ -23,42 +22,42 @@ export const externalContentMock: ExternalContentMock = {
|
|
|
23
22
|
description: 'English',
|
|
24
23
|
main: true,
|
|
25
24
|
selected: true,
|
|
26
|
-
url: '/en'
|
|
25
|
+
url: '/en',
|
|
27
26
|
},
|
|
28
27
|
{
|
|
29
28
|
code: 'es',
|
|
30
29
|
description: 'Español',
|
|
31
30
|
main: false,
|
|
32
31
|
selected: false,
|
|
33
|
-
url: '/es'
|
|
34
|
-
}
|
|
32
|
+
url: '/es',
|
|
33
|
+
},
|
|
35
34
|
],
|
|
36
35
|
legalPages: [
|
|
37
36
|
{
|
|
38
37
|
title: 'Privacy Policy',
|
|
39
|
-
url: '/privacy'
|
|
38
|
+
url: '/privacy',
|
|
40
39
|
},
|
|
41
40
|
{
|
|
42
41
|
title: 'Terms of Service',
|
|
43
|
-
url: '/terms'
|
|
42
|
+
url: '/terms',
|
|
44
43
|
},
|
|
45
44
|
{
|
|
46
45
|
title: 'Refund Policy',
|
|
47
|
-
url: '/refunds'
|
|
48
|
-
}
|
|
46
|
+
url: '/refunds',
|
|
47
|
+
},
|
|
49
48
|
],
|
|
50
49
|
reportAbuse: {
|
|
51
50
|
title: 'Report Abuse',
|
|
52
51
|
url: '/report-abuse',
|
|
53
|
-
target: '_blank'
|
|
52
|
+
target: '_blank',
|
|
54
53
|
},
|
|
55
54
|
madeWith: {
|
|
56
55
|
url: 'https://www.lightspeedhq.com',
|
|
57
56
|
target: '_blank',
|
|
58
57
|
icon: '/assets/lightspeed-icon.png',
|
|
59
58
|
poweredBy: 'Powered by',
|
|
60
|
-
company: 'Lightspeed'
|
|
61
|
-
}
|
|
59
|
+
company: 'Lightspeed',
|
|
60
|
+
},
|
|
62
61
|
},
|
|
63
62
|
|
|
64
63
|
category: {
|
|
@@ -70,15 +69,16 @@ export const externalContentMock: ExternalContentMock = {
|
|
|
70
69
|
imageUrl: '/assets/electronics-category.jpg',
|
|
71
70
|
thumbnailImageUrl: '/assets/electronics-thumb.jpg',
|
|
72
71
|
alt: 'Electronics category',
|
|
72
|
+
productsCount: 0,
|
|
73
73
|
imageBorderInfo: {
|
|
74
74
|
homogeneity: true,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
}
|
|
75
|
+
dominatingColor: {
|
|
76
|
+
red: 255,
|
|
77
|
+
green: 255,
|
|
78
|
+
blue: 255,
|
|
79
|
+
alpha: 1,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
id: 2,
|
|
@@ -86,7 +86,8 @@ export const externalContentMock: ExternalContentMock = {
|
|
|
86
86
|
url: '/categories/clothing',
|
|
87
87
|
imageUrl: '/assets/clothing-category.jpg',
|
|
88
88
|
thumbnailImageUrl: '/assets/clothing-thumb.jpg',
|
|
89
|
-
alt: 'Clothing category'
|
|
89
|
+
alt: 'Clothing category',
|
|
90
|
+
productsCount: 0,
|
|
90
91
|
},
|
|
91
92
|
{
|
|
92
93
|
id: 3,
|
|
@@ -94,8 +95,9 @@ export const externalContentMock: ExternalContentMock = {
|
|
|
94
95
|
url: '/categories/home-garden',
|
|
95
96
|
imageUrl: '/assets/home-garden-category.jpg',
|
|
96
97
|
thumbnailImageUrl: '/assets/home-garden-thumb.jpg',
|
|
97
|
-
alt: 'Home & Garden category'
|
|
98
|
-
|
|
98
|
+
alt: 'Home & Garden category',
|
|
99
|
+
productsCount: 0,
|
|
100
|
+
},
|
|
99
101
|
],
|
|
100
102
|
categoryTree: [
|
|
101
103
|
{
|
|
@@ -103,7 +105,7 @@ export const externalContentMock: ExternalContentMock = {
|
|
|
103
105
|
name: 'Electronics',
|
|
104
106
|
nameTranslated: {
|
|
105
107
|
en: 'Electronics',
|
|
106
|
-
es: 'Electrónicos'
|
|
108
|
+
es: 'Electrónicos',
|
|
107
109
|
},
|
|
108
110
|
urlPath: '/electronics',
|
|
109
111
|
enabled: true,
|
|
@@ -113,31 +115,31 @@ export const externalContentMock: ExternalContentMock = {
|
|
|
113
115
|
name: 'Smartphones',
|
|
114
116
|
nameTranslated: {
|
|
115
117
|
en: 'Smartphones',
|
|
116
|
-
es: 'Teléfonos inteligentes'
|
|
118
|
+
es: 'Teléfonos inteligentes',
|
|
117
119
|
},
|
|
118
120
|
urlPath: '/electronics/smartphones',
|
|
119
121
|
enabled: true,
|
|
120
|
-
children: []
|
|
122
|
+
children: [],
|
|
121
123
|
},
|
|
122
124
|
{
|
|
123
125
|
id: 12,
|
|
124
126
|
name: 'Laptops',
|
|
125
127
|
nameTranslated: {
|
|
126
128
|
en: 'Laptops',
|
|
127
|
-
es: 'Portátiles'
|
|
129
|
+
es: 'Portátiles',
|
|
128
130
|
},
|
|
129
131
|
urlPath: '/electronics/laptops',
|
|
130
132
|
enabled: true,
|
|
131
|
-
children: []
|
|
132
|
-
}
|
|
133
|
-
]
|
|
133
|
+
children: [],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
134
136
|
},
|
|
135
137
|
{
|
|
136
138
|
id: 2,
|
|
137
139
|
name: 'Clothing',
|
|
138
140
|
nameTranslated: {
|
|
139
141
|
en: 'Clothing',
|
|
140
|
-
es: 'Ropa'
|
|
142
|
+
es: 'Ropa',
|
|
141
143
|
},
|
|
142
144
|
urlPath: '/clothing',
|
|
143
145
|
enabled: true,
|
|
@@ -147,27 +149,27 @@ export const externalContentMock: ExternalContentMock = {
|
|
|
147
149
|
name: 'Men\'s Clothing',
|
|
148
150
|
nameTranslated: {
|
|
149
151
|
en: 'Men\'s Clothing',
|
|
150
|
-
es: 'Ropa para hombres'
|
|
152
|
+
es: 'Ropa para hombres',
|
|
151
153
|
},
|
|
152
154
|
urlPath: '/clothing/mens',
|
|
153
155
|
enabled: true,
|
|
154
|
-
children: []
|
|
156
|
+
children: [],
|
|
155
157
|
},
|
|
156
158
|
{
|
|
157
159
|
id: 22,
|
|
158
160
|
name: 'Women\'s Clothing',
|
|
159
161
|
nameTranslated: {
|
|
160
162
|
en: 'Women\'s Clothing',
|
|
161
|
-
es: 'Ropa para mujeres'
|
|
163
|
+
es: 'Ropa para mujeres',
|
|
162
164
|
},
|
|
163
165
|
urlPath: '/clothing/womens',
|
|
164
166
|
enabled: true,
|
|
165
|
-
children: []
|
|
166
|
-
}
|
|
167
|
-
]
|
|
168
|
-
}
|
|
169
|
-
]
|
|
170
|
-
}
|
|
167
|
+
children: [],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
171
173
|
};
|
|
172
174
|
|
|
173
175
|
export function getExternalContentMock(): ExternalContentMock {
|