@lightspeed/crane 1.4.1 → 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.
Files changed (116) hide show
  1. package/CHANGELOG.md +51 -4
  2. package/UPGRADE.md +19 -0
  3. package/dist/app.d.mts +1 -1028
  4. package/dist/app.d.ts +1 -1028
  5. package/dist/app.mjs +1 -1
  6. package/dist/cli.mjs +20 -7
  7. package/package.json +6 -3
  8. package/template/footers/example-footer/ExampleFooter.vue +1 -1
  9. package/template/footers/example-footer/client.ts +1 -1
  10. package/template/footers/example-footer/component/LegalLinks.vue +1 -1
  11. package/template/footers/example-footer/component/MadeWith.vue +1 -1
  12. package/template/footers/example-footer/component/ReportAbuse.vue +1 -1
  13. package/template/footers/example-footer/entity/color.ts +2 -2
  14. package/template/footers/example-footer/server.ts +1 -1
  15. package/template/headers/example-header/client.ts +1 -1
  16. package/template/headers/example-header/component/Account.vue +1 -1
  17. package/template/headers/example-header/component/Cart.vue +1 -1
  18. package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
  19. package/template/headers/example-header/component/Logo.vue +1 -1
  20. package/template/headers/example-header/component/NavigationMenu.vue +1 -1
  21. package/template/headers/example-header/component/SearchForm.vue +1 -1
  22. package/template/headers/example-header/server.ts +1 -1
  23. package/template/index.d.ts +1 -1
  24. package/template/layouts/catalog/example-catalog/Main.vue +1 -1
  25. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/client.ts +1 -1
  26. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +1 -1
  27. package/template/layouts/category/example-category/Main.vue +1 -1
  28. package/template/layouts/category/example-category/settings/content.ts +1 -1
  29. package/template/layouts/category/example-category/settings/design.ts +1 -1
  30. package/template/layouts/product/example-product/Main.vue +1 -1
  31. package/template/layouts/product/example-product/settings/content.ts +1 -1
  32. package/template/layouts/product/example-product/settings/design.ts +1 -1
  33. package/template/package.json +7 -3
  34. package/template/page-templates/example-template/pages/catalog.ts +1 -1
  35. package/template/page-templates/example-template/pages/category.ts +1 -1
  36. package/template/page-templates/example-template/pages/product.ts +1 -1
  37. package/template/preview/sections/preview.html +1 -1
  38. package/template/preview/shared/api-routes.ts +443 -0
  39. package/template/preview/shared/mock.ts +43 -41
  40. package/template/preview/shared/preview.ts +213 -101
  41. package/template/preview/shared/utils.ts +315 -2
  42. package/template/preview/ssr-server.ts +429 -0
  43. package/template/preview/vite.config.js +76 -34
  44. package/template/reference/sections/about-us/AboutUs.vue +1 -1
  45. package/template/reference/sections/about-us/client.ts +1 -1
  46. package/template/reference/sections/about-us/component/Image.vue +1 -1
  47. package/template/reference/sections/about-us/component/Stats.vue +2 -2
  48. package/template/reference/sections/about-us/component/Title.vue +1 -1
  49. package/template/reference/sections/about-us/server.ts +1 -1
  50. package/template/reference/sections/about-us/util/visibility-provider.ts +1 -1
  51. package/template/reference/sections/featured-products/FeaturedProducts.vue +65 -0
  52. package/template/reference/sections/featured-products/assets/arrow.svg +3 -0
  53. package/template/reference/sections/featured-products/assets/custom_section_showcase_1_preview.png +0 -0
  54. package/template/reference/sections/featured-products/client.ts +5 -0
  55. package/template/reference/sections/featured-products/component/ProductItem.vue +71 -0
  56. package/template/reference/sections/featured-products/component/Title.vue +31 -0
  57. package/template/reference/sections/featured-products/entity/color.ts +4 -0
  58. package/template/reference/sections/featured-products/server.ts +5 -0
  59. package/template/reference/sections/featured-products/settings/content.ts +14 -0
  60. package/template/reference/sections/featured-products/settings/design.ts +33 -0
  61. package/template/reference/sections/featured-products/settings/translations.ts +24 -0
  62. package/template/reference/sections/featured-products/showcases/1.ts +28 -0
  63. package/template/reference/sections/featured-products/showcases/translations.ts +16 -0
  64. package/template/reference/sections/featured-products/type.ts +5 -0
  65. package/template/reference/sections/intro-slider/IntroSlider.vue +1 -1
  66. package/template/reference/sections/intro-slider/client.ts +1 -1
  67. package/template/reference/sections/intro-slider/component/Slider.vue +8 -2
  68. package/template/reference/sections/intro-slider/component/Title.vue +1 -1
  69. package/template/reference/sections/intro-slider/entity/color.ts +2 -2
  70. package/template/reference/sections/intro-slider/server.ts +1 -1
  71. package/template/reference/sections/tag-lines/TagLines.vue +1 -1
  72. package/template/reference/sections/tag-lines/client.ts +1 -1
  73. package/template/reference/sections/tag-lines/component/SectionImage.vue +1 -1
  74. package/template/reference/sections/tag-lines/component/Title.vue +1 -1
  75. package/template/reference/sections/tag-lines/composables/highlighted-text-image-list.ts +2 -2
  76. package/template/reference/sections/tag-lines/server.ts +1 -1
  77. package/template/reference/sections/trending-categories/TrendingCategories.vue +70 -0
  78. package/template/reference/sections/trending-categories/assets/arrow.svg +3 -0
  79. package/template/reference/sections/trending-categories/assets/custom_section_showcase_1_preview.png +0 -0
  80. package/template/reference/sections/trending-categories/client.ts +5 -0
  81. package/template/reference/sections/trending-categories/component/CategoryItem.vue +62 -0
  82. package/template/reference/sections/trending-categories/component/Title.vue +32 -0
  83. package/template/reference/sections/trending-categories/entity/color.ts +4 -0
  84. package/template/reference/sections/trending-categories/server.ts +5 -0
  85. package/template/reference/sections/trending-categories/settings/content.ts +14 -0
  86. package/template/reference/sections/trending-categories/settings/design.ts +33 -0
  87. package/template/reference/sections/trending-categories/settings/translations.ts +24 -0
  88. package/template/reference/sections/trending-categories/showcases/1.ts +36 -0
  89. package/template/reference/sections/trending-categories/showcases/translations.ts +22 -0
  90. package/template/reference/sections/trending-categories/type.ts +5 -0
  91. package/template/reference/shared/components/Button.vue +1 -1
  92. package/template/reference/templates/reference-template-apparel/pages/catalog.ts +1 -1
  93. package/template/reference/templates/reference-template-apparel/pages/category.ts +1 -1
  94. package/template/reference/templates/reference-template-apparel/pages/home.ts +10 -0
  95. package/template/reference/templates/reference-template-apparel/pages/product.ts +1 -1
  96. package/template/reference/templates/reference-template-bike/pages/catalog.ts +1 -1
  97. package/template/reference/templates/reference-template-bike/pages/category.ts +1 -1
  98. package/template/reference/templates/reference-template-bike/pages/home.ts +10 -0
  99. package/template/reference/templates/reference-template-bike/pages/product.ts +1 -1
  100. package/template/sections/example-section/ExampleSection.vue +8 -1
  101. package/template/sections/example-section/client.ts +1 -1
  102. package/template/sections/example-section/component/button/Button.vue +1 -1
  103. package/template/sections/example-section/component/image/Image.vue +1 -1
  104. package/template/sections/example-section/component/image/ImagesGrid.vue +1 -1
  105. package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
  106. package/template/sections/example-section/component/title/Title.vue +1 -1
  107. package/template/sections/example-section/component/toggle/Toggle.vue +1 -1
  108. package/template/sections/example-section/entity/color.ts +2 -2
  109. package/template/sections/example-section/server.ts +1 -1
  110. package/template/sections/example-section/settings/translations.ts +1 -1
  111. package/template/sections/example-section/showcases/translations.ts +13 -13
  112. package/template/shared/components/LanguageSelector.vue +1 -1
  113. package/template/shared/translation.ts +16 -0
  114. package/template/shared/utils.ts +3 -1
  115. package/template/tsconfig.json +1 -0
  116. package/types.d.ts +6 -457
@@ -0,0 +1,429 @@
1
+ /**
2
+ * SSR Server - Standalone Node.js server for executing server.js modules
3
+ * Runs on a separate port, completely outside of Vite
4
+ * Uses Blockbuster's approach: @swc/core for parsing + importFromString for execution
5
+ */
6
+
7
+ import * as http from 'http';
8
+ import { readFileSync } from 'fs';
9
+ import { parse } from '@swc/core';
10
+ import { importFromString } from 'module-from-string';
11
+ import { parseHTML } from 'linkedom';
12
+
13
+ const PREVIEW_SSR_PORT = process.env.PREVIEW_SSR_PORT ? parseInt(process.env.PREVIEW_SSR_PORT, 10) : 3001;
14
+ const EXTERNAL_IMPORTS_BLOCK_START = '/* EXTERNAL_IMPORTS_START */';
15
+ const EXTERNAL_IMPORTS_BLOCK_END = '/* EXTERNAL_IMPORTS_END */';
16
+
17
+ interface GlobalDesign {
18
+ color: {
19
+ title: { hex: string };
20
+ body: { hex: string };
21
+ button: { hex: string };
22
+ link: { hex: string };
23
+ background: { hex: string };
24
+ };
25
+ }
26
+
27
+ interface RenderContext {
28
+ externalizationEnabled: boolean;
29
+ globalDesign?: GlobalDesign;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ interface RenderResult {
34
+ html: string;
35
+ }
36
+
37
+ interface ServerModule {
38
+ init(): {
39
+ render: (context: RenderContext, data: unknown) => Promise<RenderResult>;
40
+ };
41
+ default?: unknown;
42
+ }
43
+
44
+ interface ImportSpecifier {
45
+ local: {
46
+ value: string;
47
+ };
48
+ }
49
+
50
+ interface ImportDeclaration {
51
+ type: string;
52
+ source: {
53
+ value: string;
54
+ };
55
+ specifiers: ImportSpecifier[];
56
+ }
57
+
58
+ interface ParsedModule {
59
+ body: ImportDeclaration[];
60
+ }
61
+
62
+ interface ExternalImportsResult {
63
+ transformedModuleData: string;
64
+ externalModulesAsGlobals: Record<string, unknown>;
65
+ }
66
+
67
+ interface RenderRequest {
68
+ modulePath: string;
69
+ context?: RenderContext;
70
+ data?: unknown;
71
+ }
72
+
73
+ interface MockWindowInstantSite {
74
+ getSiteId: () => void;
75
+ getAppPublicToken: () => void;
76
+ getAppPublicConfig: () => void;
77
+ openSearchPage: () => void;
78
+ onTileLoaded: { add: () => void };
79
+ onTileUnloaded: { add: () => void };
80
+ }
81
+
82
+ interface MockWindowEcwid {
83
+ getOwnerId: (storeId?: number) => number;
84
+ formatCurrency: () => void;
85
+ getAppPublicToken: () => void;
86
+ getAppPublicConfig: () => void;
87
+ getInitializedWidgets: () => void;
88
+ getTrackingConsent: () => void;
89
+ getStorefrontLang: () => void;
90
+ getVisitorLocation: () => void;
91
+ openPage: () => void;
92
+ setTrackingConsent: () => void;
93
+ OnApiLoaded: { add: () => void };
94
+ OnCartChanged: { add: () => void };
95
+ OnConsentChanged: { add: () => void };
96
+ OnPageLoad: { add: () => void };
97
+ OnPageLoaded: { add: () => void };
98
+ OnPageSwitch: { add: () => void };
99
+ OnSetProfile: { add: () => void };
100
+ Customer: {
101
+ get: () => void;
102
+ signOut: () => void;
103
+ };
104
+ Cart: {
105
+ addProduct: () => void;
106
+ clear: () => void;
107
+ get: () => void;
108
+ removeProduct: () => void;
109
+ removeProducts: () => void;
110
+ calculateTotal: () => void;
111
+ };
112
+ }
113
+
114
+ interface MockWindow {
115
+ instantsite: MockWindowInstantSite;
116
+ Ecwid: MockWindowEcwid;
117
+ [key: string]: unknown;
118
+ }
119
+
120
+ // Minimal default global design to mimic Blockbuster defaults when not provided
121
+ const DEFAULT_GLOBAL_DESIGN: GlobalDesign = {
122
+ color: {
123
+ title: { hex: '#191919ff' },
124
+ body: { hex: '#191919ff' },
125
+ button: { hex: '#191919ff' },
126
+ link: { hex: '#1a7ac4ff' },
127
+ background: { hex: '#f4f4f4ff' },
128
+ },
129
+ };
130
+
131
+ // ============================================================================
132
+ // Mock Window Setup (matching Blockbuster)
133
+ // ============================================================================
134
+
135
+ const mockFn = () => {};
136
+ const mockEcwidApiPromise = { add: mockFn };
137
+
138
+ function createMockInstantSite() {
139
+ return {
140
+ getSiteId: mockFn,
141
+ getAppPublicToken: mockFn,
142
+ getAppPublicConfig: mockFn,
143
+ openSearchPage: mockFn,
144
+ onTileLoaded: mockEcwidApiPromise,
145
+ onTileUnloaded: mockEcwidApiPromise,
146
+ };
147
+ }
148
+
149
+ function createMockEcwid(storeId?: number) {
150
+ return {
151
+ // defaults to 0 to prevent @lightspeed/ecom-headless initialization errors
152
+ getOwnerId: () => storeId ?? 0,
153
+ formatCurrency: mockFn,
154
+ getAppPublicToken: mockFn,
155
+ getAppPublicConfig: mockFn,
156
+ getInitializedWidgets: mockFn,
157
+ getTrackingConsent: mockFn,
158
+ getStorefrontLang: mockFn,
159
+ getVisitorLocation: mockFn,
160
+ openPage: mockFn,
161
+ setTrackingConsent: mockFn,
162
+ OnApiLoaded: mockEcwidApiPromise,
163
+ OnCartChanged: mockEcwidApiPromise,
164
+ OnConsentChanged: mockEcwidApiPromise,
165
+ OnPageLoad: mockEcwidApiPromise,
166
+ OnPageLoaded: mockEcwidApiPromise,
167
+ OnPageSwitch: mockEcwidApiPromise,
168
+ OnSetProfile: mockEcwidApiPromise,
169
+ Customer: {
170
+ get: mockFn,
171
+ signOut: mockFn,
172
+ },
173
+ Cart: {
174
+ addProduct: mockFn,
175
+ clear: mockFn,
176
+ get: mockFn,
177
+ removeProduct: mockFn,
178
+ removeProducts: mockFn,
179
+ calculateTotal: mockFn,
180
+ },
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Creates a mock window object for SSR using linkedom,
186
+ * including mocked `instantsite` and `Ecwid` objects to prevent errors
187
+ * from server-side code that references them (matching Blockbuster).
188
+ */
189
+ function createMockWindow(storeId?: number): MockWindow {
190
+ const { window } = parseHTML('');
191
+ const mockWindow = window as unknown as MockWindow;
192
+
193
+ mockWindow.instantsite = createMockInstantSite();
194
+ mockWindow.Ecwid = createMockEcwid(storeId);
195
+
196
+ return mockWindow;
197
+ }
198
+
199
+ // ============================================================================
200
+ // External Imports Handling (Blockbuster approach)
201
+ // ============================================================================
202
+
203
+ function getExternalModuleName(source: string): string {
204
+ // Remove \0 prefix and ?commonjs-external suffix
205
+ // e.g., "\0@vue/compiler-dom?commonjs-external" -> "@vue/compiler-dom"
206
+ const suffix = '?commonjs-external';
207
+ return source.split(suffix)[0].slice(1);
208
+ }
209
+
210
+ function getModuleImportIdentifier(specifiers: ImportSpecifier[]): string {
211
+ // Extract the local variable names from import specifiers
212
+ // e.g., [{ local: { value: 'e' } }] -> 'e'
213
+ return specifiers.map(({ local }) => local.value).join(', ');
214
+ }
215
+
216
+ async function transformExternalModulesToGlobals(body: ImportDeclaration[]): Promise<Record<string, unknown>> {
217
+ // Parse the AST and extract import declarations
218
+ const globals: Record<string, unknown> = {};
219
+
220
+ const importPromises = body.map(async (node) => {
221
+ if (node.type === 'ImportDeclaration') {
222
+ const { source, specifiers } = node;
223
+ const moduleName = getExternalModuleName(source.value);
224
+ const importIdentifier = getModuleImportIdentifier(specifiers);
225
+
226
+ // Import the module and add it to globals with the variable name
227
+ try {
228
+ globals[importIdentifier] = await import(moduleName);
229
+ } catch (error: unknown) {
230
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
231
+ console.warn(`Failed to import ${moduleName}:`, errorMessage);
232
+ globals[importIdentifier] = {};
233
+ }
234
+ }
235
+ });
236
+
237
+ await Promise.all(importPromises);
238
+
239
+ return globals;
240
+ }
241
+
242
+ async function replaceExternalImportsWithGlobals(fullModuleData: string): Promise<ExternalImportsResult> {
243
+ // Find the EXTERNAL_IMPORTS block
244
+ const externalImportsStart = fullModuleData.indexOf(EXTERNAL_IMPORTS_BLOCK_START);
245
+ const externalImportsEnd = fullModuleData.indexOf(EXTERNAL_IMPORTS_BLOCK_END);
246
+
247
+ if (externalImportsStart === -1 || externalImportsEnd === -1) {
248
+ return {
249
+ transformedModuleData: fullModuleData,
250
+ externalModulesAsGlobals: {},
251
+ };
252
+ }
253
+
254
+ // Extract the external imports code block
255
+ const externalImportsCode = fullModuleData.slice(externalImportsStart, externalImportsEnd);
256
+ const remainingModuleCode = fullModuleData.slice(externalImportsEnd + EXTERNAL_IMPORTS_BLOCK_END.length);
257
+
258
+ // Parse the imports code with @swc/core
259
+ try {
260
+ const parsed = (await parse(externalImportsCode)) as ParsedModule;
261
+ const { body } = parsed;
262
+ const externalModulesAsGlobals = await transformExternalModulesToGlobals(body);
263
+
264
+ return {
265
+ transformedModuleData: remainingModuleCode,
266
+ externalModulesAsGlobals,
267
+ };
268
+ } catch (error: unknown) {
269
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
270
+ console.warn('Failed to parse external imports:', errorMessage);
271
+ return {
272
+ transformedModuleData: remainingModuleCode,
273
+ externalModulesAsGlobals: {},
274
+ };
275
+ }
276
+ }
277
+
278
+ // ============================================================================
279
+ // Globals Creation
280
+ // ============================================================================
281
+
282
+ function createGlobals(window: unknown): Record<string, unknown> {
283
+ // Create base globals for SSR execution
284
+ // With useCurrentGlobal: true, window properties (document, location, etc.)
285
+ // will be accessible from the current global context
286
+ return {
287
+ window,
288
+ fetch,
289
+ TextEncoder,
290
+ TextDecoder,
291
+ URLSearchParams,
292
+ };
293
+ }
294
+
295
+ async function loadServerModule(modulePath: string, storeId?: number): Promise<ServerModule> {
296
+ try {
297
+ // Read the file from disk
298
+ const moduleData = readFileSync(modulePath, 'utf8');
299
+
300
+ // Create mock window with storeId (matching Blockbuster)
301
+ const mockWindow = createMockWindow(storeId);
302
+ const baseGlobals = createGlobals(mockWindow);
303
+
304
+ // Set global document and window for Vue setup functions
305
+ // This ensures that when Vue components' setup functions run, they can access document
306
+ const globalThisWithProps = globalThis as Record<string, unknown>;
307
+ const previousGlobalDocument = globalThisWithProps.document;
308
+ const previousGlobalWindow = globalThisWithProps.window;
309
+ globalThisWithProps.document = (mockWindow as Record<string, unknown>).document;
310
+ globalThisWithProps.window = mockWindow;
311
+
312
+ try {
313
+ // Replace external imports with globals (Blockbuster approach)
314
+ const { transformedModuleData, externalModulesAsGlobals }
315
+ = await replaceExternalImportsWithGlobals(moduleData);
316
+
317
+ // Combine base globals with external modules
318
+ const globals = {
319
+ ...externalModulesAsGlobals,
320
+ ...baseGlobals,
321
+ };
322
+
323
+ // Execute the module with importFromString (Blockbuster approach)
324
+ // Use useCurrentGlobal: true to avoid property descriptor conflicts with window properties
325
+ const module = await importFromString(transformedModuleData, { globals, useCurrentGlobal: true });
326
+
327
+ // Return the module exports
328
+ return module.default || module;
329
+ } finally {
330
+ // Restore previous global state
331
+ globalThisWithProps.document = previousGlobalDocument;
332
+ globalThisWithProps.window = previousGlobalWindow;
333
+ }
334
+ } catch (error: unknown) {
335
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
336
+ console.error('Error loading server module:', errorMessage);
337
+ throw error;
338
+ }
339
+ }
340
+
341
+ // ============================================================================
342
+ // HTTP Server
343
+ // ============================================================================
344
+
345
+ const server = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
346
+ // Enable CORS
347
+ res.setHeader('Access-Control-Allow-Origin', '*');
348
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
349
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
350
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
351
+
352
+ // Handle preflight
353
+ if (req.method === 'OPTIONS') {
354
+ res.statusCode = 200;
355
+ res.end();
356
+ return;
357
+ }
358
+
359
+ // POST /load-server-module - Load module and return HTML
360
+ if (req.method === 'POST' && req.url === '/load-server-module') {
361
+ let body = '';
362
+ req.on('data', (chunk) => {
363
+ body += chunk.toString();
364
+ });
365
+
366
+ req.on('end', async () => {
367
+ try {
368
+ const { modulePath, context: rawContext, data } = JSON.parse(body) as RenderRequest;
369
+
370
+ if (!modulePath) {
371
+ res.statusCode = 400;
372
+ res.setHeader('Content-Type', 'application/json');
373
+ res.end(JSON.stringify({ error: 'modulePath is required' }));
374
+ return;
375
+ }
376
+
377
+ // Normalize context
378
+ const context: RenderContext = {
379
+ externalizationEnabled: true,
380
+ ...rawContext,
381
+ };
382
+ if (!context.globalDesign || !context.globalDesign.color) {
383
+ context.globalDesign = DEFAULT_GLOBAL_DESIGN;
384
+ }
385
+
386
+ // Load the server module
387
+ const serverModule = await loadServerModule(modulePath);
388
+
389
+ // Call init() to get the render function
390
+ const { render } = serverModule.init();
391
+
392
+ // Call render with context and data
393
+ const result = await render(context, data || { defaults: {}, background: {}, externalContent: {} });
394
+
395
+ res.statusCode = 200;
396
+ res.setHeader('Content-Type', 'application/json');
397
+ res.end(JSON.stringify({ success: true, html: result.html }));
398
+ } catch (error: unknown) {
399
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
400
+ console.error('Error loading server module:', error);
401
+ res.statusCode = 500;
402
+ res.setHeader('Content-Type', 'application/json');
403
+ res.end(JSON.stringify({
404
+ error: 'Failed to load server module',
405
+ message: errorMessage,
406
+ }));
407
+ }
408
+ });
409
+ return;
410
+ }
411
+
412
+ // 404
413
+ res.statusCode = 404;
414
+ res.setHeader('Content-Type', 'application/json');
415
+ res.end(JSON.stringify({ error: 'Not found' }));
416
+ });
417
+
418
+ /**
419
+ * Start the SSR server and return a promise that resolves when it's listening
420
+ */
421
+ export function startServer(): Promise<{ server: http.Server; port: number }> {
422
+ return new Promise((resolve) => {
423
+ server.listen(PREVIEW_SSR_PORT, () => {
424
+ const actualPort = (server.address() as { port: number }).port;
425
+ console.log(`🚀 SSR Server running on http://localhost:${actualPort}`);
426
+ resolve({ server, port: actualPort });
427
+ });
428
+ });
429
+ }
@@ -1,11 +1,18 @@
1
- import {defineConfig} from 'vite';
2
- import path from "path";
3
- import fs from "fs";
1
+ import { defineConfig } from 'vite';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { handleApiRequest } from './shared/api-routes.js';
4
5
 
5
6
  export default defineConfig({
6
7
  root: '.',
7
8
  server: {
8
- fs: {strict: false},
9
+ fs: { strict: false },
10
+ cors: true,
11
+ headers: {
12
+ 'Access-Control-Allow-Origin': '*',
13
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
14
+ 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
15
+ },
9
16
  },
10
17
  plugins: [
11
18
  {
@@ -13,7 +20,7 @@ export default defineConfig({
13
20
  configureServer(server) {
14
21
  server.httpServer?.once('listening', () => {
15
22
  const baseUrl = `http://localhost:${server.config.server.port || 5173}`;
16
- console.log("Preview URL: ", baseUrl + '/preview/sections/preview.html\n');
23
+ console.log('Preview URL: ', baseUrl + '/preview/sections/preview.html\n');
17
24
  console.log('🛑 Press Ctrl+C to stop the local server 🛑\n');
18
25
  });
19
26
  },
@@ -22,51 +29,86 @@ export default defineConfig({
22
29
  name: 'no-vite-cache',
23
30
  configureServer(server) {
24
31
  // use process.cwd() for analytics path
25
- const analyticsDir = path.resolve(process.cwd(), 'preview', 'shared')
26
- const analyticsFile = path.resolve(analyticsDir, 'analytics.json')
32
+ const analyticsDir = path.resolve(process.cwd(), 'preview', 'shared');
33
+ const analyticsFile = path.resolve(analyticsDir, 'analytics.json');
27
34
 
28
35
  // ensure analytics folder & file exist
29
- fs.mkdirSync(analyticsDir, {recursive: true})
36
+ fs.mkdirSync(analyticsDir, { recursive: true });
30
37
  if (!fs.existsSync(analyticsFile)) {
31
- fs.writeFileSync(analyticsFile, JSON.stringify({}, null, 2), 'utf-8')
38
+ fs.writeFileSync(analyticsFile, JSON.stringify({}, null, 2), 'utf-8');
32
39
  }
33
40
 
34
41
  // helper to bump and persist counter for a given section
35
42
  function bumpSectionCount(section) {
36
- const raw = fs.readFileSync(analyticsFile, 'utf-8')
37
- const data = JSON.parse(raw)
38
- const next = (data[section] || 0) + 1
39
- data[section] = next
40
- fs.writeFileSync(analyticsFile, JSON.stringify(data, null, 2), 'utf-8')
41
- return next
43
+ const raw = fs.readFileSync(analyticsFile, 'utf-8');
44
+ const data = JSON.parse(raw);
45
+ const next = (data[section] || 0) + 1;
46
+ data[section] = next;
47
+ fs.writeFileSync(analyticsFile, JSON.stringify(data, null, 2), 'utf-8');
48
+ return next;
42
49
  }
43
50
 
44
51
  server.middlewares.use((req, res, next) => {
45
- const url = req.url || ''
52
+ // Invalidate all caches so we fetch most recent sections data
53
+ server.moduleGraph.invalidateAll();
54
+
55
+ const url = req.url || '';
56
+
57
+ // Skip HMR and internal Vite requests
58
+ if (url.startsWith('/@') || url.includes('__vite') || url.includes('.hot-update.')) {
59
+ return next();
60
+ }
61
+
62
+ // Add CORS headers and disable caching for our API responses
63
+ if (url.startsWith('/api/') || url.startsWith('/chosen-section')) {
64
+ res.setHeader('Access-Control-Allow-Origin', '*');
65
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
66
+ res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
67
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
68
+ res.setHeader('Pragma', 'no-cache');
69
+ res.setHeader('Expires', '0');
70
+ }
46
71
 
47
- // cache-bust for any /sections/ URL
48
- if (url.includes('/sections/')) {
49
- server.moduleGraph.invalidateAll()
50
- return next()
51
- }
72
+ // Handle preflight OPTIONS requests
73
+ if (req.method === 'OPTIONS') {
74
+ res.statusCode = 200;
75
+ return res.end();
76
+ }
77
+
78
+ // Let Vite handle all non-API requests for proper HMR
79
+ if (!url.startsWith('/api/') && !url.startsWith('/chosen-section')) {
80
+ return next();
81
+ }
52
82
 
53
- if (url.startsWith('/chosen-section')) {
54
- // extract "sectionName" from "/chosen-section/<sectionName>"
83
+ if (url.startsWith('/chosen-section')) {
84
+ // extract "sectionName" from "/chosen-section/<sectionName>"
55
85
 
56
- const sectionName = req.url.replace(/\/$/, '').split('/').filter(Boolean).pop()
57
- const newCount = bumpSectionCount(sectionName)
86
+ const sectionName = req.url.replace(/\/$/, '').split('/').filter(Boolean).pop();
87
+ const newCount = bumpSectionCount(sectionName);
58
88
 
59
- res.statusCode = 200
60
- res.setHeader('Content-Type', 'application/json')
61
- return res.end(JSON.stringify({
62
- section: sectionName,
63
- count: newCount
64
- }))
65
- }
89
+ res.statusCode = 200;
90
+ res.setHeader('Content-Type', 'application/json');
91
+ return res.end(JSON.stringify({
92
+ section: sectionName,
93
+ count: newCount,
94
+ }));
95
+ }
96
+
97
+ // Handle API routes
98
+ if (url.startsWith('/api/v1/')) {
99
+ // Extract auth header from request
100
+ const authHeader = req.headers['authorization'] || '';
66
101
 
67
- // all other requests
68
- next()
102
+ // Delegate to API router
103
+ (async () => {
104
+ await handleApiRequest(req, res, authHeader);
105
+ })();
106
+ return; // Exit early for async handling
69
107
  }
108
+
109
+ // all other requests
110
+ next();
111
+ },
70
112
  );
71
113
  },
72
114
  },
@@ -15,7 +15,7 @@
15
15
  </template>
16
16
 
17
17
  <script setup lang="ts">
18
- import { useBackgroundElementDesign } from '@lightspeed/crane';
18
+ import { useBackgroundElementDesign } from '@lightspeed/crane-api';
19
19
  import { computed, CSSProperties } from 'vue';
20
20
 
21
21
  import Image from './component/Image.vue';
@@ -1,4 +1,4 @@
1
- import { createVueClientApp } from '@lightspeed/crane';
1
+ import { createVueClientApp } from '@lightspeed/crane-api';
2
2
 
3
3
  import ExampleSection from './AboutUs.vue';
4
4
  import type { Content, Design } from './type.ts';
@@ -20,7 +20,7 @@ import {
20
20
  useImageElementContent,
21
21
  useImageElementDesign,
22
22
  useLayoutElementDesign,
23
- } from '@lightspeed/crane';
23
+ } from '@lightspeed/crane-api';
24
24
  import { computed, CSSProperties } from 'vue';
25
25
 
26
26
  import type { Content, Design } from '../type.ts';
@@ -30,14 +30,14 @@
30
30
  </template>
31
31
 
32
32
  <script setup lang="ts">
33
- import type { Card, InputBoxContent, ButtonContent } from '@lightspeed/crane';
33
+ import type { Card, InputBoxContent, ButtonContent } from '@lightspeed/crane-api';
34
34
  import {
35
35
  useTextElementDesign,
36
36
  EditorTypes,
37
37
  useDeckElementContent,
38
38
  useButtonElementContent,
39
39
  useButtonElementDesign,
40
- } from '@lightspeed/crane';
40
+ } from '@lightspeed/crane-api';
41
41
  import { computed, CSSProperties } from 'vue';
42
42
 
43
43
  import type { Content, Design } from '../type.ts';
@@ -8,7 +8,7 @@
8
8
  </template>
9
9
 
10
10
  <script setup lang="ts">
11
- import { useInputboxElementContent, useTextElementDesign } from '@lightspeed/crane';
11
+ import { useInputboxElementContent, useTextElementDesign } from '@lightspeed/crane-api';
12
12
  import { computed, CSSProperties } from 'vue';
13
13
 
14
14
  import type { Content, Design } from '../type.ts';
@@ -1,4 +1,4 @@
1
- import { createVueServerApp } from '@lightspeed/crane';
1
+ import { createVueServerApp } from '@lightspeed/crane-api';
2
2
 
3
3
  import ExampleSection from './AboutUs.vue';
4
4
  import type { Content, Design } from './type.ts';
@@ -4,7 +4,7 @@ import {
4
4
  useImageElementDesign,
5
5
  useInputboxElementContent,
6
6
  useTextElementDesign,
7
- } from '@lightspeed/crane';
7
+ } from '@lightspeed/crane-api';
8
8
  import { computed } from 'vue';
9
9
 
10
10
  import type { Content, Design } from '../type.ts';