@deneb-ui/cli 2.0.28 → 2.0.29
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
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const path =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
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(
|
|
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 === '
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
<\/script>
|
|
787
122
|
</body>
|
|
788
|
-
</html
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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);
|