@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +33 -0
  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 +3 -2
  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 +6 -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 +347 -39
  39. package/template/preview/shared/mock.ts +43 -41
  40. package/template/preview/shared/preview.ts +205 -126
  41. package/template/preview/shared/utils.ts +208 -62
  42. package/template/preview/ssr-server.ts +429 -0
  43. package/template/preview/vite.config.js +64 -65
  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,16 +1,12 @@
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
4
  import { handleApiRequest } from './shared/api-routes.js';
5
5
 
6
- // Hardcoded configuration
7
- const SECTION_NAME = 'example-section';
8
- const SHOWCASE_ID = '1';
9
-
10
6
  export default defineConfig({
11
7
  root: '.',
12
8
  server: {
13
- fs: {strict: false},
9
+ fs: { strict: false },
14
10
  cors: true,
15
11
  headers: {
16
12
  'Access-Control-Allow-Origin': '*',
@@ -24,7 +20,7 @@ export default defineConfig({
24
20
  configureServer(server) {
25
21
  server.httpServer?.once('listening', () => {
26
22
  const baseUrl = `http://localhost:${server.config.server.port || 5173}`;
27
- console.log("Preview URL: ", baseUrl + '/preview/sections/preview.html\n');
23
+ console.log('Preview URL: ', baseUrl + '/preview/sections/preview.html\n');
28
24
  console.log('🛑 Press Ctrl+C to stop the local server 🛑\n');
29
25
  });
30
26
  },
@@ -33,83 +29,86 @@ export default defineConfig({
33
29
  name: 'no-vite-cache',
34
30
  configureServer(server) {
35
31
  // use process.cwd() for analytics path
36
- const analyticsDir = path.resolve(process.cwd(), 'preview', 'shared')
37
- const analyticsFile = path.resolve(analyticsDir, 'analytics.json')
32
+ const analyticsDir = path.resolve(process.cwd(), 'preview', 'shared');
33
+ const analyticsFile = path.resolve(analyticsDir, 'analytics.json');
38
34
 
39
35
  // ensure analytics folder & file exist
40
- fs.mkdirSync(analyticsDir, {recursive: true})
36
+ fs.mkdirSync(analyticsDir, { recursive: true });
41
37
  if (!fs.existsSync(analyticsFile)) {
42
- fs.writeFileSync(analyticsFile, JSON.stringify({}, null, 2), 'utf-8')
38
+ fs.writeFileSync(analyticsFile, JSON.stringify({}, null, 2), 'utf-8');
43
39
  }
44
40
 
45
41
  // helper to bump and persist counter for a given section
46
42
  function bumpSectionCount(section) {
47
- const raw = fs.readFileSync(analyticsFile, 'utf-8')
48
- const data = JSON.parse(raw)
49
- const next = (data[section] || 0) + 1
50
- data[section] = next
51
- fs.writeFileSync(analyticsFile, JSON.stringify(data, null, 2), 'utf-8')
52
- 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;
53
49
  }
54
50
 
55
51
  server.middlewares.use((req, res, next) => {
56
- const url = req.url || ''
52
+ // Invalidate all caches so we fetch most recent sections data
53
+ server.moduleGraph.invalidateAll();
57
54
 
58
- // Skip HMR and internal Vite requests
59
- if (url.startsWith('/@') || url.includes('__vite') || url.includes('.hot-update.')) {
60
- return next()
61
- }
55
+ const url = req.url || '';
62
56
 
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
- }
57
+ // Skip HMR and internal Vite requests
58
+ if (url.startsWith('/@') || url.includes('__vite') || url.includes('.hot-update.')) {
59
+ return next();
60
+ }
72
61
 
73
- // Handle preflight OPTIONS requests
74
- if (req.method === 'OPTIONS') {
75
- res.statusCode = 200
76
- return res.end()
77
- }
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
+ }
78
71
 
79
- // Let Vite handle all non-API requests for proper HMR
80
- if (!url.startsWith('/api/') && !url.startsWith('/chosen-section')) {
81
- return next()
82
- }
72
+ // Handle preflight OPTIONS requests
73
+ if (req.method === 'OPTIONS') {
74
+ res.statusCode = 200;
75
+ return res.end();
76
+ }
83
77
 
84
- if (url.startsWith('/chosen-section')) {
85
- // extract "sectionName" from "/chosen-section/<sectionName>"
78
+ // Let Vite handle all non-API requests for proper HMR
79
+ if (!url.startsWith('/api/') && !url.startsWith('/chosen-section')) {
80
+ return next();
81
+ }
86
82
 
87
- const sectionName = req.url.replace(/\/$/, '').split('/').filter(Boolean).pop()
88
- const newCount = bumpSectionCount(sectionName)
83
+ if (url.startsWith('/chosen-section')) {
84
+ // extract "sectionName" from "/chosen-section/<sectionName>"
89
85
 
90
- res.statusCode = 200
91
- res.setHeader('Content-Type', 'application/json')
92
- return res.end(JSON.stringify({
93
- section: sectionName,
94
- count: newCount
95
- }))
96
- }
86
+ const sectionName = req.url.replace(/\/$/, '').split('/').filter(Boolean).pop();
87
+ const newCount = bumpSectionCount(sectionName);
97
88
 
98
- // Handle API routes
99
- if (url.startsWith('/api/v1/')) {
100
- const sectionName = SECTION_NAME;
101
- const showcaseId = SHOWCASE_ID;
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
+ }
102
96
 
103
- // Delegate to API router
104
- (async () => {
105
- await handleApiRequest(req, res, sectionName, showcaseId);
106
- })();
107
- return; // Exit early for async handling
108
- }
97
+ // Handle API routes
98
+ if (url.startsWith('/api/v1/')) {
99
+ // Extract auth header from request
100
+ const authHeader = req.headers['authorization'] || '';
109
101
 
110
- // all other requests
111
- next()
102
+ // Delegate to API router
103
+ (async () => {
104
+ await handleApiRequest(req, res, authHeader);
105
+ })();
106
+ return; // Exit early for async handling
112
107
  }
108
+
109
+ // all other requests
110
+ next();
111
+ },
113
112
  );
114
113
  },
115
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';