@lightspeed/crane 1.4.0 → 1.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightspeed/crane",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
5
  "bin": "bin/crane.js",
6
6
  "main": "./dist/app.mjs",
@@ -26,10 +26,12 @@
26
26
  "scripts": {
27
27
  "dev": "unbuild --stub",
28
28
  "build": "unbuild",
29
+ "link": "npm link",
29
30
  "type-check": "tsc --noEmit",
30
31
  "lint": "eslint \"./{src,tests}/**/*.{js,ts}\"",
31
32
  "lint:all": "yarn lint && yarn type-check",
32
- "test": "jest"
33
+ "test": "jest",
34
+ "test:end-to-end": "jest end-to-end.test.ts --no-coverage"
33
35
  },
34
36
  "engines": {
35
37
  "node": "22"
@@ -55,50 +57,50 @@
55
57
  "mock-fs": "^5.5.0",
56
58
  "ts-jest": "^29.4.0",
57
59
  "ts-node": "^10.9.2",
58
- "typescript": "5.4.5",
60
+ "typescript": "5.9.2",
59
61
  "unbuild": "^3.5.0",
60
62
  "vite-plugin-dts": "^4.5.4",
61
63
  "vite-plugin-static-copy": "^3.1.0"
62
64
  },
63
65
  "dependencies": {
64
- "@jridgewell/sourcemap-codec": "^1.4.15",
65
- "@lightspeed/eslint-config-crane": "1.1.2",
66
+ "@jridgewell/sourcemap-codec": "^1.5.4",
67
+ "@lightspeed/eslint-config-crane": "1.1.3",
66
68
  "@types/prompts": "^2.4.2",
67
- "@vitejs/plugin-vue": "^4.1.0",
69
+ "@vitejs/plugin-vue": "^6.0.1",
68
70
  "adm-zip": "^0.5.16",
69
- "ajv": "^8.12.0",
70
- "ajv-formats": "^2.1.1",
71
+ "ajv": "^8.17.1",
72
+ "ajv-formats": "^3.0.1",
71
73
  "axios": "^1.11.0",
72
74
  "axios-concurrency": "^1.0.4",
73
75
  "cac": "^6.7.14",
74
76
  "cli-progress": "^3.12.0",
75
77
  "eslint": "^9.33.0",
76
- "fs-extra": "^11.1.1",
77
- "glob": "^9.3.5",
78
+ "fs-extra": "^11.3.1",
79
+ "glob": "^11.0.3",
78
80
  "jsonpath-plus": "^10.3.0",
79
81
  "kolorist": "^1.8.0",
80
82
  "prompts": "^2.4.2",
81
- "semver": "^7.5.4",
82
- "terser": "^5.35.0",
83
+ "semver": "^7.7.2",
84
+ "terser": "^5.44.0",
83
85
  "tinycolor2": "^1.6.0",
84
- "typescript": "5.4.5",
85
- "vite": "^6.3.5",
86
- "vite-plugin-checker": "^0.6.1",
86
+ "typescript": "5.9.2",
87
+ "vite": "^7.1.4",
88
+ "vite-plugin-checker": "^0.10.3",
87
89
  "vite-plugin-compression": "^0.5.1",
88
90
  "vite-plugin-externals": "^0.6.2",
89
- "vite-tsconfig-paths": "^4.2.0",
90
- "vue": "^3.4.0",
91
- "vue-tsc": "^1.8.0"
91
+ "vite-tsconfig-paths": "^5.1.4",
92
+ "vue": "^3.5.21",
93
+ "vue-tsc": "^3.0.6"
92
94
  },
93
95
  "peerDependencies": {
94
- "vue": "^3.4.0"
96
+ "vue": "^3.5.21"
95
97
  },
96
98
  "overrides": {
97
99
  "axios-concurrency": {
98
100
  "axios": "1.11.0"
99
101
  },
100
102
  "magic-string": "0.30.10",
101
- "glob": "^9.3.5",
102
- "stylus": "^0.63.0"
103
+ "glob": "^11.0.3",
104
+ "stylus": "^0.64.0"
103
105
  }
104
106
  }
@@ -16,7 +16,6 @@ export default {
16
16
  },
17
17
  menu: {
18
18
  type: 'NAVIGATION_MENU',
19
- text: '$label.showcase_1.menu.text',
20
19
  },
21
20
  },
22
21
  design: {
@@ -11,7 +11,6 @@ export default {
11
11
  content: {
12
12
  menu: {
13
13
  type: 'NAVIGATION_MENU',
14
- text: '$label.showcase_2.menu.text',
15
14
  },
16
15
  logo: {
17
16
  type: 'LOGO',
@@ -2,6 +2,7 @@
2
2
  "name": "__placeholder__",
3
3
  "private": true,
4
4
  "version": "0.0.1",
5
+ "type": "module",
5
6
  "scripts": {
6
7
  "build": "crane build",
7
8
  "deploy": "crane build && crane deploy"
@@ -10,13 +11,13 @@
10
11
  "@lightspeed/crane": "latest",
11
12
  "@lightspeed/eslint-config-crane": "latest",
12
13
  "vue": "^3.4.0",
13
- "eslint": "9.22.0"
14
+ "eslint": "9.33.0"
14
15
  },
15
16
  "engines": {
16
17
  "node": ">=22"
17
18
  },
18
19
  "overrides": {
19
- "glob": "^9.3.5",
20
- "stylus": "^0.63.0"
20
+ "glob": "^11.0.3",
21
+ "stylus": "^0.64.0"
21
22
  }
22
23
  }
@@ -0,0 +1,135 @@
1
+ import * as path from 'path';
2
+ import { getContentAndDesign } from './preview';
3
+ import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
4
+ import type { IncomingMessage, ServerResponse } from 'http';
5
+
6
+ /**
7
+ * Set no-cache headers to prevent browser caching
8
+ */
9
+ function setNoCacheHeaders(res: ServerResponse): void {
10
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
11
+ res.setHeader('Pragma', 'no-cache');
12
+ res.setHeader('Expires', '0');
13
+ res.setHeader('Surrogate-Control', 'no-store');
14
+ }
15
+
16
+ /**
17
+ * Handle GET /api/v1/tile
18
+ * Returns tile data with content and design
19
+ */
20
+ export async function handleGetTile(
21
+ _req: IncomingMessage,
22
+ res: ServerResponse,
23
+ sectionName: string,
24
+ showcaseId: string
25
+ ): Promise<void> {
26
+ try {
27
+ const distPath = path.resolve(process.cwd(), 'dist');
28
+
29
+ // Use getContentAndDesign from preview.ts to fetch content and design
30
+ const { content, design } = await getContentAndDesign(sectionName, showcaseId, distPath);
31
+
32
+ // Fetch and update tiles
33
+ const tilesResponse = await fetchTiles();
34
+ const responseData = updateTilesSection(tilesResponse, sectionName, showcaseId, content, design);
35
+
36
+ res.statusCode = 200;
37
+ res.setHeader('Content-Type', 'application/json');
38
+ setNoCacheHeaders(res);
39
+ res.end(JSON.stringify(responseData, null, 2));
40
+
41
+ } catch (error) {
42
+ console.error('[API Routes] ❌ Error loading tile data:', error);
43
+ res.statusCode = 500;
44
+ res.setHeader('Content-Type', 'application/json');
45
+ setNoCacheHeaders(res);
46
+ res.end(JSON.stringify({
47
+ error: 'Failed to load tile data',
48
+ message: error instanceof Error ? error.message : String(error),
49
+ sectionName,
50
+ showcaseId
51
+ }));
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Handle POST /api/v1/custom_content
57
+ * Updates custom content for a section
58
+ */
59
+ export async function handleUpdateCustomContent(
60
+ _req: IncomingMessage,
61
+ res: ServerResponse,
62
+ sectionName: string,
63
+ showcaseId: string
64
+ ): Promise<void> {
65
+ try {
66
+ const distPath = path.resolve(process.cwd(), 'dist');
67
+ let finalResponseData = null;
68
+
69
+ // Use updateCustomContent function
70
+ const updatedCustomContent = await updateCustomContent(sectionName, showcaseId, distPath);
71
+
72
+ if (updatedCustomContent) {
73
+ finalResponseData = updatedCustomContent;
74
+ }
75
+
76
+ // Build the response data
77
+ const responseData = finalResponseData || {
78
+ error: 'Failed to update custom content',
79
+ sectionName,
80
+ showcaseId
81
+ };
82
+
83
+ res.statusCode = finalResponseData ? 200 : 500;
84
+ res.setHeader('Content-Type', 'application/json');
85
+ setNoCacheHeaders(res);
86
+ res.end(JSON.stringify(responseData, null, 2));
87
+
88
+ } catch (error) {
89
+ console.error('[API Routes] ❌ Error in update-custom-content endpoint:', error);
90
+ res.statusCode = 500;
91
+ res.setHeader('Content-Type', 'application/json');
92
+ setNoCacheHeaders(res);
93
+ res.end(JSON.stringify({
94
+ error: 'Failed to process request',
95
+ message: error instanceof Error ? error.message : String(error),
96
+ sectionName,
97
+ showcaseId
98
+ }));
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Main API router
104
+ * Routes requests to appropriate handlers
105
+ */
106
+ export function handleApiRequest(
107
+ req: IncomingMessage,
108
+ res: ServerResponse,
109
+ sectionName: string,
110
+ showcaseId: string
111
+ ): void {
112
+ const url = req.url || '';
113
+
114
+ // Route: GET /api/v1/tile
115
+ if (url.startsWith('/api/v1/tile')) {
116
+ handleGetTile(req, res, sectionName, showcaseId);
117
+ return;
118
+ }
119
+
120
+ // Route: POST /api/v1/custom_content
121
+ if (url.startsWith('/api/v1/custom_content')) {
122
+ handleUpdateCustomContent(req, res, sectionName, showcaseId);
123
+ return;
124
+ }
125
+
126
+ // Unknown API route
127
+ res.statusCode = 404;
128
+ res.setHeader('Content-Type', 'application/json');
129
+ setNoCacheHeaders(res);
130
+ res.end(JSON.stringify({
131
+ error: 'API route not found',
132
+ url: url
133
+ }));
134
+ }
135
+
@@ -376,3 +376,36 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
376
376
  mount('#app', state);
377
377
  }
378
378
  }
379
+
380
+ export async function getContentAndDesign(sectionName: string, showcaseId: string, distFolder:string) {
381
+ const content = await loadModule(`${distFolder}/sections/${sectionName}/js/settings/content.mjs`);
382
+ const contentTranslations = await loadModule(`${distFolder}/sections/${sectionName}/js/settings/translations.mjs`);
383
+ const showcaseTranslations = await loadModule(`${distFolder}/sections/${sectionName}/js/showcases/translations.mjs`);
384
+ const showcase = await loadModule(`${distFolder}/sections/${sectionName}/js/showcases/${showcaseId}.mjs`);
385
+ const design = await loadModule(`${distFolder}/sections/${sectionName}/js/settings/design.mjs`);
386
+
387
+ // Get showcase background and create background design
388
+ const showcaseBackground = showcase.default?.design?.background;
389
+ const backgroundDesign = createBackgroundDesign(showcaseBackground);
390
+
391
+ const ovveridenDesign = designTransformer(design.default, showcase.default.design || {});
392
+
393
+ const overriddenContent = getContentToRender(
394
+ content.default,
395
+ showcase.default.content || {},
396
+ contentTranslations.default.en,
397
+ showcaseTranslations.default.en,
398
+ sectionName
399
+ );
400
+
401
+ // Ensure background design always overrides any existing background
402
+ const finalDesign = {...ovveridenDesign};
403
+ finalDesign.background = backgroundDesign.background;
404
+
405
+ const state = {
406
+ content: overriddenContent,
407
+ design: finalDesign,
408
+ };
409
+
410
+ return state;
411
+ }
@@ -1,3 +1,170 @@
1
+ import { getContentAndDesign } from './preview';
2
+
3
+ // Hardcoded URLs and token
4
+ const TILES_URL = '';
5
+ const CUSTOM_CONTENT_URL = '';
6
+ const BEARER_TOKEN = '';
7
+
1
8
  export async function loadModule(path: string): Promise<any> {
2
- return import(/* @vite-ignore */ path);
9
+ // Add cache busting parameter with timestamp
10
+ const cacheBuster = `?t=${Date.now()}`;
11
+ const pathWithCacheBuster = path + cacheBuster;
12
+ return import(/* @vite-ignore */ pathWithCacheBuster);
13
+ }
14
+
15
+ /**
16
+ * Fetches tiles from the hardcoded URL using hardcoded bearer token
17
+ * @returns The tiles response object or null if failed
18
+ */
19
+ export async function fetchTiles(): Promise<any> {
20
+ try {
21
+ const response = await fetch(TILES_URL, {
22
+ method: 'GET',
23
+ headers: {
24
+ 'Authorization': BEARER_TOKEN
25
+ }
26
+ });
27
+
28
+ if (!response.ok) {
29
+ console.error('Failed to fetch tiles:', response.status, response.statusText);
30
+ return null;
31
+ }
32
+
33
+ const tilesResponse = await response.json();
34
+ return tilesResponse;
35
+
36
+ } catch (error) {
37
+ console.error('Error fetching tiles:', error);
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Updates the matching section in tiles with new content and design
44
+ * @param tilesResponse - The tiles response object
45
+ * @param sectionName - The section name to match (e.g., 'example-section')
46
+ * @param showcaseId - The showcase ID to match
47
+ * @param content - The new content to replace
48
+ * @param design - The new design to replace
49
+ * @returns The updated tiles response object
50
+ */
51
+ export function updateTilesSection(
52
+ tilesResponse: any,
53
+ sectionName: string,
54
+ showcaseId: string,
55
+ content: any,
56
+ design: any
57
+ ): any {
58
+ const targetPattern = `_${sectionName}_${showcaseId}`;
59
+
60
+ const targetItem = tilesResponse.tiles.find((item: any) => {
61
+ const itemId = item.sourceId;
62
+ if (itemId && typeof itemId === 'string') {
63
+ return itemId.includes(targetPattern);
64
+ }
65
+ return false;
66
+ });
67
+
68
+ if (targetItem) {
69
+ // Replace only content and design, preserve all other properties including tils
70
+ targetItem.content = content;
71
+ targetItem.design = design;
72
+ }
73
+
74
+ return tilesResponse;
75
+ }
76
+
77
+ /**
78
+ * Fetches tiles and returns the ID of a section matching the pattern
79
+ * @param sectionName - The section name to match (e.g., 'example-section')
80
+ * @param showcaseId - The showcase ID to match
81
+ * @returns The section ID or null if not found
82
+ */
83
+ export async function getSectionId(sectionName: string, showcaseId: string): Promise<string | null> {
84
+ try {
85
+ const tilesResponse = await fetchTiles();
86
+ if (!tilesResponse || !tilesResponse.tiles) {
87
+ return null;
88
+ }
89
+
90
+ const targetPattern = `_${sectionName}_${showcaseId}`;
91
+
92
+ const targetItem = tilesResponse.tiles.find((item: any) => {
93
+ const itemId = item.sourceId;
94
+ return itemId?.includes(targetPattern);
95
+ });
96
+
97
+ return targetItem ? targetItem.id : null;
98
+
99
+ } catch (error) {
100
+ console.error('Error getting section ID:', error);
101
+ return null;
102
+ }
103
+ }
104
+
105
+
106
+ /**
107
+ * Fetches custom content and updates the matching section with content and design from getContentAndDesign
108
+ * @param sectionName - The section name to match (e.g., 'example-section')
109
+ * @param showcaseId - The showcase ID to match
110
+ * @param distPath - The dist path for getContentAndDesign
111
+ * @returns The updated custom content response or null if failed
112
+ */
113
+ export async function updateCustomContent(
114
+ sectionName: string,
115
+ showcaseId: string,
116
+ distPath: string
117
+ ): Promise<any> {
118
+ try {
119
+ // First, get the section ID from tiles
120
+ const sectionId = await getSectionId(sectionName, showcaseId);
121
+
122
+ if (!sectionId) {
123
+ console.error('Section ID not found for:', sectionName, showcaseId);
124
+ return null;
125
+ }
126
+
127
+ // Fetch content and design using getContentAndDesign from preview.ts
128
+ const { content, design } = await getContentAndDesign(sectionName, showcaseId, distPath);
129
+
130
+ // Fetch custom content
131
+ const response = await fetch(CUSTOM_CONTENT_URL, {
132
+ method: 'GET',
133
+ headers: {
134
+ 'Authorization': BEARER_TOKEN
135
+ }
136
+ });
137
+
138
+ if (!response.ok) {
139
+ console.error('Failed to fetch custom content:', response.status, response.statusText);
140
+ return null;
141
+ }
142
+
143
+ const customContentResponse = await response.json();
144
+
145
+ if (!customContentResponse.customContent || !customContentResponse.customContent.sections) {
146
+ console.error('Invalid custom content response structure');
147
+ return null;
148
+ }
149
+
150
+ // Find the section by ID
151
+ const targetSection = customContentResponse.customContent.sections.find((section: any) => {
152
+ return section.state.data.tileId === sectionId;
153
+ });
154
+
155
+ if (!targetSection) {
156
+ console.error('Section not found in custom content with ID:', sectionId);
157
+ return null;
158
+ }
159
+
160
+ // Update content and design
161
+ targetSection.state.data.content = JSON.stringify(content);
162
+ targetSection.state.data.design = JSON.stringify(design);
163
+
164
+ return customContentResponse;
165
+
166
+ } catch (error) {
167
+ console.error('Error updating custom content:', error);
168
+ return null;
169
+ }
3
170
  }
@@ -1,11 +1,22 @@
1
1
  import {defineConfig} from 'vite';
2
2
  import path from "path";
3
3
  import fs from "fs";
4
+ import { handleApiRequest } from './shared/api-routes.js';
5
+
6
+ // Hardcoded configuration
7
+ const SECTION_NAME = 'example-section';
8
+ const SHOWCASE_ID = '1';
4
9
 
5
10
  export default defineConfig({
6
11
  root: '.',
7
12
  server: {
8
13
  fs: {strict: false},
14
+ cors: true,
15
+ headers: {
16
+ 'Access-Control-Allow-Origin': '*',
17
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
18
+ 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
19
+ },
9
20
  },
10
21
  plugins: [
11
22
  {
@@ -44,9 +55,29 @@ export default defineConfig({
44
55
  server.middlewares.use((req, res, next) => {
45
56
  const url = req.url || ''
46
57
 
47
- // cache-bust for any /sections/ URL
48
- if (url.includes('/sections/')) {
49
- server.moduleGraph.invalidateAll()
58
+ // Skip HMR and internal Vite requests
59
+ if (url.startsWith('/@') || url.includes('__vite') || url.includes('.hot-update.')) {
60
+ return next()
61
+ }
62
+
63
+ // Add CORS headers and disable caching for our API responses
64
+ if (url.startsWith('/api/') || url.startsWith('/chosen-section')) {
65
+ res.setHeader('Access-Control-Allow-Origin', '*')
66
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
67
+ res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
68
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate')
69
+ res.setHeader('Pragma', 'no-cache')
70
+ res.setHeader('Expires', '0')
71
+ }
72
+
73
+ // Handle preflight OPTIONS requests
74
+ if (req.method === 'OPTIONS') {
75
+ res.statusCode = 200
76
+ return res.end()
77
+ }
78
+
79
+ // Let Vite handle all non-API requests for proper HMR
80
+ if (!url.startsWith('/api/') && !url.startsWith('/chosen-section')) {
50
81
  return next()
51
82
  }
52
83
 
@@ -64,6 +95,18 @@ export default defineConfig({
64
95
  }))
65
96
  }
66
97
 
98
+ // Handle API routes
99
+ if (url.startsWith('/api/v1/')) {
100
+ const sectionName = SECTION_NAME;
101
+ const showcaseId = SHOWCASE_ID;
102
+
103
+ // Delegate to API router
104
+ (async () => {
105
+ await handleApiRequest(req, res, sectionName, showcaseId);
106
+ })();
107
+ return; // Exit early for async handling
108
+ }
109
+
67
110
  // all other requests
68
111
  next()
69
112
  }