@lightspeed/crane 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli.mjs +15 -15
  3. package/package.json +3 -3
  4. package/template/footers/example-footer/client.ts +1 -0
  5. package/template/footers/example-footer/server.ts +1 -0
  6. package/template/headers/example-header/client.ts +1 -0
  7. package/template/headers/example-header/server.ts +1 -0
  8. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/client.ts +1 -0
  9. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +1 -0
  10. package/template/preview/shared/api-routes.ts +170 -4
  11. package/template/preview/shared/preview.ts +20 -2
  12. package/template/preview/shared/utils.ts +3 -2
  13. package/template/preview/ssr-server.ts +3 -2
  14. package/template/preview/vite.config.js +4 -2
  15. package/template/reference/sections/featured-products/client.ts +1 -0
  16. package/template/reference/sections/featured-products/server.ts +1 -0
  17. package/template/reference/sections/intro-slider/client.ts +1 -0
  18. package/template/reference/sections/intro-slider/server.ts +1 -0
  19. package/template/reference/sections/tag-lines/client.ts +1 -0
  20. package/template/reference/sections/tag-lines/composables/highlighted-text-image-list.ts +2 -1
  21. package/template/reference/sections/tag-lines/server.ts +1 -0
  22. package/template/reference/sections/trending-categories/client.ts +1 -0
  23. package/template/reference/sections/trending-categories/server.ts +1 -0
  24. package/template/reference/shared/utils/styles.ts +1 -0
  25. package/template/sections/example-section/client.ts +1 -0
  26. package/template/sections/example-section/server.ts +1 -0
  27. package/template/sections/example-section/showcases/1.ts +2 -22
  28. package/template/sections/example-section/showcases/2.ts +2 -22
  29. package/template/sections/example-section/showcases/3.ts +2 -22
  30. package/template/sections/example-section/showcases/translations.ts +4 -142
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightspeed/crane",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "type": "module",
5
5
  "bin": "bin/crane.js",
6
6
  "main": "./dist/app.mjs",
@@ -39,7 +39,7 @@
39
39
  "license": "MIT",
40
40
  "devDependencies": {
41
41
  "@jest/globals": "^30.0.4",
42
- "@stylistic/eslint-plugin": "^5.1.0",
42
+ "@lightspeed/eslint-config": "workspace:*",
43
43
  "@types/adm-zip": "^0.5.7",
44
44
  "@types/axios-concurrency": "^1.0.0",
45
45
  "@types/cli-progress": "^3.11.6",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@jridgewell/sourcemap-codec": "^1.5.4",
67
- "@lightspeed/crane-api": "1.0.0",
67
+ "@lightspeed/crane-api": "1.0.1",
68
68
  "@lightspeed/eslint-config-crane": "1.1.3",
69
69
  "@types/prompts": "^2.4.2",
70
70
  "@vitejs/plugin-vue": "^6.0.1",
@@ -1,4 +1,5 @@
1
1
  import { createVueClientApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleFooter from './ExampleFooter.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueServerApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleFooter from './ExampleFooter.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueClientApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleHeader from './ExampleHeader.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueServerApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleHeader from './ExampleHeader.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueClientApp } from '@lightspeed/crane-api';
2
+
2
3
  import CustomBottomBar from './CustomBottomBar.vue';
3
4
  import { Content, Design } from '../../type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueServerApp } from '@lightspeed/crane-api';
2
+
2
3
  import CustomBottomBar from './CustomBottomBar.vue';
3
4
  import { Content, Design } from '../../type.ts';
4
5
 
@@ -1,9 +1,11 @@
1
- import * as path from 'path';
2
1
  import * as fs from 'fs';
2
+ import type { IncomingMessage, ServerResponse } from 'http';
3
+ import * as path from 'path';
3
4
  import { URL } from 'url';
5
+
4
6
  import { getShowcaseData } from './preview';
5
7
  import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
6
- import type { IncomingMessage, ServerResponse } from 'http';
8
+
7
9
 
8
10
  /**
9
11
  * Extract query parameter from URL
@@ -17,6 +19,29 @@ function getQueryParam(url: string, paramName: string): string | null {
17
19
  }
18
20
  }
19
21
 
22
+ /**
23
+ * Read request body as JSON
24
+ */
25
+ function readRequestBody(req: IncomingMessage): Promise<Record<string, unknown>> {
26
+ return new Promise((resolve, reject) => {
27
+ let body = '';
28
+ req.on('data', (chunk) => {
29
+ body += chunk.toString();
30
+ });
31
+ req.on('end', () => {
32
+ try {
33
+ const parsed = body ? JSON.parse(body) : null;
34
+ resolve(parsed);
35
+ } catch (error) {
36
+ reject(error);
37
+ }
38
+ });
39
+ req.on('error', (error) => {
40
+ reject(error);
41
+ });
42
+ });
43
+ }
44
+
20
45
  /**
21
46
  * Set no-cache headers to prevent browser caching
22
47
  */
@@ -72,7 +97,7 @@ function getShowcaseIds(sectionName: string, distPath: string): string[] {
72
97
  * Fetches tiles from remote once and updates them for each section/showcase
73
98
  * Accepts section names as parameter from Chrome extension
74
99
  */
75
- export async function handleGetTile(
100
+ async function handleGetTile(
76
101
  _req: IncomingMessage,
77
102
  res: ServerResponse,
78
103
  sectionNames: string[],
@@ -120,6 +145,147 @@ export async function handleGetTile(
120
145
  }
121
146
  }
122
147
 
148
+ /**
149
+ * Handle PUT /api/v1/tile
150
+ * Updates tiles with content and design from remote
151
+ * Accepts an array of tiles in the request body
152
+ * For each tile, fetches remote data, updates content and design, and sends to original_url
153
+ */
154
+ async function handleUpdateTile(
155
+ req: IncomingMessage,
156
+ res: ServerResponse,
157
+ authToken?: string,
158
+ originalUrl?: string,
159
+ ): Promise<void> {
160
+ const requestBody = await readRequestBody(req);
161
+ try {
162
+ // Read request body containing tiles array
163
+
164
+ if (!requestBody || !Array.isArray(requestBody.tiles)) {
165
+ res.statusCode = 400;
166
+ res.setHeader('Content-Type', 'application/json');
167
+ setNoCacheHeaders(res);
168
+ res.end(JSON.stringify({
169
+ error: 'Invalid request body',
170
+ message: 'Expected { tiles: [...] } in request body',
171
+ }));
172
+ return;
173
+ }
174
+
175
+ const tiles = requestBody.tiles;
176
+
177
+ // Fetch tiles from remote - add published=false query parameter
178
+ const tilesUrl = originalUrl ? `${originalUrl}?published=false` : '';
179
+ const remoteTiles = await fetchTiles(authToken, tilesUrl);
180
+
181
+ if (!remoteTiles || !remoteTiles.tiles) {
182
+ console.error('Can\'t fetch remote tiles or they dont exist from url', tilesUrl);
183
+ res.setHeader('Content-Type', 'application/json');
184
+ setNoCacheHeaders(res);
185
+ res.end(JSON.stringify(requestBody, null, 2));
186
+ return;
187
+ }
188
+
189
+ // Iterate over tiles from request body and update with remote data
190
+ const updatedTiles = tiles.map((tile: Record<string, unknown>) => {
191
+ // Find matching tile in remote data by id
192
+ const remoteTile = remoteTiles.tiles.find((rt: Record<string, unknown>) => rt.id === tile.id);
193
+
194
+ if (remoteTile) {
195
+ // Update content and design from remote
196
+ return {
197
+ ...tile,
198
+ content: remoteTile.content,
199
+ design: remoteTile.design,
200
+ };
201
+ }
202
+
203
+ // If no match found, return tile as-is
204
+ return tile;
205
+ });
206
+
207
+ // Prepare updated request body
208
+ const updatedRequestBody = {
209
+ ...requestBody,
210
+ tiles: updatedTiles,
211
+ };
212
+
213
+ // Make PUT request to original_url with updated tiles
214
+ if (!originalUrl) {
215
+ console.error('Tile update url was not provided');
216
+ res.statusCode = 400;
217
+ res.setHeader('Content-Type', 'application/json');
218
+ setNoCacheHeaders(res);
219
+ res.end(JSON.stringify(requestBody, null, 2));
220
+ return;
221
+ }
222
+
223
+ const headers: Record<string, string> = {
224
+ 'Content-Type': 'application/json',
225
+ };
226
+
227
+ if (authToken) {
228
+ headers['Authorization'] = authToken;
229
+ }
230
+
231
+ const response = await fetch(originalUrl, {
232
+ method: 'PUT',
233
+ headers,
234
+ body: JSON.stringify(updatedRequestBody),
235
+ });
236
+
237
+ if (!response.ok) {
238
+ console.error('[API Routes] Failed to update tiles at original_url:', response.status, response.statusText);
239
+ res.statusCode = response.status;
240
+ res.setHeader('Content-Type', 'application/json');
241
+ setNoCacheHeaders(res);
242
+ res.end(JSON.stringify(requestBody, null, 2));
243
+ return;
244
+ }
245
+
246
+ const responseData = await response.json();
247
+
248
+ res.statusCode = 200;
249
+ res.setHeader('Content-Type', 'application/json');
250
+ setNoCacheHeaders(res);
251
+ res.end(JSON.stringify(responseData, null, 2));
252
+ } catch (error) {
253
+ console.error('[API Routes] ❌ Error updating tiles:', error);
254
+ res.statusCode = 500;
255
+ res.setHeader('Content-Type', 'application/json');
256
+ setNoCacheHeaders(res);
257
+ res.end(JSON.stringify(requestBody, null, 2));
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Handle /api/v1/tile (GET and PUT)
263
+ * Routes to appropriate handler based on request method
264
+ */
265
+ export async function handleTile(
266
+ req: IncomingMessage,
267
+ res: ServerResponse,
268
+ sectionNames: string[],
269
+ authToken?: string,
270
+ originalUrl?: string,
271
+ ): Promise<void> {
272
+ const method = req.method?.toUpperCase();
273
+
274
+ if (method === 'GET') {
275
+ await handleGetTile(req, res, sectionNames, authToken, originalUrl);
276
+ } else if (method === 'PUT') {
277
+ await handleUpdateTile(req, res, authToken, originalUrl);
278
+ } else {
279
+ res.statusCode = 405;
280
+ res.setHeader('Content-Type', 'application/json');
281
+ setNoCacheHeaders(res);
282
+ res.end(JSON.stringify({
283
+ error: 'Method not allowed',
284
+ allowedMethods: ['GET', 'PUT'],
285
+ }));
286
+ }
287
+ }
288
+
123
289
  /**
124
290
  * Handle GET /api/v1/sections
125
291
  * Returns list of available sections with their showcases
@@ -405,7 +571,7 @@ export function handleApiRequest(
405
571
  // Extract section names from query parameter (comma-separated)
406
572
  const sectionNamesParam = getQueryParam(url, 'sections');
407
573
  const sectionNames = sectionNamesParam ? sectionNamesParam.split(',').map(s => s.trim()) : [];
408
- handleGetTile(req, res, sectionNames, authToken, originalUrl);
574
+ handleTile(req, res, sectionNames, authToken, originalUrl);
409
575
  return;
410
576
  }
411
577
 
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { loadModule, renderServerModule } from './utils';
3
2
  import { getExternalContentMock } from './mock';
3
+ import { loadModule, renderServerModule } from './utils';
4
4
 
5
5
  let distFolderPath: string | null = null;
6
6
  let currentApp: any = null;
@@ -431,6 +431,24 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
431
431
  }
432
432
  }
433
433
 
434
+ async function getLayout(showcase: any, sectionName: string, distFolder: string) {
435
+ // First, try to get layoutId from showcase
436
+ if (showcase.default?.layoutId) {
437
+ return showcase.default.layoutId;
438
+ }
439
+
440
+ // If not present in showcase, read from section's layout.mjs
441
+ const basePath = `${distFolder}/sections/${sectionName}`;
442
+ const layoutModule = await loadModule(`${basePath}/js/settings/layout.mjs`) as any;
443
+
444
+ // layout.mjs exports an array of layout configurations
445
+ // Return the first element's layoutId
446
+ if (Array.isArray(layoutModule.default) && layoutModule.default.length > 0) {
447
+ return layoutModule.default[0].layoutId;
448
+ }
449
+ throw new Error('Layout module is not an array or is empty');
450
+ }
451
+
434
452
  /**
435
453
  * Prepares showcase data (content and design) without rendering.
436
454
  * Used by API routes to return tile data.
@@ -444,7 +462,7 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
444
462
  const backgroundDesign = createBackgroundDesign(showcaseBackground, true); // API render
445
463
  const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {});
446
464
  overriddenDesign.background = backgroundDesign;
447
- overriddenDesign.layout = (showcase as any).default?.layoutId;
465
+ overriddenDesign.layout = await getLayout(showcase, sectionName, distFolder);
448
466
 
449
467
  // Prepare content
450
468
  const overriddenContent = getContentToRender(
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { getShowcaseState } from './preview';
3
- import * as path from 'path';
4
2
  import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ import { getShowcaseState } from './preview';
5
6
 
6
7
  /**
7
8
  * Renders a server.js module by directly calling the SSR server.
@@ -4,11 +4,12 @@
4
4
  * Uses Blockbuster's approach: @swc/core for parsing + importFromString for execution
5
5
  */
6
6
 
7
- import * as http from 'http';
8
7
  import { readFileSync } from 'fs';
8
+ import * as http from 'http';
9
+
9
10
  import { parse } from '@swc/core';
10
- import { importFromString } from 'module-from-string';
11
11
  import { parseHTML } from 'linkedom';
12
+ import { importFromString } from 'module-from-string';
12
13
 
13
14
  const PREVIEW_SSR_PORT = process.env.PREVIEW_SSR_PORT ? parseInt(process.env.PREVIEW_SSR_PORT, 10) : 3001;
14
15
  const EXTERNAL_IMPORTS_BLOCK_START = '/* EXTERNAL_IMPORTS_START */';
@@ -1,6 +1,8 @@
1
- import { defineConfig } from 'vite';
2
- import path from 'path';
3
1
  import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ import { defineConfig } from 'vite';
5
+
4
6
  import { handleApiRequest } from './shared/api-routes.js';
5
7
 
6
8
  export default defineConfig({
@@ -1,4 +1,5 @@
1
1
  import { createVueClientApp } from '@lightspeed/crane-api';
2
+
2
3
  import FeaturedProducts from './FeaturedProducts.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueServerApp } from '@lightspeed/crane-api';
2
+
2
3
  import FeaturedProducts from './FeaturedProducts.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueClientApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleSection from './IntroSlider.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueServerApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleSection from './IntroSlider.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueClientApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleSection from './TagLines.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,3 @@
1
- import { computed } from 'vue';
2
1
  import {
3
2
  EditorTypes,
4
3
  ImageContent,
@@ -6,6 +5,8 @@ import {
6
5
  useDeckElementContent,
7
6
  useTextElementDesign,
8
7
  } from '@lightspeed/crane-api';
8
+ import { computed } from 'vue';
9
+
9
10
  import { Content, Design } from '../type.ts';
10
11
 
11
12
  export type Highlight = {
@@ -1,4 +1,5 @@
1
1
  import { createVueServerApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleSection from './TagLines.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueClientApp } from '@lightspeed/crane-api';
2
+
2
3
  import TrendingCategories from './TrendingCategories.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueServerApp } from '@lightspeed/crane-api';
2
+
2
3
  import TrendingCategories from './TrendingCategories.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { CSSProperties } from 'vue';
2
+
2
3
  import { getColorString } from './color.ts';
3
4
 
4
5
  export function getTextStyles(design: TextDesignData): CSSProperties {
@@ -1,4 +1,5 @@
1
1
  import { createVueClientApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleSection from './ExampleSection.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { createVueServerApp } from '@lightspeed/crane-api';
2
+
2
3
  import ExampleSection from './ExampleSection.vue';
3
4
  import { Content, Design } from './type.ts';
4
5
 
@@ -160,30 +160,11 @@ export default {
160
160
  },
161
161
  toggle: {
162
162
  type: 'TOGGLE',
163
- label: '$label.showcase_1.toggle.label',
164
- description: '$label.showcase_1.toggle.description',
165
- defaults: {
166
- enabled: true,
167
- },
163
+ enabled: true,
168
164
  },
169
165
  selectbox: {
170
166
  type: 'SELECTBOX',
171
- placeholder: '$label.showcase_1.selectbox.placeholder',
172
- label: '$label.showcase_1.selectbox.label',
173
- description: '$label.showcase_1.selectbox.description',
174
- options: [
175
- {
176
- value: 'one',
177
- label: '$label.showcase_1.selectbox.one.label',
178
- },
179
- {
180
- value: 'two',
181
- label: '$label.showcase_1.selectbox.two.label',
182
- },
183
- ],
184
- defaults: {
185
- value: 'one',
186
- },
167
+ value: 'one',
187
168
  },
188
169
  info: {
189
170
  type: 'INFO',
@@ -196,7 +177,6 @@ export default {
196
177
  },
197
178
  button: {
198
179
  type: 'BUTTON',
199
- label: '$label.showcase_1.button.label',
200
180
  title: '$label.showcase_1.button.defaults.title',
201
181
  buttonType: 'HYPER_LINK',
202
182
  link: 'https://www.example.com',
@@ -139,30 +139,11 @@ export default {
139
139
  },
140
140
  toggle: {
141
141
  type: 'TOGGLE',
142
- label: '$label.showcase_2.toggle.label',
143
- description: '$label.showcase_2.toggle.description',
144
- defaults: {
145
- enabled: true,
146
- },
142
+ enabled: true,
147
143
  },
148
144
  selectbox: {
149
145
  type: 'SELECTBOX',
150
- placeholder: '$label.showcase_2.selectbox.placeholder',
151
- label: '$label.showcase_2.selectbox.label',
152
- description: '$label.showcase_2.selectbox.description',
153
- options: [
154
- {
155
- value: 'one',
156
- label: '$label.showcase_2.selectbox.one.label',
157
- },
158
- {
159
- value: 'two',
160
- label: '$label.showcase_2.selectbox.two.label',
161
- },
162
- ],
163
- defaults: {
164
- value: 'one',
165
- },
146
+ value: 'one',
166
147
  },
167
148
  info: {
168
149
  type: 'INFO',
@@ -175,7 +156,6 @@ export default {
175
156
  },
176
157
  button: {
177
158
  type: 'BUTTON',
178
- label: '$label.showcase_2.button.label',
179
159
  title: '$label.showcase_2.button.defaults.title',
180
160
  buttonType: 'HYPER_LINK',
181
161
  link: 'https://www.example.com',
@@ -156,30 +156,11 @@ export default {
156
156
  },
157
157
  toggle: {
158
158
  type: 'TOGGLE',
159
- label: '$label.showcase_3.toggle.label',
160
- description: '$label.showcase_3.toggle.description',
161
- defaults: {
162
- enabled: true,
163
- },
159
+ enabled: true,
164
160
  },
165
161
  selectbox: {
166
162
  type: 'SELECTBOX',
167
- placeholder: '$label.showcase_3.selectbox.placeholder',
168
- label: '$label.showcase_3.selectbox.label',
169
- description: '$label.showcase_3.selectbox.description',
170
- options: [
171
- {
172
- value: 'one',
173
- label: '$label.showcase_3.selectbox.one.label',
174
- },
175
- {
176
- value: 'two',
177
- label: '$label.showcase_3.selectbox.two.label',
178
- },
179
- ],
180
- defaults: {
181
- value: 'one',
182
- },
163
+ value: 'one',
183
164
  },
184
165
  info: {
185
166
  type: 'INFO',
@@ -192,7 +173,6 @@ export default {
192
173
  },
193
174
  button: {
194
175
  type: 'BUTTON',
195
- label: '$label.showcase_1.button.label',
196
176
  title: '$label.showcase_1.button.defaults.title',
197
177
  buttonType: 'HYPER_LINK',
198
178
  link: 'https://www.example.com',