@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.
- package/CHANGELOG.md +51 -4
- package/UPGRADE.md +19 -0
- package/dist/app.d.mts +1 -1028
- package/dist/app.d.ts +1 -1028
- package/dist/app.mjs +1 -1
- package/dist/cli.mjs +20 -7
- package/package.json +6 -3
- package/template/footers/example-footer/ExampleFooter.vue +1 -1
- package/template/footers/example-footer/client.ts +1 -1
- package/template/footers/example-footer/component/LegalLinks.vue +1 -1
- package/template/footers/example-footer/component/MadeWith.vue +1 -1
- package/template/footers/example-footer/component/ReportAbuse.vue +1 -1
- package/template/footers/example-footer/entity/color.ts +2 -2
- package/template/footers/example-footer/server.ts +1 -1
- package/template/headers/example-header/client.ts +1 -1
- package/template/headers/example-header/component/Account.vue +1 -1
- package/template/headers/example-header/component/Cart.vue +1 -1
- package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
- package/template/headers/example-header/component/Logo.vue +1 -1
- package/template/headers/example-header/component/NavigationMenu.vue +1 -1
- package/template/headers/example-header/component/SearchForm.vue +1 -1
- package/template/headers/example-header/server.ts +1 -1
- package/template/index.d.ts +1 -1
- package/template/layouts/catalog/example-catalog/Main.vue +1 -1
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/client.ts +1 -1
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +1 -1
- package/template/layouts/category/example-category/Main.vue +1 -1
- package/template/layouts/category/example-category/settings/content.ts +1 -1
- package/template/layouts/category/example-category/settings/design.ts +1 -1
- package/template/layouts/product/example-product/Main.vue +1 -1
- package/template/layouts/product/example-product/settings/content.ts +1 -1
- package/template/layouts/product/example-product/settings/design.ts +1 -1
- package/template/package.json +7 -3
- package/template/page-templates/example-template/pages/catalog.ts +1 -1
- package/template/page-templates/example-template/pages/category.ts +1 -1
- package/template/page-templates/example-template/pages/product.ts +1 -1
- package/template/preview/sections/preview.html +1 -1
- package/template/preview/shared/api-routes.ts +443 -0
- package/template/preview/shared/mock.ts +43 -41
- package/template/preview/shared/preview.ts +213 -101
- package/template/preview/shared/utils.ts +315 -2
- package/template/preview/ssr-server.ts +429 -0
- package/template/preview/vite.config.js +76 -34
- package/template/reference/sections/about-us/AboutUs.vue +1 -1
- package/template/reference/sections/about-us/client.ts +1 -1
- package/template/reference/sections/about-us/component/Image.vue +1 -1
- package/template/reference/sections/about-us/component/Stats.vue +2 -2
- package/template/reference/sections/about-us/component/Title.vue +1 -1
- package/template/reference/sections/about-us/server.ts +1 -1
- package/template/reference/sections/about-us/util/visibility-provider.ts +1 -1
- package/template/reference/sections/featured-products/FeaturedProducts.vue +65 -0
- package/template/reference/sections/featured-products/assets/arrow.svg +3 -0
- package/template/reference/sections/featured-products/assets/custom_section_showcase_1_preview.png +0 -0
- package/template/reference/sections/featured-products/client.ts +5 -0
- package/template/reference/sections/featured-products/component/ProductItem.vue +71 -0
- package/template/reference/sections/featured-products/component/Title.vue +31 -0
- package/template/reference/sections/featured-products/entity/color.ts +4 -0
- package/template/reference/sections/featured-products/server.ts +5 -0
- package/template/reference/sections/featured-products/settings/content.ts +14 -0
- package/template/reference/sections/featured-products/settings/design.ts +33 -0
- package/template/reference/sections/featured-products/settings/translations.ts +24 -0
- package/template/reference/sections/featured-products/showcases/1.ts +28 -0
- package/template/reference/sections/featured-products/showcases/translations.ts +16 -0
- package/template/reference/sections/featured-products/type.ts +5 -0
- package/template/reference/sections/intro-slider/IntroSlider.vue +1 -1
- package/template/reference/sections/intro-slider/client.ts +1 -1
- package/template/reference/sections/intro-slider/component/Slider.vue +8 -2
- package/template/reference/sections/intro-slider/component/Title.vue +1 -1
- package/template/reference/sections/intro-slider/entity/color.ts +2 -2
- package/template/reference/sections/intro-slider/server.ts +1 -1
- package/template/reference/sections/tag-lines/TagLines.vue +1 -1
- package/template/reference/sections/tag-lines/client.ts +1 -1
- package/template/reference/sections/tag-lines/component/SectionImage.vue +1 -1
- package/template/reference/sections/tag-lines/component/Title.vue +1 -1
- package/template/reference/sections/tag-lines/composables/highlighted-text-image-list.ts +2 -2
- package/template/reference/sections/tag-lines/server.ts +1 -1
- package/template/reference/sections/trending-categories/TrendingCategories.vue +70 -0
- package/template/reference/sections/trending-categories/assets/arrow.svg +3 -0
- package/template/reference/sections/trending-categories/assets/custom_section_showcase_1_preview.png +0 -0
- package/template/reference/sections/trending-categories/client.ts +5 -0
- package/template/reference/sections/trending-categories/component/CategoryItem.vue +62 -0
- package/template/reference/sections/trending-categories/component/Title.vue +32 -0
- package/template/reference/sections/trending-categories/entity/color.ts +4 -0
- package/template/reference/sections/trending-categories/server.ts +5 -0
- package/template/reference/sections/trending-categories/settings/content.ts +14 -0
- package/template/reference/sections/trending-categories/settings/design.ts +33 -0
- package/template/reference/sections/trending-categories/settings/translations.ts +24 -0
- package/template/reference/sections/trending-categories/showcases/1.ts +36 -0
- package/template/reference/sections/trending-categories/showcases/translations.ts +22 -0
- package/template/reference/sections/trending-categories/type.ts +5 -0
- package/template/reference/shared/components/Button.vue +1 -1
- package/template/reference/templates/reference-template-apparel/pages/catalog.ts +1 -1
- package/template/reference/templates/reference-template-apparel/pages/category.ts +1 -1
- package/template/reference/templates/reference-template-apparel/pages/home.ts +10 -0
- package/template/reference/templates/reference-template-apparel/pages/product.ts +1 -1
- package/template/reference/templates/reference-template-bike/pages/catalog.ts +1 -1
- package/template/reference/templates/reference-template-bike/pages/category.ts +1 -1
- package/template/reference/templates/reference-template-bike/pages/home.ts +10 -0
- package/template/reference/templates/reference-template-bike/pages/product.ts +1 -1
- package/template/sections/example-section/ExampleSection.vue +8 -1
- package/template/sections/example-section/client.ts +1 -1
- package/template/sections/example-section/component/button/Button.vue +1 -1
- package/template/sections/example-section/component/image/Image.vue +1 -1
- package/template/sections/example-section/component/image/ImagesGrid.vue +1 -1
- package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
- package/template/sections/example-section/component/title/Title.vue +1 -1
- package/template/sections/example-section/component/toggle/Toggle.vue +1 -1
- package/template/sections/example-section/entity/color.ts +2 -2
- package/template/sections/example-section/server.ts +1 -1
- package/template/sections/example-section/settings/translations.ts +1 -1
- package/template/sections/example-section/showcases/translations.ts +13 -13
- package/template/shared/components/LanguageSelector.vue +1 -1
- package/template/shared/translation.ts +16 -0
- package/template/shared/utils.ts +3 -1
- package/template/tsconfig.json +1 -0
- 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
|
|
3
|
-
import fs from
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
83
|
+
if (url.startsWith('/chosen-section')) {
|
|
84
|
+
// extract "sectionName" from "/chosen-section/<sectionName>"
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
|
|
86
|
+
const sectionName = req.url.replace(/\/$/, '').split('/').filter(Boolean).pop();
|
|
87
|
+
const newCount = bumpSectionCount(sectionName);
|
|
58
88
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
//
|
|
68
|
-
|
|
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';
|
|
@@ -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';
|