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