@deneb-ui/cli 2.0.28 → 2.0.30

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.
@@ -1,687 +1,17 @@
1
1
  #!/usr/bin/env node
2
-
3
- const crypto = require('node:crypto');
4
- const fs = require('node:fs');
5
- const http = require('node:http');
6
- const path = require('node:path');
7
- const { spawn } = require('node:child_process');
8
-
9
- const DEFAULT_API_PORT = 4174;
10
- const DEFAULT_PREVIEW_PORT = 4173;
11
- const MAX_LOG_LENGTH = 96 * 1024;
12
- const LOOPBACK_HOST = '127.0.0.1';
13
- const LOCAL_PREVIEW_ROUTE = '/__fivora_local_preview';
14
- const FALLBACK_LOCAL_VISUAL_BRIDGE_SCRIPT = String.raw`
15
- (() => {
16
- const PREVIOUS_BRAND = ['market', 'place'].join('');
17
- const PREVIOUS_BRIDGE_KEY = '__' + PREVIOUS_BRAND.toUpperCase() + '_LOCAL_VISUAL_BRIDGE__';
18
- const PREVIOUS_PREVIEW_PREFIX = PREVIOUS_BRAND.toUpperCase() + '_PREVIEW_';
19
- const previousPreviewMessage = (suffix) => PREVIOUS_PREVIEW_PREFIX + suffix;
20
- if (window.__FIVORA_LOCAL_VISUAL_BRIDGE__ || window[PREVIOUS_BRIDGE_KEY]) return;
21
- window.__FIVORA_LOCAL_VISUAL_BRIDGE__ = true;
22
- window[PREVIOUS_BRIDGE_KEY] = true;
23
-
24
- const EDIT_MODE_MESSAGE = 'FIVORA_PREVIEW_EDIT_MODE';
25
- const LEGACY_EDIT_MODE_MESSAGE = previousPreviewMessage('EDIT_MODE');
26
- const CLICK_MESSAGE = 'FIVORA_PREVIEW_ELEMENT_CLICKED';
27
- const LEGACY_CLICK_MESSAGE = previousPreviewMessage('ELEMENT_CLICKED');
28
- const READY_MESSAGE = 'FIVORA_PREVIEW_READY';
29
- const LEGACY_READY_MESSAGE = previousPreviewMessage('READY');
30
- const FIELD_ATTRIBUTE = 'data-preview-field-path';
31
- const LIST_ATTRIBUTE = 'data-preview-list-path';
32
- const ITEM_ATTRIBUTE = 'data-preview-item-path';
33
- const ACTIVE_ATTRIBUTE = 'data-fivora-local-edit-target';
34
- const SELECTED_ATTRIBUTE = 'data-fivora-local-selected-target';
35
- const LEGACY_ACTIVE_ATTRIBUTE = 'data-' + PREVIOUS_BRAND + '-local-edit-target';
36
- const LEGACY_SELECTED_ATTRIBUTE = 'data-' + PREVIOUS_BRAND + '-local-selected-target';
37
- const EDITABLE_SELECTOR =
38
- '[data-preview-field-path], [data-preview-list-path], [data-preview-item-path]';
39
- // The local lab is edit-first when embedded. The portal can still switch the
40
- // preview back to normal navigation mode with EDIT_MODE_MESSAGE.
41
- let editMode = window.parent !== window;
42
- let descriptors = new Map();
43
- let activeTarget = null;
44
- let selectedTarget = null;
45
- let parentOrigin = '*';
46
-
47
- const style = document.createElement('style');
48
- style.setAttribute('data-fivora-local-visual-bridge', '');
49
- style.setAttribute('data-' + PREVIOUS_BRAND + '-local-visual-bridge', '');
50
- style.textContent = [
51
- '[' + ACTIVE_ATTRIBUTE + '], [' + LEGACY_ACTIVE_ATTRIBUTE + '] {',
52
- ' outline: 2px solid #06b6d4 !important;',
53
- ' outline-offset: 3px !important;',
54
- ' cursor: pointer !important;',
55
- ' box-shadow: 0 0 0 5px rgba(6, 182, 212, 0.16) !important;',
56
- '}',
57
- '[' + SELECTED_ATTRIBUTE + '], [' + LEGACY_SELECTED_ATTRIBUTE + '] {',
58
- ' outline: 2px dashed #06b6d4 !important;',
59
- ' outline-offset: 3px !important;',
60
- ' box-shadow: 0 0 0 5px rgba(6, 182, 212, 0.18) !important;',
61
- '}',
62
- 'html[data-fivora-local-edit-mode="true"] [data-preview-field-path],',
63
- 'html[data-fivora-local-edit-mode="true"] [data-preview-list-path],',
64
- 'html[data-fivora-local-edit-mode="true"] [data-preview-item-path],',
65
- 'html[data-fivora-local-edit-mode="true"] [data-preview-field-path],',
66
- 'html[data-fivora-local-edit-mode="true"] [data-preview-list-path],',
67
- 'html[data-fivora-local-edit-mode="true"] [data-preview-item-path] {',
68
- ' cursor: pointer !important;',
69
- '}',
70
- ].join('\n');
71
- document.head.appendChild(style);
72
- document.documentElement.setAttribute(
73
- 'data-fivora-local-edit-mode',
74
- String(editMode),
75
- );
76
- document.documentElement.setAttribute(
77
- 'data-fivora-local-edit-mode',
78
- String(editMode),
79
- );
80
-
81
- function rememberParentOrigin(event) {
82
- if (event.source !== window.parent) return false;
83
- if (parentOrigin === '*' && event.origin && event.origin !== 'null') {
84
- parentOrigin = event.origin;
85
- }
86
- return parentOrigin === '*' || event.origin === parentOrigin;
87
- }
88
-
89
- function post(payload) {
90
- window.parent.postMessage(payload, parentOrigin);
91
- }
92
-
93
- function clearActiveTarget() {
94
- if (activeTarget) {
95
- activeTarget.removeAttribute(ACTIVE_ATTRIBUTE);
96
- activeTarget.removeAttribute(LEGACY_ACTIVE_ATTRIBUTE);
97
- }
98
- activeTarget = null;
99
- }
100
-
101
- function selectTarget(target) {
102
- if (selectedTarget && selectedTarget !== target) {
103
- selectedTarget.removeAttribute(SELECTED_ATTRIBUTE);
104
- selectedTarget.removeAttribute(LEGACY_SELECTED_ATTRIBUTE);
105
- }
106
- selectedTarget = target;
107
- selectedTarget.setAttribute(SELECTED_ATTRIBUTE, 'true');
108
- selectedTarget.setAttribute(LEGACY_SELECTED_ATTRIBUTE, 'true');
109
- }
110
-
111
- function clearSelectedTarget() {
112
- if (selectedTarget) {
113
- selectedTarget.removeAttribute(SELECTED_ATTRIBUTE);
114
- selectedTarget.removeAttribute(LEGACY_SELECTED_ATTRIBUTE);
115
- }
116
- selectedTarget = null;
117
- }
118
-
119
- function findEditableTarget(rawTarget) {
120
- if (!(rawTarget instanceof Element)) return null;
121
- return rawTarget.closest(EDITABLE_SELECTOR);
122
- }
123
-
124
- function fieldValue(target, fieldPath) {
125
- const descriptor = descriptors.get(fieldPath);
126
- if (descriptor && ['string', 'number', 'boolean'].includes(typeof descriptor.value)) {
127
- return descriptor.value;
128
- }
129
- if (target instanceof HTMLImageElement) return target.currentSrc || target.src || '';
130
- return (target.textContent || '').trim();
131
- }
132
-
133
- function onPointerOver(event) {
134
- if (!editMode) return;
135
- const target = findEditableTarget(event.target);
136
- if (!target || target === activeTarget) return;
137
- clearActiveTarget();
138
- activeTarget = target;
139
- activeTarget.setAttribute(ACTIVE_ATTRIBUTE, 'true');
140
- }
141
-
142
- function onPointerOut(event) {
143
- if (!editMode || !activeTarget) return;
144
- const next = event.relatedTarget;
145
- if (next instanceof Node && activeTarget.contains(next)) return;
146
- clearActiveTarget();
147
- }
148
-
149
- function findRelatedFields(target, fieldPath, itemPath) {
150
- const related = [];
151
- const seen = new Set(fieldPath ? [fieldPath] : []);
152
- const prefix =
153
- itemPath ||
154
- (fieldPath && fieldPath.replace(/(\[\d+\])?\.\w+$/, '').replace(/\[\d+\]$/, ''));
155
- if (prefix) {
156
- for (const [path, descriptor] of descriptors) {
157
- if (
158
- path !== fieldPath &&
159
- !seen.has(path) &&
160
- (path.startsWith(prefix + '.') || path.startsWith(prefix + '[')) &&
161
- descriptor?.kind !== 'collection'
162
- ) {
163
- seen.add(path);
164
- related.push({
165
- path,
166
- label: descriptor.label || path.split('.').pop() || 'Content',
167
- type: descriptor.type || 'text',
168
- });
169
- }
170
- }
171
- }
172
- let current = target instanceof Element ? target : null;
173
- let depth = 0;
174
- while (current && current !== document.body && depth < 3) {
175
- const candidatePath = current.getAttribute(FIELD_ATTRIBUTE);
176
- if (
177
- candidatePath &&
178
- candidatePath !== fieldPath &&
179
- !seen.has(candidatePath)
180
- ) {
181
- const descriptor = descriptors.get(candidatePath);
182
- if (descriptor?.kind !== 'collection') {
183
- seen.add(candidatePath);
184
- related.push({
185
- path: candidatePath,
186
- label: descriptor?.label || candidatePath.split('.').pop() || 'Content',
187
- type: descriptor?.type || 'text',
188
- });
189
- }
190
- }
191
- current = current.parentElement;
192
- depth += 1;
193
- }
194
- return related;
195
- }
196
-
197
- function onClick(event) {
198
- if (!editMode) return;
199
- const target = findEditableTarget(event.target);
200
- if (!target) return;
201
- event.preventDefault();
202
- event.stopPropagation();
203
- event.stopImmediatePropagation();
204
- selectTarget(target);
205
-
206
- const fieldTarget = target.closest('[' + FIELD_ATTRIBUTE + ']');
207
- const listTarget = target.closest('[' + LIST_ATTRIBUTE + ']');
208
- const itemTarget = target.closest('[' + ITEM_ATTRIBUTE + ']');
209
- const fieldPath = fieldTarget?.getAttribute(FIELD_ATTRIBUTE) || null;
210
- const listPath = listTarget?.getAttribute(LIST_ATTRIBUTE) || null;
211
- const itemPath = itemTarget?.getAttribute(ITEM_ATTRIBUTE) || null;
212
- const descriptor = fieldPath ? descriptors.get(fieldPath) : null;
213
- const rect = target.getBoundingClientRect();
214
- const itemIndexMatch = itemPath?.match(/\[(\d+)\](?!.*\[\d+\])/);
215
-
216
- const clickPayload = {
217
- type: CLICK_MESSAGE,
218
- fieldPath,
219
- fieldValue: fieldPath ? fieldValue(fieldTarget || target, fieldPath) : '',
220
- elementTag: target.tagName.toLowerCase(),
221
- isImage:
222
- target instanceof HTMLImageElement || descriptor?.type === 'image',
223
- boundingRect: {
224
- top: rect.top,
225
- left: rect.left,
226
- width: rect.width,
227
- height: rect.height,
228
- },
229
- collectionPath: listPath,
230
- listPath,
231
- itemPath,
232
- itemIndex: itemIndexMatch ? Number(itemIndexMatch[1]) : null,
233
- descriptorKind: fieldPath ? 'field' : listPath ? 'collection' : null,
234
- relatedFields: findRelatedFields(target, fieldPath, itemPath),
235
- };
236
- post(clickPayload);
237
- post({ ...clickPayload, type: LEGACY_CLICK_MESSAGE });
238
- }
239
-
240
- window.addEventListener('message', (event) => {
241
- if (!rememberParentOrigin(event) || !event.data || typeof event.data !== 'object') {
242
- return;
243
- }
244
- if (event.data.type !== EDIT_MODE_MESSAGE && event.data.type !== LEGACY_EDIT_MODE_MESSAGE) return;
245
- editMode = event.data.editMode === true;
246
- descriptors = new Map(
247
- Array.isArray(event.data.fields)
248
- ? event.data.fields
249
- .filter((field) => field && typeof field.path === 'string')
250
- .map((field) => [field.path, field])
251
- : [],
252
- );
253
- document.documentElement.setAttribute(
254
- 'data-fivora-local-edit-mode',
255
- String(editMode),
256
- );
257
- document.documentElement.setAttribute(
258
- 'data-fivora-local-edit-mode',
259
- String(editMode),
260
- );
261
- if (!editMode) {
262
- clearActiveTarget();
263
- clearSelectedTarget();
264
- }
265
- });
266
-
267
- document.addEventListener('mouseover', onPointerOver, true);
268
- document.addEventListener('mouseout', onPointerOut, true);
269
- document.addEventListener('click', onClick, true);
270
- post({ type: READY_MESSAGE, pathname: window.location.pathname });
271
- post({ type: LEGACY_READY_MESSAGE, pathname: window.location.pathname });
272
- })();
273
- `;
274
-
275
- let LOCAL_VISUAL_BRIDGE_SCRIPT = FALLBACK_LOCAL_VISUAL_BRIDGE_SCRIPT;
276
- try {
277
- const productionBridge = require('./template-preview-focus-bridge.cjs');
278
- if (typeof productionBridge === 'string' && productionBridge.trim()) {
279
- LOCAL_VISUAL_BRIDGE_SCRIPT = productionBridge;
280
- }
281
- } catch {
282
- // Source checkouts can run before the generated production bridge exists.
283
- // The package build always creates it; retain a compact fallback for safety.
284
- }
285
-
286
- function fail(message) {
287
- process.stderr.write(`Local Template Lab: ${message}\n`);
288
- process.exit(1);
289
- }
290
-
291
- try {
292
- // Validate the generated iframe program as well as this outer runner file.
293
- new Function(LOCAL_VISUAL_BRIDGE_SCRIPT);
294
- } catch (error) {
295
- fail(`Visual editor bridge is invalid: ${error.message}`);
296
- }
297
-
298
- function parsePort(value, flag, fallback) {
299
- if (value === undefined) return fallback;
300
- const port = Number(value);
301
- if (!Number.isInteger(port) || port < 1024 || port > 65535) {
302
- fail(`${flag} must be a port between 1024 and 65535.`);
303
- }
304
- return port;
305
- }
306
-
307
- function parseArguments(argv) {
308
- const args = [...argv];
309
- let templatePath = '';
310
- let apiPort;
311
- let previewPort;
312
- let skipInstall = false;
313
-
314
- for (let index = 0; index < args.length; index += 1) {
315
- const arg = args[index];
316
- if (arg === '--api-port') {
317
- apiPort = args[index + 1];
318
- index += 1;
319
- } else if (arg.startsWith('--api-port=')) {
320
- apiPort = arg.slice('--api-port='.length);
321
- } else if (arg === '--preview-port') {
322
- previewPort = args[index + 1];
323
- index += 1;
324
- } else if (arg.startsWith('--preview-port=')) {
325
- previewPort = arg.slice('--preview-port='.length);
326
- } else if (arg === '--skip-install') {
327
- skipInstall = true;
328
- } else if (arg === '--help' || arg === '-h') {
329
- process.stdout.write(
330
- [
331
- 'Fivora Local Template Lab',
332
- '',
333
- 'Usage:',
334
- ' npm run lab -- <template-directory> [options]',
335
- '',
336
- 'Options:',
337
- ' --api-port <port> Loopback controller port (default: 4174)',
338
- ' --preview-port <port> Template dev-server port (default: 4173)',
339
- ' --skip-install Do not install missing local dependencies',
340
- '',
341
- ].join('\n'),
342
- );
343
- process.exit(0);
344
- } else if (arg.startsWith('-')) {
345
- fail(`Unknown option: ${arg}`);
346
- } else if (!templatePath) {
347
- templatePath = arg;
348
- } else {
349
- fail(`Unexpected argument: ${arg}`);
350
- }
351
- }
352
-
353
- if (!templatePath) {
354
- fail('A template directory is required. Run with --help for usage.');
355
- }
356
-
357
- return {
358
- templatePath: path.resolve(templatePath),
359
- apiPort: parsePort(apiPort, '--api-port', DEFAULT_API_PORT),
360
- previewPort: parsePort(
361
- previewPort,
362
- '--preview-port',
363
- DEFAULT_PREVIEW_PORT,
364
- ),
365
- skipInstall,
366
- };
367
- }
368
-
369
- function readJson(filePath, label) {
370
- try {
371
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
372
- } catch (error) {
373
- fail(
374
- `${label} is missing or invalid at ${filePath}: ${
375
- error instanceof Error ? error.message : 'unknown error'
376
- }`,
377
- );
378
- }
379
- }
380
-
381
- const options = parseArguments(process.argv.slice(2));
382
- if (!fs.existsSync(options.templatePath)) {
383
- fail(`Template directory does not exist: ${options.templatePath}`);
384
- }
385
- if (!fs.statSync(options.templatePath).isDirectory()) {
386
- fail(`Template path must be a directory: ${options.templatePath}`);
387
- }
388
-
389
- const manifestFile = [
390
- 'fivora-template.json',
391
- 'fivora-template.json',
392
- ].find((name) => fs.existsSync(path.join(options.templatePath, name))) || 'fivora-template.json';
393
-
394
- const manifestPath = path.join(options.templatePath, manifestFile);
395
- const packagePath = path.join(options.templatePath, 'package.json');
396
- const manifest = readJson(manifestPath, 'Template manifest');
397
- const packageJson = readJson(packagePath, 'package.json');
398
-
399
- if (manifest.framework !== 'nextjs-static-export') {
400
- fail('Template manifest framework must be "nextjs-static-export".');
401
- }
402
- if (!packageJson.scripts || typeof packageJson.scripts.dev !== 'string') {
403
- fail('Template package.json must define a dev script for live preview.');
404
- }
405
- if (typeof manifest.siteDataFile !== 'string' || !manifest.siteDataFile.trim()) {
406
- fail('Template manifest siteDataFile is required.');
407
- }
408
-
409
- const siteDataPath = path.resolve(
410
- options.templatePath,
411
- manifest.siteDataFile.trim(),
412
- );
413
- const relativeSiteDataPath = path.relative(options.templatePath, siteDataPath);
414
- if (
415
- relativeSiteDataPath.startsWith('..') ||
416
- path.isAbsolute(relativeSiteDataPath)
417
- ) {
418
- fail('Template manifest siteDataFile must stay inside the template directory.');
419
- }
420
-
421
- const controllerToken = crypto.randomBytes(24).toString('base64url');
422
- const apiUrl = `http://${LOOPBACK_HOST}:${options.apiPort}`;
423
- const sourcePreviewUrl = `http://${LOOPBACK_HOST}:${options.previewPort}`;
424
- const previewUrl = `${apiUrl}${LOCAL_PREVIEW_ROUTE}`;
425
- const validatorPath = path.join(
426
- __dirname,
427
- 'deneb-template-validator.cjs',
428
- );
429
-
430
- let shuttingDown = false;
431
- let devProcess = null;
432
- let validationProcess = null;
433
- let readinessTimer = null;
434
- let devRestartTimer = null;
435
- let devRestartAttempts = 0;
436
- const MAX_DEV_RESTART_ATTEMPTS = 5;
437
- let devLog = '';
438
- let validationLog = '';
439
-
440
- const state = {
441
- protocolVersion: 2,
442
- connected: true,
443
- templateName:
444
- typeof manifest.name === 'string' && manifest.name.trim()
445
- ? manifest.name.trim()
446
- : typeof packageJson.name === 'string'
447
- ? packageJson.name
448
- : path.basename(options.templatePath),
449
- templatePath: options.templatePath,
450
- previewUrl,
451
- apiUrl,
452
- devStatus: 'starting',
453
- devError: null,
454
- startedAt: new Date().toISOString(),
455
- validation: {
456
- status: 'idle',
457
- startedAt: null,
458
- completedAt: null,
459
- exitCode: null,
460
- },
461
- };
462
-
463
- function appendLog(target, chunk) {
464
- const text = chunk.toString();
465
- if (target === 'dev') {
466
- devLog = `${devLog}${text}`.slice(-MAX_LOG_LENGTH);
467
- } else {
468
- validationLog = `${validationLog}${text}`.slice(-MAX_LOG_LENGTH);
469
- }
470
- process.stdout.write(text);
471
- }
472
-
473
- function statusPayload() {
474
- return {
475
- ...state,
476
- devLog,
477
- validationLog,
478
- };
479
- }
480
-
481
- function setCorsHeaders(request, response) {
482
- const origin = request.headers.origin;
483
- if (origin && /^(https?:\/\/|null$)/.test(origin)) {
484
- response.setHeader('Access-Control-Allow-Origin', origin);
485
- response.setHeader('Vary', 'Origin');
486
- }
487
- response.setHeader(
488
- 'Access-Control-Allow-Headers',
489
- 'Authorization, Content-Type',
490
- );
491
- response.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
492
- response.setHeader('Access-Control-Allow-Private-Network', 'true');
493
- response.setHeader('Cache-Control', 'no-store');
494
- response.setHeader('X-Content-Type-Options', 'nosniff');
495
- }
496
-
497
- function sendJson(response, statusCode, payload) {
498
- const body = JSON.stringify(payload);
499
- response.statusCode = statusCode;
500
- response.setHeader('Content-Type', 'application/json; charset=utf-8');
501
- response.setHeader('Content-Length', Buffer.byteLength(body));
502
- response.end(body);
503
- }
504
-
505
- function isAuthorized(request) {
506
- return request.headers.authorization === `Bearer ${controllerToken}`;
507
- }
508
-
509
- function readSiteData() {
510
- try {
511
- return JSON.parse(fs.readFileSync(siteDataPath, 'utf8'));
512
- } catch (error) {
513
- throw new Error(
514
- `Unable to read ${manifest.siteDataFile}: ${
515
- error instanceof Error ? error.message : 'unknown error'
516
- }`,
517
- );
518
- }
519
- }
520
-
521
- function startValidation() {
522
- if (validationProcess) return false;
523
-
524
- validationLog = '';
525
- state.validation = {
526
- status: 'running',
527
- startedAt: new Date().toISOString(),
528
- completedAt: null,
529
- exitCode: null,
530
- };
531
-
532
- validationProcess = spawn(
533
- process.execPath,
534
- [validatorPath, 'validate', options.templatePath],
535
- {
536
- cwd: __dirname,
537
- env: process.env,
538
- stdio: ['ignore', 'pipe', 'pipe'],
539
- },
540
- );
541
- validationProcess.stdout.on('data', (chunk) => appendLog('validation', chunk));
542
- validationProcess.stderr.on('data', (chunk) => appendLog('validation', chunk));
543
- validationProcess.on('error', (error) => {
544
- appendLog('validation', `\nUnable to start validation: ${error.message}\n`);
545
- });
546
- validationProcess.on('close', (code) => {
547
- state.validation = {
548
- ...state.validation,
549
- status: code === 0 ? 'passed' : 'failed',
550
- completedAt: new Date().toISOString(),
551
- exitCode: code,
552
- };
553
- validationProcess = null;
554
- });
555
- return true;
556
- }
557
-
558
- function probePreview() {
559
- if (shuttingDown || state.devStatus === 'ready') return;
560
- const request = http.get(sourcePreviewUrl, (response) => {
561
- response.resume();
562
- if (response.statusCode && response.statusCode < 500) {
563
- state.devStatus = 'ready';
564
- state.devError = null;
565
- devRestartAttempts = 0;
566
- process.stdout.write(`\nLive preview ready: ${previewUrl}\n`);
567
- return;
568
- }
569
- readinessTimer = setTimeout(probePreview, 600);
570
- });
571
- request.setTimeout(900, () => request.destroy());
572
- request.on('error', () => {
573
- readinessTimer = setTimeout(probePreview, 600);
574
- });
575
- }
576
-
577
- function scheduleDevRestart(reason) {
578
- if (shuttingDown || devRestartTimer) return;
579
- if (devRestartAttempts >= MAX_DEV_RESTART_ATTEMPTS) {
580
- state.devStatus = 'failed';
581
- state.devError = reason;
582
- return;
583
- }
584
- devRestartAttempts += 1;
585
- state.devStatus = 'starting';
586
- state.devError = null;
587
- appendLog(
588
- 'dev',
589
- `\nPreview server stopped during navigation (${reason}). Restarting (${devRestartAttempts}/${MAX_DEV_RESTART_ATTEMPTS})...\n`,
590
- );
591
- devRestartTimer = setTimeout(() => {
592
- devRestartTimer = null;
593
- startPreview();
594
- }, 900);
595
- }
596
-
597
- function runInstallThenPreview() {
598
- const nodeModulesPath = path.join(options.templatePath, 'node_modules');
599
- if (!options.skipInstall && !fs.existsSync(nodeModulesPath)) {
600
- state.devStatus = 'installing';
601
- const installCommand =
602
- typeof manifest.installCommand === 'string' && manifest.installCommand.trim()
603
- ? manifest.installCommand.trim()
604
- : 'npm install';
605
- appendLog('dev', `Installing local dependencies with: ${installCommand}\n`);
606
- const install = spawn(installCommand, {
607
- cwd: options.templatePath,
608
- env: process.env,
609
- shell: true,
610
- stdio: ['ignore', 'pipe', 'pipe'],
611
- });
612
- devProcess = install;
613
- install.stdout.on('data', (chunk) => appendLog('dev', chunk));
614
- install.stderr.on('data', (chunk) => appendLog('dev', chunk));
615
- install.on('error', (error) => {
616
- state.devStatus = 'failed';
617
- state.devError = error.message;
618
- devProcess = null;
619
- });
620
- install.on('close', (code) => {
621
- devProcess = null;
622
- if (code !== 0) {
623
- state.devStatus = 'failed';
624
- state.devError = `Dependency installation exited with code ${code}.`;
625
- return;
626
- }
627
- startPreview();
628
- });
629
- return;
630
- }
631
-
632
- startPreview();
633
- }
634
-
635
- function startPreview() {
636
- state.devStatus = 'starting';
637
- state.devError = null;
638
- appendLog(
639
- 'dev',
640
- `Starting template source server on ${sourcePreviewUrl}. Source edits will hot reload.\n`,
641
- );
642
- const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
643
- devProcess = spawn(
644
- npmCmd,
645
- [
646
- 'run',
647
- 'dev',
648
- '--',
649
- '--hostname',
650
- LOOPBACK_HOST,
651
- '--port',
652
- String(options.previewPort),
653
- ],
654
- {
655
- cwd: options.templatePath,
656
- env: {
657
- ...process.env,
658
- NEXT_PUBLIC_SITE_BASE_PATH: '',
659
- },
660
- shell: process.platform === 'win32',
661
- stdio: ['ignore', 'pipe', 'pipe'],
662
- },
663
- );
664
- devProcess.stdout.on('data', (chunk) => appendLog('dev', chunk));
665
- devProcess.stderr.on('data', (chunk) => appendLog('dev', chunk));
666
- devProcess.on('error', (error) => {
667
- state.devStatus = 'failed';
668
- state.devError = error.message;
669
- devProcess = null;
670
- });
671
- devProcess.on('close', (code, signal) => {
672
- devProcess = null;
673
- if (!shuttingDown) {
674
- const reason = signal
675
- ? `signal ${signal}`
676
- : `exit code ${code}`;
677
- scheduleDevRestart(reason);
678
- }
679
- });
680
- probePreview();
681
- }
682
-
683
- function previewShellHtml() {
684
- return `<!doctype html>
2
+ const Q=require("node:crypto"),_=require("node:fs"),I=require("node:http"),f=require("node:path"),{spawn:D}=require("node:child_process"),Z=4174,q=4173,F=96*1024,v="127.0.0.1",U="/__fivora_local_preview",ee=`(()=>{const p=["market","place"].join(""),R="__"+p.toUpperCase()+"_LOCAL_VISUAL_BRIDGE__",y=p.toUpperCase()+"_PREVIEW_",h=t=>y+t;if(window.__FIVORA_LOCAL_VISUAL_BRIDGE__||window[R])return;window.__FIVORA_LOCAL_VISUAL_BRIDGE__=!0,window[R]=!0;const O="FIVORA_PREVIEW_EDIT_MODE",V=h("EDIT_MODE"),x="FIVORA_PREVIEW_ELEMENT_CLICKED",P=h("ELEMENT_CLICKED"),G="FIVORA_PREVIEW_READY",U=h("READY"),T="data-preview-field-path",L="data-preview-list-path",v="data-preview-item-path",w="data-fivora-local-edit-target",A="data-fivora-local-selected-target",C="data-"+p+"-local-edit-target",_="data-"+p+"-local-selected-target",B="[data-preview-field-path], [data-preview-list-path], [data-preview-item-path]";let r=window.parent!==window,E=new Map,l=null,i=null,m="*";const g=document.createElement("style");g.setAttribute("data-fivora-local-visual-bridge",""),g.setAttribute("data-"+p+"-local-visual-bridge",""),g.textContent=["["+w+"], ["+C+"] {"," outline: 2px solid #06b6d4 !important;"," outline-offset: 3px !important;"," cursor: pointer !important;"," box-shadow: 0 0 0 5px rgba(6, 182, 212, 0.16) !important;","}","["+A+"], ["+_+"] {"," outline: 2px dashed #06b6d4 !important;"," outline-offset: 3px !important;"," box-shadow: 0 0 0 5px rgba(6, 182, 212, 0.18) !important;","}",'html[data-fivora-local-edit-mode="true"] [data-preview-field-path],','html[data-fivora-local-edit-mode="true"] [data-preview-list-path],','html[data-fivora-local-edit-mode="true"] [data-preview-item-path],','html[data-fivora-local-edit-mode="true"] [data-preview-field-path],','html[data-fivora-local-edit-mode="true"] [data-preview-list-path],','html[data-fivora-local-edit-mode="true"] [data-preview-item-path] {'," cursor: pointer !important;","}"].join(\`
3
+ \`),document.head.appendChild(g),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(r)),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(r));function Y(t){return t.source!==window.parent?!1:(m==="*"&&t.origin&&t.origin!=="null"&&(m=t.origin),m==="*"||t.origin===m)}function I(t){window.parent.postMessage(t,m)}function b(){l&&(l.removeAttribute(w),l.removeAttribute(C)),l=null}function F(t){i&&i!==t&&(i.removeAttribute(A),i.removeAttribute(_)),i=t,i.setAttribute(A,"true"),i.setAttribute(_,"true")}function W(){i&&(i.removeAttribute(A),i.removeAttribute(_)),i=null}function S(t){return t instanceof Element?t.closest(B):null}function k(t,e){const c=E.get(e);return c&&["string","number","boolean"].includes(typeof c.value)?c.value:t instanceof HTMLImageElement?t.currentSrc||t.src||"":(t.textContent||"").trim()}function K(t){if(!r)return;const e=S(t.target);!e||e===l||(b(),l=e,l.setAttribute(w,"true"))}function N(t){if(!r||!l)return;const e=t.relatedTarget;e instanceof Node&&l.contains(e)||b()}function j(t,e,c){const f=[],s=new Set(e?[e]:[]),n=c||e&&e.replace(/(\\[\\d+\\])?\\.\\w+$/,"").replace(/\\[\\d+\\]$/,"");if(n)for(const[a,o]of E)a!==e&&!s.has(a)&&(a.startsWith(n+".")||a.startsWith(n+"["))&&o?.kind!=="collection"&&(s.add(a),f.push({path:a,label:o.label||a.split(".").pop()||"Content",type:o.type||"text"}));let d=t instanceof Element?t:null,u=0;for(;d&&d!==document.body&&u<3;){const a=d.getAttribute(T);if(a&&a!==e&&!s.has(a)){const o=E.get(a);o?.kind!=="collection"&&(s.add(a),f.push({path:a,label:o?.label||a.split(".").pop()||"Content",type:o?.type||"text"}))}d=d.parentElement,u+=1}return f}function H(t){if(!r)return;const e=S(t.target);if(!e)return;t.preventDefault(),t.stopPropagation(),t.stopImmediatePropagation(),F(e);const c=e.closest("["+T+"]"),f=e.closest("["+L+"]"),s=e.closest("["+v+"]"),n=c?.getAttribute(T)||null,d=f?.getAttribute(L)||null,u=s?.getAttribute(v)||null,a=n?E.get(n):null,o=e.getBoundingClientRect(),D=u?.match(/\\[(\\d+)\\](?!.*\\[\\d+\\])/),M={type:x,fieldPath:n,fieldValue:n?k(c||e,n):"",elementTag:e.tagName.toLowerCase(),isImage:e instanceof HTMLImageElement||a?.type==="image",boundingRect:{top:o.top,left:o.left,width:o.width,height:o.height},collectionPath:d,listPath:d,itemPath:u,itemIndex:D?Number(D[1]):null,descriptorKind:n?"field":d?"collection":null,relatedFields:j(e,n,u)};I(M),I({...M,type:P})}window.addEventListener("message",t=>{!Y(t)||!t.data||typeof t.data!="object"||t.data.type!==O&&t.data.type!==V||(r=t.data.editMode===!0,E=new Map(Array.isArray(t.data.fields)?t.data.fields.filter(e=>e&&typeof e.path=="string").map(e=>[e.path,e]):[]),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(r)),document.documentElement.setAttribute("data-fivora-local-edit-mode",String(r)),r||(b(),W()))}),document.addEventListener("mouseover",K,!0),document.addEventListener("mouseout",N,!0),document.addEventListener("click",H,!0),I({type:G,pathname:window.location.pathname}),I({type:U,pathname:window.location.pathname})})();`;let w=ee;try{const e=require("./template-preview-focus-bridge.cjs");typeof e=="string"&&e.trim()&&(w=e)}catch{}const te=`var __name = typeof __name === "function" ? __name : ((target, value) => (typeof Object.defineProperty === "function" ? Object.defineProperty(target, "name", { value, configurable: true }) : target));
4
+ `;w.includes("__name")&&!w.includes("var __name")&&(w=`${te}${w}`);function p(e){process.stderr.write(`Local Template Lab: ${e}
5
+ `),process.exit(1)}try{new Function(w)}catch(e){p(`Visual editor bridge is invalid: ${e.message}`)}function W(e,t,a){if(e===void 0)return a;const n=Number(e);return(!Number.isInteger(n)||n<1024||n>65535)&&p(`${t} must be a port between 1024 and 65535.`),n}function ae(e){const t=[...e];let a="",n,r,s=!1;for(let m=0;m<t.length;m+=1){const d=t[m];d==="--api-port"?(n=t[m+1],m+=1):d.startsWith("--api-port=")?n=d.slice(11):d==="--preview-port"?(r=t[m+1],m+=1):d.startsWith("--preview-port=")?r=d.slice(15):d==="--skip-install"?s=!0:d==="--help"||d==="-h"?(process.stdout.write(["Fivora Local Template Lab","","Usage:"," npm run lab -- <template-directory> [options]","","Options:"," --api-port <port> Loopback controller port (default: 4174)"," --preview-port <port> Template dev-server port (default: 4173)"," --skip-install Do not install missing local dependencies",""].join(`
6
+ `)),process.exit(0)):d.startsWith("-")?p(`Unknown option: ${d}`):a?p(`Unexpected argument: ${d}`):a=d}return a||p("A template directory is required. Run with --help for usage."),{templatePath:f.resolve(a),apiPort:W(n,"--api-port",Z),previewPort:W(r,"--preview-port",q),skipInstall:s}}function B(e,t){try{return JSON.parse(_.readFileSync(e,"utf8"))}catch(a){p(`${t} is missing or invalid at ${e}: ${a instanceof Error?a.message:"unknown error"}`)}}const i=ae(process.argv.slice(2));_.existsSync(i.templatePath)||p(`Template directory does not exist: ${i.templatePath}`),_.statSync(i.templatePath).isDirectory()||p(`Template path must be a directory: ${i.templatePath}`);const ne=["fivora-template.json","fivora-template.json"].find(e=>_.existsSync(f.join(i.templatePath,e)))||"fivora-template.json",re=f.join(i.templatePath,ne),ie=f.join(i.templatePath,"package.json"),c=B(re,"Template manifest"),T=B(ie,"package.json");c.framework!=="nextjs-static-export"&&p('Template manifest framework must be "nextjs-static-export".'),(!T.scripts||typeof T.scripts.dev!="string")&&p("Template package.json must define a dev script for live preview."),(typeof c.siteDataFile!="string"||!c.siteDataFile.trim())&&p("Template manifest siteDataFile is required.");const N=f.resolve(i.templatePath,c.siteDataFile.trim()),j=f.relative(i.templatePath,N);(j.startsWith("..")||f.isAbsolute(j))&&p("Template manifest siteDataFile must stay inside the template directory.");const H=Q.randomBytes(24).toString("base64url"),y=`http://${v}:${i.apiPort}`,G=`http://${v}:${i.previewPort}`,$=`${y}${U}`,oe=f.join(__dirname,"deneb-template-validator.cjs");let P=!1,u=null,h=null,b=null,A=null,R=0;const k=5;let x="",C="";const o={protocolVersion:2,connected:!0,templateName:typeof c.name=="string"&&c.name.trim()?c.name.trim():typeof T.name=="string"?T.name:f.basename(i.templatePath),templatePath:i.templatePath,previewUrl:$,apiUrl:y,devStatus:"starting",devError:null,startedAt:new Date().toISOString(),validation:{status:"idle",startedAt:null,completedAt:null,exitCode:null}};function l(e,t){const a=t.toString();e==="dev"?x=`${x}${a}`.slice(-F):C=`${C}${a}`.slice(-F),process.stdout.write(a)}function Y(){return{...o,devLog:x,validationLog:C}}function se(e,t){const a=e.headers.origin;a&&/^(https?:\/\/|null$)/.test(a)&&(t.setHeader("Access-Control-Allow-Origin",a),t.setHeader("Vary","Origin")),t.setHeader("Access-Control-Allow-Headers","Authorization, Content-Type"),t.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),t.setHeader("Access-Control-Allow-Private-Network","true"),t.setHeader("Cache-Control","no-store"),t.setHeader("X-Content-Type-Options","nosniff")}function g(e,t,a){const n=JSON.stringify(a);e.statusCode=t,e.setHeader("Content-Type","application/json; charset=utf-8"),e.setHeader("Content-Length",Buffer.byteLength(n)),e.end(n)}function le(e){return e.headers.authorization===`Bearer ${H}`}function de(){try{return JSON.parse(_.readFileSync(N,"utf8"))}catch(e){throw new Error(`Unable to read ${c.siteDataFile}: ${e instanceof Error?e.message:"unknown error"}`)}}function pe(){return h?!1:(C="",o.validation={status:"running",startedAt:new Date().toISOString(),completedAt:null,exitCode:null},h=D(process.execPath,[oe,"validate",i.templatePath],{cwd:__dirname,env:process.env,stdio:["ignore","pipe","pipe"]}),h.stdout.on("data",e=>l("validation",e)),h.stderr.on("data",e=>l("validation",e)),h.on("error",e=>{l("validation",`
7
+ Unable to start validation: ${e.message}
8
+ `)}),h.on("close",e=>{o.validation={...o.validation,status:e===0?"passed":"failed",completedAt:new Date().toISOString(),exitCode:e},h=null}),!0)}function V(){if(P||o.devStatus==="ready")return;const e=I.get(G,t=>{if(t.resume(),t.statusCode&&t.statusCode<500){o.devStatus="ready",o.devError=null,R=0,process.stdout.write(`
9
+ Live preview ready: ${$}
10
+ `);return}b=setTimeout(V,600)});e.setTimeout(900,()=>e.destroy()),e.on("error",()=>{b=setTimeout(V,600)})}function ce(e){if(!(P||A)){if(R>=k){o.devStatus="failed",o.devError=e;return}R+=1,o.devStatus="starting",o.devError=null,l("dev",`
11
+ Preview server stopped during navigation (${e}). Restarting (${R}/${k})...
12
+ `),A=setTimeout(()=>{A=null,M()},900)}}function ue(){const e=f.join(i.templatePath,"node_modules");if(!i.skipInstall&&!_.existsSync(e)){o.devStatus="installing";const t=typeof c.installCommand=="string"&&c.installCommand.trim()?c.installCommand.trim():"npm install";l("dev",`Installing local dependencies with: ${t}
13
+ `);const a=D(t,{cwd:i.templatePath,env:process.env,shell:!0,stdio:["ignore","pipe","pipe"]});u=a,a.stdout.on("data",n=>l("dev",n)),a.stderr.on("data",n=>l("dev",n)),a.on("error",n=>{o.devStatus="failed",o.devError=n.message,u=null}),a.on("close",n=>{if(u=null,n!==0){o.devStatus="failed",o.devError=`Dependency installation exited with code ${n}.`;return}M()});return}M()}function M(){o.devStatus="starting",o.devError=null,l("dev",`Starting template source server on ${G}. Source edits will hot reload.
14
+ `);const e=process.platform==="win32"?"npm.cmd":"npm";u=D(e,["run","dev","--","--hostname",v,"--port",String(i.previewPort)],{cwd:i.templatePath,env:{...process.env,NEXT_PUBLIC_SITE_BASE_PATH:""},shell:process.platform==="win32",stdio:["ignore","pipe","pipe"]}),u.stdout.on("data",t=>l("dev",t)),u.stderr.on("data",t=>l("dev",t)),u.on("error",t=>{o.devStatus="failed",o.devError=t.message,u=null}),u.on("close",(t,a)=>{if(u=null,!P){const n=a?`signal ${a}`:`exit code ${t}`;ce(n)}}),V()}function me(){return`<!doctype html>
685
15
  <html lang="en">
686
16
  <head>
687
17
  <meta charset="utf-8">
@@ -699,7 +29,7 @@ function previewShellHtml() {
699
29
  <iframe id="template-preview" title="Local template preview"></iframe>
700
30
  <script>
701
31
  (() => {
702
- const BRIDGE_SOURCE = ${JSON.stringify(LOCAL_VISUAL_BRIDGE_SCRIPT)};
32
+ const BRIDGE_SOURCE = ${JSON.stringify(w)};
703
33
  const preview = document.getElementById('template-preview');
704
34
  const errorBox = document.getElementById('bridge-error');
705
35
  const PREVIOUS_PREVIEW_PREFIX = ['MARKET', 'PLACE'].join('') + '_PREVIEW_';
@@ -724,15 +54,19 @@ function previewShellHtml() {
724
54
  portalOrigin = event.origin;
725
55
  }
726
56
  if (portalOrigin !== '*' && event.origin !== portalOrigin) return;
727
- if (event.data.type === 'FIVORA_PREVIEW_EDIT_MODE' ||
57
+ if (event.data.type === 'FIVORA_PREVIEW_STYLE_PATCH') {
58
+ const key = 'STYLE_PATCH:' + (event.data.fieldPath || event.data.targetPath || 'default');
59
+ savedMessages.set(key, event.data);
60
+ } else if (event.data.type === 'FIVORA_PREVIEW_EDIT_MODE' ||
728
61
  event.data.type === previousPreviewMessage('EDIT_MODE') ||
729
62
  event.data.type === 'FIVORA_PREVIEW_SITE_DATA' ||
730
63
  event.data.type === previousPreviewMessage('SITE_DATA') ||
731
64
  event.data.type === 'FIVORA_PREVIEW_FOCUS_PAGE' ||
732
- event.data.type === previousPreviewMessage('FOCUS_PAGE')) {
65
+ event.data.type === previousPreviewMessage('FOCUS_PAGE') ||
66
+ event.data.type === 'FIVORA_PREVIEW_CONTENT_PATCH') {
733
67
  savedMessages.set(event.data.type, event.data);
734
68
  }
735
- if (childReady && (String(event.data.type || '').startsWith('FIVORA_PREVIEW_') || String(event.data.type || '').startsWith(PREVIOUS_PREVIEW_PREFIX))) {
69
+ if (String(event.data.type || '').startsWith('FIVORA_PREVIEW_') || String(event.data.type || '').startsWith(PREVIOUS_PREVIEW_PREFIX)) {
736
70
  sendToChild(event.data);
737
71
  }
738
72
  return;
@@ -760,7 +94,8 @@ function previewShellHtml() {
760
94
  try {
761
95
  const script = preview.contentDocument.createElement('script');
762
96
  script.setAttribute('data-fivora-local-visual-bridge', '');
763
- script.textContent = BRIDGE_SOURCE;
97
+ const NAME_SHIM = "var __name = typeof __name === 'function' ? __name : ((target, value) => (typeof Object.defineProperty === 'function' ? Object.defineProperty(target, 'name', { value, configurable: true }) : target)); ";
98
+ script.textContent = (BRIDGE_SOURCE.includes('__name') && !BRIDGE_SOURCE.includes('var __name') ? NAME_SHIM : '') + BRIDGE_SOURCE;
764
99
  preview.contentDocument.head.appendChild(script);
765
100
  preview.contentWindow.__FIVORA_LOCAL_VISUAL_BRIDGE_ATTACHED__ = true;
766
101
  script.remove();
@@ -783,247 +118,25 @@ function previewShellHtml() {
783
118
 
784
119
  preview.src = '/';
785
120
  })();
786
- </script>
121
+ <\/script>
787
122
  </body>
788
- </html>`;
789
- }
790
-
791
- function ignoreAbortedStreamError(error) {
792
- if (!error || typeof error !== 'object') return false;
793
- const code = 'code' in error ? String(error.code) : '';
794
- return (
795
- code === 'ECONNRESET' ||
796
- code === 'ECONNABORTED' ||
797
- code === 'EPIPE' ||
798
- code === 'ERR_STREAM_DESTROYED'
799
- );
800
- }
801
-
802
- function proxyPreviewRequest(request, response) {
803
- const headers = { ...request.headers };
804
- headers.host = `${LOOPBACK_HOST}:${options.previewPort}`;
805
- delete headers['accept-encoding'];
806
- delete headers.authorization;
807
-
808
- const upstream = http.request(
809
- {
810
- hostname: LOOPBACK_HOST,
811
- port: options.previewPort,
812
- method: request.method,
813
- path: request.url,
814
- headers,
815
- },
816
- (upstreamResponse) => {
817
- response.writeHead(
818
- upstreamResponse.statusCode || 502,
819
- upstreamResponse.statusMessage,
820
- upstreamResponse.headers,
821
- );
822
- upstreamResponse.on('error', (error) => {
823
- if (!ignoreAbortedStreamError(error)) {
824
- appendLog('dev', `\nPreview proxy upstream error: ${error.message}\n`);
825
- }
826
- if (!response.writableEnded) response.destroy();
827
- });
828
- response.on('error', (error) => {
829
- if (!ignoreAbortedStreamError(error)) {
830
- appendLog('dev', `\nPreview proxy response error: ${error.message}\n`);
831
- }
832
- upstreamResponse.destroy();
833
- });
834
- upstreamResponse.pipe(response);
835
- },
836
- );
837
- upstream.on('error', (error) => {
838
- if (!ignoreAbortedStreamError(error)) {
839
- appendLog('dev', `\nPreview proxy request error: ${error.message}\n`);
840
- }
841
- if (!response.headersSent) {
842
- sendJson(response, 502, {
843
- message: `Local preview is not ready: ${error.message}`,
844
- });
845
- } else if (!response.writableEnded) {
846
- response.destroy();
847
- }
848
- });
849
- request.on('error', (error) => {
850
- if (!ignoreAbortedStreamError(error)) {
851
- appendLog('dev', `\nPreview proxy client error: ${error.message}\n`);
852
- }
853
- upstream.destroy();
854
- });
855
- request.pipe(upstream);
856
- }
857
-
858
- const server = http.createServer((request, response) => {
859
- const url = new URL(request.url || '/', apiUrl);
860
- if (request.method === 'GET' && url.pathname === LOCAL_PREVIEW_ROUTE) {
861
- const html = previewShellHtml();
862
- response.writeHead(200, {
863
- 'Cache-Control': 'no-store',
864
- 'Content-Length': Buffer.byteLength(html),
865
- 'Content-Type': 'text/html; charset=utf-8',
866
- });
867
- response.end(html);
868
- return;
869
- }
870
- if (!url.pathname.startsWith('/api/')) {
871
- proxyPreviewRequest(request, response);
872
- return;
873
- }
874
-
875
- setCorsHeaders(request, response);
876
- if (request.method === 'OPTIONS') {
877
- response.statusCode = 204;
878
- response.end();
879
- return;
880
- }
881
-
882
- if (!isAuthorized(request)) {
883
- sendJson(response, 401, { message: 'Invalid Local Template Lab token.' });
884
- return;
885
- }
886
-
887
- if (request.method === 'GET' && url.pathname === '/api/status') {
888
- sendJson(response, 200, statusPayload());
889
- return;
890
- }
891
-
892
- if (request.method === 'GET' && url.pathname === '/api/site-data') {
893
- try {
894
- sendJson(response, 200, {
895
- manifest,
896
- siteData: readSiteData(),
897
- siteDataFile: manifest.siteDataFile,
898
- });
899
- } catch (error) {
900
- sendJson(response, 500, {
901
- message: error instanceof Error ? error.message : 'Unable to read site data.',
902
- });
903
- }
904
- return;
905
- }
906
-
907
- if (request.method === 'POST' && url.pathname === '/api/validate') {
908
- if (!startValidation()) {
909
- sendJson(response, 409, { message: 'Validation is already running.' });
910
- return;
911
- }
912
- sendJson(response, 202, statusPayload());
913
- return;
914
- }
915
-
916
- sendJson(response, 404, { message: 'Local Template Lab endpoint not found.' });
917
- });
918
-
919
- function pipeProxySockets(clientSocket, upstreamSocket) {
920
- const closeBoth = () => {
921
- if (!clientSocket.destroyed) clientSocket.destroy();
922
- if (!upstreamSocket.destroyed) upstreamSocket.destroy();
923
- };
924
- const onSocketError = (error) => {
925
- if (!ignoreAbortedStreamError(error)) {
926
- appendLog('dev', `\nPreview proxy socket error: ${error.message}\n`);
927
- }
928
- closeBoth();
929
- };
930
- clientSocket.on('error', onSocketError);
931
- upstreamSocket.on('error', onSocketError);
932
- clientSocket.on('close', () => {
933
- if (!upstreamSocket.destroyed) upstreamSocket.end();
934
- });
935
- upstreamSocket.on('close', () => {
936
- if (!clientSocket.destroyed) clientSocket.end();
937
- });
938
- upstreamSocket.pipe(clientSocket);
939
- clientSocket.pipe(upstreamSocket);
940
- }
941
-
942
- server.on('upgrade', (request, socket, head) => {
943
- if (request.url?.startsWith('/api/')) {
944
- socket.destroy();
945
- return;
946
- }
947
- socket.on('error', (error) => {
948
- if (!ignoreAbortedStreamError(error)) {
949
- appendLog('dev', `\nPreview proxy upgrade client error: ${error.message}\n`);
950
- }
951
- });
952
- const headers = { ...request.headers };
953
- headers.host = `${LOOPBACK_HOST}:${options.previewPort}`;
954
- const upstreamRequest = http.request({
955
- hostname: LOOPBACK_HOST,
956
- port: options.previewPort,
957
- method: request.method,
958
- path: request.url,
959
- headers,
960
- });
961
- upstreamRequest.on('upgrade', (upstreamResponse, upstreamSocket, upstreamHead) => {
962
- const responseHeaders = Object.entries(upstreamResponse.headers)
963
- .flatMap(([name, value]) => {
964
- const values = Array.isArray(value) ? value : [value];
965
- return values
966
- .filter((entry) => entry !== undefined)
967
- .map((entry) => `${name}: ${entry}`);
968
- })
969
- .join('\r\n');
970
- socket.write(
971
- `HTTP/1.1 ${upstreamResponse.statusCode || 101} ${upstreamResponse.statusMessage || 'Switching Protocols'}\r\n${responseHeaders}\r\n\r\n`,
972
- );
973
- if (head.length) upstreamSocket.write(head);
974
- if (upstreamHead.length) socket.write(upstreamHead);
975
- pipeProxySockets(socket, upstreamSocket);
976
- });
977
- upstreamRequest.on('error', (error) => {
978
- if (!ignoreAbortedStreamError(error)) {
979
- appendLog('dev', `\nPreview proxy upgrade upstream error: ${error.message}\n`);
980
- }
981
- if (!socket.destroyed) socket.destroy();
982
- });
983
- upstreamRequest.end();
984
- });
985
-
986
- function stopChild(child) {
987
- if (!child || child.killed) return;
988
- child.kill('SIGTERM');
989
- }
990
-
991
- function shutdown() {
992
- if (shuttingDown) return;
993
- shuttingDown = true;
994
- if (readinessTimer) clearTimeout(readinessTimer);
995
- if (devRestartTimer) clearTimeout(devRestartTimer);
996
- stopChild(validationProcess);
997
- stopChild(devProcess);
998
- server.close(() => process.exit(0));
999
- setTimeout(() => process.exit(0), 1500).unref();
1000
- }
1001
-
1002
- server.on('error', (error) => {
1003
- fail(
1004
- `Unable to start loopback controller at ${apiUrl}: ${
1005
- error instanceof Error ? error.message : 'unknown error'
1006
- }`,
1007
- );
1008
- });
1009
-
1010
- server.listen(options.apiPort, LOOPBACK_HOST, () => {
1011
- process.stdout.write(
1012
- [
1013
- '',
1014
- 'Fivora Local Template Lab is running.',
1015
- `Template: ${state.templateName}`,
1016
- `Controller URL: ${apiUrl}`,
1017
- `Connection token: ${controllerToken}`,
1018
- `Preview URL: ${previewUrl}`,
1019
- '',
1020
- 'Paste the controller URL and connection token into Developer Portal > Local Test Lab.',
1021
- 'Press Ctrl+C to stop. No template files are uploaded by this process.',
1022
- '',
1023
- ].join('\n'),
1024
- );
1025
- runInstallThenPreview();
1026
- });
1027
-
1028
- process.on('SIGINT', shutdown);
1029
- process.on('SIGTERM', shutdown);
123
+ </html>`}function E(e){if(!e||typeof e!="object")return!1;const t="code"in e?String(e.code):"";return t==="ECONNRESET"||t==="ECONNABORTED"||t==="EPIPE"||t==="ERR_STREAM_DESTROYED"}function fe(e,t){const a={...e.headers};a.host=`${v}:${i.previewPort}`,delete a["accept-encoding"],delete a.authorization;const n=I.request({hostname:v,port:i.previewPort,method:e.method,path:e.url,headers:a},r=>{t.writeHead(r.statusCode||502,r.statusMessage,r.headers),r.on("error",s=>{E(s)||l("dev",`
124
+ Preview proxy upstream error: ${s.message}
125
+ `),t.writableEnded||t.destroy()}),t.on("error",s=>{E(s)||l("dev",`
126
+ Preview proxy response error: ${s.message}
127
+ `),r.destroy()}),r.pipe(t)});n.on("error",r=>{E(r)||l("dev",`
128
+ Preview proxy request error: ${r.message}
129
+ `),t.headersSent?t.writableEnded||t.destroy():g(t,502,{message:`Local preview is not ready: ${r.message}`})}),e.on("error",r=>{E(r)||l("dev",`
130
+ Preview proxy client error: ${r.message}
131
+ `),n.destroy()}),e.pipe(n)}const S=I.createServer((e,t)=>{const a=new URL(e.url||"/",y);if(e.method==="GET"&&a.pathname===U){const n=me();t.writeHead(200,{"Cache-Control":"no-store","Content-Length":Buffer.byteLength(n),"Content-Type":"text/html; charset=utf-8"}),t.end(n);return}if(!a.pathname.startsWith("/api/")){fe(e,t);return}if(se(e,t),e.method==="OPTIONS"){t.statusCode=204,t.end();return}if(!le(e)){g(t,401,{message:"Invalid Local Template Lab token."});return}if(e.method==="GET"&&a.pathname==="/api/status"){g(t,200,Y());return}if(e.method==="GET"&&a.pathname==="/api/site-data"){try{g(t,200,{manifest:c,siteData:de(),siteDataFile:c.siteDataFile})}catch(n){g(t,500,{message:n instanceof Error?n.message:"Unable to read site data."})}return}if(e.method==="POST"&&a.pathname==="/api/validate"){if(!pe()){g(t,409,{message:"Validation is already running."});return}g(t,202,Y());return}g(t,404,{message:"Local Template Lab endpoint not found."})});function ve(e,t){const a=()=>{e.destroyed||e.destroy(),t.destroyed||t.destroy()},n=r=>{E(r)||l("dev",`
132
+ Preview proxy socket error: ${r.message}
133
+ `),a()};e.on("error",n),t.on("error",n),e.on("close",()=>{t.destroyed||t.end()}),t.on("close",()=>{e.destroyed||e.end()}),t.pipe(e),e.pipe(t)}S.on("upgrade",(e,t,a)=>{if(e.url?.startsWith("/api/")){t.destroy();return}t.on("error",s=>{E(s)||l("dev",`
134
+ Preview proxy upgrade client error: ${s.message}
135
+ `)});const n={...e.headers};n.host=`${v}:${i.previewPort}`;const r=I.request({hostname:v,port:i.previewPort,method:e.method,path:e.url,headers:n});r.on("upgrade",(s,m,d)=>{const J=Object.entries(s.headers).flatMap(([z,O])=>(Array.isArray(O)?O:[O]).filter(L=>L!==void 0).map(L=>`${z}: ${L}`)).join(`\r
136
+ `);t.write(`HTTP/1.1 ${s.statusCode||101} ${s.statusMessage||"Switching Protocols"}\r
137
+ ${J}\r
138
+ \r
139
+ `),a.length&&m.write(a),d.length&&t.write(d),ve(t,m)}),r.on("error",s=>{E(s)||l("dev",`
140
+ Preview proxy upgrade upstream error: ${s.message}
141
+ `),t.destroyed||t.destroy()}),r.end()});function K(e){!e||e.killed||e.kill("SIGTERM")}function X(){P||(P=!0,b&&clearTimeout(b),A&&clearTimeout(A),K(h),K(u),S.close(()=>process.exit(0)),setTimeout(()=>process.exit(0),1500).unref())}S.on("error",e=>{p(`Unable to start loopback controller at ${y}: ${e instanceof Error?e.message:"unknown error"}`)}),S.listen(i.apiPort,v,()=>{process.stdout.write(["","Fivora Local Template Lab is running.",`Template: ${o.templateName}`,`Controller URL: ${y}`,`Connection token: ${H}`,`Preview URL: ${$}`,"","Paste the controller URL and connection token into Developer Portal > Local Test Lab.","Press Ctrl+C to stop. No template files are uploaded by this process.",""].join(`
142
+ `)),ue()}),process.on("SIGINT",X),process.on("SIGTERM",X);